2011.04.07
Objects and Data Structures
Procedural code (code using data structures) makes it easy to add new functions without
changing the existing data structures. OO code, on the other hand, makes it easy to add
new classes without changing existing functions.
The complement is also true:
Procedural code makes it hard to add new data structures because all the functions must
change. OO code makes it hard to add new functions because all the classes must change.
So, the things that are hard for OO are easy for procedures, and the things that are
hard for procedures are easy for OO!
In any complex system there are going to be times when we want to add new data
types rather than new functions. For these cases objects and OO are most appropriate. On
the other hand, there will also be times when we’ll want to add new functions as opposed
to data types. In that case procedural code and data structures will be more appropriate.
Mature programmers know that the idea that everything is an object is a myth. Sometimes
you really do want simple data structures with procedures operating on them.
The Law of Demeter
There is a well-known heuristic called the Law of Demeter that says a module should not
know about the innards of the objects it manipulates.
They are indicative of a muddled design whose authors are unsure of—or worse, ignorant of—whether they need protection from functions or types.
i look on indicative of as a compound preposition.
2011.04.12
public class PerDiemMealExpenses implements MealExpenses {
public int getTotal() {
// return the per diem default
}
}
This is called the SPECIAL CASE PATTERN [Fowler]. You create a class or configure an
object so that it handles a special case for you. When you do, the client code doesn’t have
to deal with exceptional behavior. That behavior is encapsulated in the special case object.
If you work in a code base with code like this, it might not look all that bad to you, but it is
bad! When we return null, we are essentially creating work for ourselves and foisting
problems upon our callers. All it takes is one missing null check to send an application
spinning out of control .
If you code this way, you will minimize the chance of NullPointerExceptions and your
code will be cleaner.
you should avoid passing null in your code whenever possible.
Don't return null, don't pass null.
In most programming languages there is no good way to deal with a null that is
passed by a caller accidentally. Because this is the case, the rational approach is to forbid
passing null by default. When you do, you can code with the knowledge that a null in an
argument list is an indication of a problem, and end up with far fewer careless mistakes.
Clean code is readable, but it must also be robust. These are not conflicting goals. We can
write robust clean code if we see error handling as a separate concern, something that is
viewable independently of our main logic. To the degree that we are able to do that, we can
reason about it independently, and we can make great strides in the maintainability of our
code.
Learning tests verify that the third-party packages we are using work the way we
expect them to .
Without these boundary tests to ease the migration, we might be tempted to stay with the old version longer than we should.
We had a pretty good idea of where our world ended and the new world began. As we
worked, we sometimes bumped up against this boundary. Though mists and clouds of
ignorance obscured our view beyond the boundary, our work made us aware of what we
wanted the boundary interface to be.
In the Figure above, you can see that we insulated the CommunicationsController classes
from the transmitter API (which was out of our control and undefined). By using our own
application specific interface, we kept our CommunicationsController code clean and
expressive. Once the transmitter API was defined, we wrote the TransmitterAdapter to
bridge the gap . The ADAPTER encapsulated the interaction with the API and provides a
single place to change when the API evolves .
Interesting things happen at boundaries. Change is one of those things. Good software
designs accommodate change without huge investments and rework. When we use code
that is out of our control, special care must be taken to protect our investment and make
sure future change is not too costly.
Code at the boundaries needs clear separation and tests that define expectations. We
should avoid letting too much of our code know about the third-party particulars. It’s better
to depend on something you control than on something you don’t control, lest it end up
controlling you.
We manage third-party boundaries by having very few places in the code that refer to
them. We may wrap them as we did with Map, or we may use an ADAPTER to convert from
our perfect interface to the provided interface. Either way our code speaks to us better,
promotes internally consistent usage across the boundary, and has fewer maintenance
points when the third-party code changes.