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.