Consider this quote from Bob Martin, recently found in an old newsgroup post:
Not all functionality relevant to a domain object should be a method of
a domain object. For example, consider a modem class. A behavior that
is relevant to a modem is “ConfigureForUnix”. However this behavior should
not be a method of Modem since there is no end to them. You will be adding
ConfigureForXXX methods to the modem class for every new operating system
that comes along. Rather, we would like to see the ConfigureForXXX behaviors
placed into command objects decoupled from the Modem class.
It is a mistake I have been guilty of in the past – I have a domain class and I’ve been told that I should encapsulate logic inside of my objects, so I create methods on my class. For example, I may create a “Save” method that knows how to save the object, and a “Load” method that knows how to retrieve it from the database.
But the simple fact is that logic needs context. Sometimes the context is all inside of the class itself (variables and properties), and that is well and good. But not always. For domain objects it is in fact common to need access to some sort of external context (how was this object created) or environment (what is the value of setting X).
Hard won experience has taught me that finding that context from inside of a domain object method can create complexity (events, obscure flag variables, etc.) and introduce coupling. I definitely don’t want that!
The solution is to put the code outside of the domain objects. Some might say that this throws away encapsulation, breaking one of the primary rules of OO. Put that way, it definitely sounds like a bad idea! The first time someone brought that point up, I was at a loss for words. They were 100% correct – my finely thought through design was breaking encapsulation! That could not be good. Or could it? Read on…
The ideal of a domain object with properties and methods, magically encapsulating all data and behavior is ultimately just too simplistic. Objects do not live in isolation. Behavior is defined only in terms of an environment. Let me explain that using a simple example…
I can shout loudly, and if you are standing near me in my office, you will hear me. But alter the environment, say by placing us floating in space, and I can shout all I like without you hearing a thing. In OO terms, I can add a “Shout” method to my Person class, but the environment is never static, so eventually my method is doomed to failure, and my class is doomed to become legacy code, forced to be re-written because it is coupled too tightly to its environment.
If I really really want to have a Shout method to my Person class (and that is by no means certain), then I have to provide the environment (or context) as a parameter to that method, and have the actual shouting be implemented inside of the environment. That is the Visitor pattern.
Another alternative is to provide a singleton-style environment, implemented by a Registry pattern. This approach can free you from the need to pass the context around via parameters all of the time.
Incidentally, Test Driven Design will lead you naturally to one of these approaches, because you will need to test the Shout method, and the only way to do that is to provide a mockup of the environment/context that can verify that the Shout method actually tried to do something.
A last thought on encapsulation, and the title of this post. The environment (the woods) encapsulates behavior. If a tree falls in the woods:
Tree.Fall(woods)
then the sound is communicated to all within hearing distance. Perhaps only a log hears the sound:
Public Sub HandleSound(sound) Handles Woods.Sound Trace.WriteLine("A sound was heard")End Sub
Or perhaps nothing hears it and the question remains unanswered.