Taking into consideration that I'm making an attempt at Domain-Driven Design and overall good separation of concerns when it comes to business logic, data access logic, etc…
Question: How do you validate a child object, when validation depends on one or more fields of a parent object? What about when the parent object's validation depends on the state of child objects?
Let's use a simple, contrived example: annual employee performance reviews.
John, a manager at his company, needs to complete a review for each of his subordinates. Collectively, all of the reviews John needs to perform this year are referred to as a Review Process. When John closes all of the reviews in his Review Process, the Review Process itself can be closed.
He's using my system to create, update, and close employee reviews. The system defines a ReviewProcess
object, and it contains a List(Of Reviews)
belonging to that Review Process, as follows:
Public Class ReviewProcess
Public Property ReviewProcessID As Integer
Public Property EmployeeID As Integer
Public ReadOnly Property Reviews As List(Of Review)
Public Property CloseDate As Date
Public Sub Add(review As Review)
_Reviews.Add(review)
End Sub
End Class
Public Class Review
Public Property ReviewID As Integer
Public Property ReviewProcessID As Integer
Public Property EmployeeID As Integer
Public Property Rating As Integer
Public Property Comments As String
Public Property CloseDate As Date
End Class
John needs to add a new review to his Review Process. In the UI, he clicks Add, selects Steve's Employee ID, and clicks save:
Private Sub SaveClickEvent(sender As Object, e As EventArgs) Handles btnSave.Click
' _reviewProcess is a form-level variable, already instantiated when the form is opened.
Dim newReview As New Review With {
.ReviewProcessID = _reviewProcess.ReviewProcessID,
.EmployeeID = DirectCast(cboEmployees.SelectedItem, Employee).EmployeeID
}
Dim service As New ReviewService
Dim result As New ValidationResult
result = service.Create(newReview)
If result.IsValid Then
_reviewProcess.Add(newReview)
Else
' Show the validation errors to the user.
End If
End Sub
Inside the ReviewService.Create() function, the following code is found. A FluentValidation validator is instantiated to check the that the new Review is valid to save to the database:
Public Class ReviewService
Public Function Create(item As Review) As ValidationResult
Dim validator As New ReviewValidator
Dim results As ValidationResult = validator.Validate(item, ruleSet:="OnSave")
' SE question: how/where do we validate that the parent ReviewProcess does not have a close date?
If results.IsValid Then
Dim repository As New ReviewDataAccess
repository.Create(item)
End If
Return results
End Function
End Class
For completeness, here is the ReviewValidator class:
Public Class ReviewValidator
Inherits AbstractValidator(Of Review)
Public Sub New()
RuleSet("OnSave",
Sub()
RuleFor(Function(x) x.ReviewProcessID).NotEmpty
RuleFor(Function(x) x.EmployeeID).NotEmpty
End Sub
)
RuleSet("OnClose",
Sub()
RuleFor(Function(x) x.Rating).NotEmpty
RuleFor(Function(x) x.Comments).NotEmpty
RuleFor(Function(x) x.CloseDate).NotEmpty
End Sub
)
End Sub
End Class
This is where I'm having a breakdown of DDD/OOP understanding. The architecture I have described above is fine, until I hit the business rule that "If a Review Process is closed, new Reviews cannot be added."
Repeating the question:
Question: How do you validate a child object, when validation depends on one or more fields of a parent object? What about when the parent object's validation depends on the state of child objects?
Sure, I can ensure that the UI doesn't enable the Add button when the _reviewProcess.CloseDate <> Nothing
, but that's obviously not in line with separation of concerns. I want to ensure this business rule is in the right place. This is where my question lies. There is also a business rule that a Review Process cannot be closed unless all of the Reviews that it owns are also closed. How does this fit into my design, if at all? Maybe I have to rethink and redo the design (with your help)?
I understand that maybe my design is way off base. Feedback on that is welcome.
Best Answer
Well I don't know if this will suffice to be a satisfying answer to your question or not, if not I hope that it can still help in some way, shape or form!
Answer (sort of??)
So I am just going to come right out and espouse my personal opinion (being based off solid design principles) and say that I would STRONGLY recommend against the general design of a component, in which there is ever a child object which must have some direct knowledge of its own parent. I do say this knowing that there are a handful of very esoteric scenarios in which this sort of thing would be acceptable, but for the most part I think that this would be introducing a level of complexity and coupling which is antithesis to the whole idea of creating an inheritance structure to begin with.
SOLUTION
Now that being said, I would propose that in a scenario like you give as an example...
The proper solution, as far as your software design goes would be to employ the power of Events (assuming c# is used, although you could use some sort of callback for other languages without such first class features). This solution would look something like the following:
ReviewProcess
is created, it should register an event listener to the given event, something such asOnReviewClosed
.ReviewProcess
should also be tracking whichReview
object is raising the event (pass this information via event args). Just to ensure that you could rule out any edge cases where someone closes the same review several times and counts for all of them, etc.Review
class so that it knows what information should be included in the event, in other words, what information does theReviewProcess
need to be aware of on the event.OnClick
to trigger theReview.Complete()
method which in turn would raise the event.This sort of approach offers the following main benefits
Review
object is marked as closed without having to modify any exiting code and run the risk of braking code elsewhere because of the weird backwards coupling.Review
class which is not really the responsibility of theReview
itself, but rather some tertiary service which it should belong to naturally!