Recently I discovered that the most flexible solution would be to create a special object that just transfers the data between the steps. You don't have to inject anything, if you want some custom data for the second step, you just create your data object manually. Without the dependency injection and inheritance requirements, the steps become completely isolated. You need some sort of controller to drive the flow, and your users can easily implement another controller with any of the steps replaced with different objects and different behaviour.
However, sometimes you, like me, are attached to the old fashioned idea of keeping behaviour with data. You need your class to expose the property and calculate its value:
Public Overridable ReadOnly Property SomeProperty() As SomeClass
Get
Dim result As New SomeClass
'Set some properties
Return result
End Get
End Property
Private _someProperty As SomeClass
Public Property SomeProperty() As SomeClass
Get
If _someProperty Is Nothing Then _someProperty = CalculateValue()
Return _someProperty
End Get
Set(ByVal value As SomeClass)
_someProperty = value
End Set
End Property
Protected Overridable Function CalculateValue() As SomeClass
Dim result As New SomeClass
' Set some properties
Return result
End Function
A similar pattern can be applied to value objects. The trick is to make the associated private variable nullable:
Private _someProperty As Nullable(Of Boolean)
Public Overridable Property SomeProperty() As Boolean
Get
If Not _someProperty.HasValue Then _someProperty = CalculateValue()
Return _someProperty
End Get
Set(ByVal value As Boolean)
_someProperty = value
End Set
End Property
Protected Overridable Function CalculateValue() As Boolean
'Do stuff
End Function
Finally, let's try a similar thing with enumerables. Typically, we expose an enumerable as a readonly property, and fill the values at the initialization stage. Here we do a similar trick, but on a certain condition. We add a boolean property AutoCalculateValues and calculate them only if it is set to true, which is the default behaviour:
Private _autoCalculateValues As Boolean = True
Public Property AutoCalculateValues() As Boolean
Get
Return _autoCalculateValues
End Get
Set(ByVal value As Boolean)
_autoCalculateValues = value
End Set
End Property
Private _someProperty As IList(Of SomeClass)
Public Overridable ReadOnly Property SomeProperty() As IEnumerable(Of SomeClass)
Get
If _someProperty Is Nothing Then
_someProperty = New List(Of SomeClass)
End If
Return _someProperty
End Get
End Property
Protected Overridable Sub CalculateValues(ByVal list As IList(Of SomeClass))
list.Add(New SomeClass)
'...
End Sub
This time, the property is not settable, but the idea that it is initialized by default but the initialization can be switched off.
So, what are the advantages? In terms of testability, we can set the desired values directly, instead of using mocks, which simplifies our life a bit, and our tests become even more clear. But the main point is about reuse. Testability with mocks implies that one can modify the behaviour of our class by inheritance. A user of our framework can create a descendant of our class, overriding the property, and inject it into the workflow, just as we do in our tests. Making the property settable, on the other hand, makes it possible to extend the behaviour by composition: a user can, for example, add another class that sets the property in response to some external conditions. Another, more important, possibility, is to set the property manually. For example, on a Web page, the block elements are usually go under each other, but sometimes you want to position your div manually. You don't override the div class, you just tell it, "don't calculate your position, I'll do it myself". Note that the old inheritance possibility is still there -- you can override the CalculateValue() method if you want.
So, the system definitely becomes more open. This will definitely make the users of your component happier, but also opens more possibilities for misuse. For example, I could set the value that would put the system into an invalid state. However, the same result can be "achieved" by inheritance, only with more efforts. So, apart from validation, the best solution is writing docs properly. And remember that what seems like misusing to you might be a perfect way of using for others.
I'm using these ideas for some time with Inka, and I discovered that my design has improved. Each time I'm tempted to mock a parameterless method, I'm trying to convert it into a property, and then make it settable. I discovered that I have less dependencies and better decoupling, less pulls and more pushes, less methods that cover all possible situations, more small classes specifically designed for each situation.
No comments:
Post a Comment