Supple Design (p. 244):
"A lot of overengineering has been justified in the name of flexibility. But more often than not, excessive layers of abstraction and indirection get in the way. Look at the design of software that really empowers the people who handle it; you will usually see something simple."
Evans goes on to say that the early version of software will probably be rigid and stiff. And that many do not even acquire the suppleness that he is talking about.
What Evans says is a good point. But it seems to be very hard to achieve. I have seen systems with a lot of overengineering and lots of accompanying abstraction layers piled onto it. It's hard to understand it all at first. But after looking at it for a while, trying things out and actually reading the source code, things are not that bad overall. And when you and the other developers get into this mindset where you understand the code so well, you find it really hard to be able to break out of the box and actually evaluate the complexity of the design. At least until it has become some big ball of mud and then no one knows what to do with it anymore.
In this chapter, Evans offers some patterns that lend suppleness to a design. Combined with the advice from the previous chapter on how to actually dig deeper at a domain model, this might be able to illustrate what a supple design is and how to achieve it.
- Intention-Revealing Interfaces
This is actually similar to a combination of ">Fowler's uncommunicative name and inconsistent names code smell. You can eliminate them by renaming the class, method or interface to something that quickly conveys the intent of the code without forcing the developer to go into the details of the implementation. Also, the new name should be consistent with the ubiquitous language.
- Side-Effect-Free Functions
Seems like this pattern should be called Side-Effect-Free Operations since in this section, Evans already defines the term function as an operation that returns results without producing side effects.
Sometimes, certain operations need to be combined together to achieve a desired result. The order which they are combined might matter because each operation might propagate side effects that "prepare" the system for the next operation. For instance, when reading from a file, you have do first open the file, scan to the segment that you want to read from and then start reading to the points where you are interested in. The file stream has a certain state that needs to be taken into consideration. When the order matters, the developer has to take special care in combining those operations, forcing him/her to go into the details of the code to find out the order of doing things.
To solve this problem, you should favor side-effect-free operations i.e. functions. And when it is possible to do so, abstract complex values into their own Value Object.
Ruby has this convention of using the ! character for methods that change the internal state. This makes it more explicit which functions have side effects.
1 array = ['a', 'b', 'c', 'c', 'd', 'e', 'e' ] # create array of characters 2 # uniq removes duplication in a array but maintains order 3 array.uniq # => ["a", "b", "c", "d", "e"] without modifying array 4 array # => ["a", "b", "c", "c", "d", "e", "e"] 5 array.uniq! # => ["a", "b", "c", "d", "e"] array is now different 6 array # => ["a", "b", "c", "d", "e"] a
Sometimes it is not possible to use side-effect-free operations especially when dealing with Entities. In such cases, you can help make those side-effects more explicit by placing assertions for pre-conditions, post-conditions and invariants.
Languages such as Eiffel support assertions as part of the language. For languages that do not support them natively, Evans suggests making unit tests to enforce those assertions. So, right now, unit test do not only test for correctness, but they also convey implicit details to the developers. Unit tests are concrete examples that show how to actually use different objects and methods together.
Assertions are also part of the design-by-contract methodology.
- Conceptual Contours
This pattern deals with how you should decompose/ compose your components (classes, methods, modules) so that they do not feel fragmented. For instance, making your components very fine-grained might allow for a lot of flexibility in usage, but it also leads to an explosion of details that need to be managed. On the other hand, making your components very high-level makes it easy to achieve common tasks but it becomes hard to do other tasks. For example, doing bit manipulation in Java is hard because it does not have good support for that.
The important thing is to realize how your developers will be using those classes. Java developers do not do a lot of bit manipulation so it is all right to provide them with functions that deal with integers instead of bits and bytes. You find out how your developers use certain components by observing how the system has evolved through refactorings. After various refactorings, you will find that certain components are fairly stable; they are your conceptual contours. However, they are not set in stone. Future refactorings might remove those contours for better ones.
- Standalone Classes
Classes that depend too much on one another promote unhealthy coupling. Coupling makes it hard for developers to understand the code and make changes to it since one change could easily affect another class. In the extreme case, some classes should be able to function in isolation.
A good way to detect for coupling, is to check for inappropriate intimacy, feature envy and message chain code smells. Some metric suites might also be able to detect unhealthy coupling between classes.
- Closure on Operations
I think this pattern is based on the principle of least surprise. For instance, if I give a method some argument, I would expect that the method also returns something that is conceptually similar to the type of the argument. If I give the argument 1 - an
int- to the function increment, I would expect it to return another integer but with the value incremented. It might return a
doublebut I would not expect it to suddenly return a
char. It's this coherence between the argument and the return values that makes it easy to see what the operation is actually doing.
Some of these refactorings that have been suggested are pretty small and some are more involved. But they do impact the overall readability and maintainability of the code. The most important thing when doing these refactorings is to understand the fact that these are only rules of thumbs. Refactoring is not a one way process in this case. Sometimes, a previous refactoring might work better and you would have to revert back to it.
Evans also metions Domain-Specific Languages in this chapter as a way to tie the model and the implementation more tightly. He cautions that while the binding between the model and the implementation might be better, it might be a maintenance nightmare to keep the domain-specific language in tune with the ubiquitous language. Also, certain languages like Scheme, Smalltalk and Ruby are more suited for domain-specific language than others like Java and C#.
1 puts Time.now #=> Tue May 10 17:03:43 CDT 2005 2 puts 20.minutes.ago #=> Tue May 10 16:43:43 CDT 2005 3 puts 20.hours.from_now #=> Wed May 11 13:03:43 CDT 2005 4 puts 20.weeks.from_now #=> Tue Sep 27 17:03:43 CDT 2005 5 puts 20.months.ago #=> Thu Sep 18 17:03:43 CDT 2003
Domain-Specific Languages should be readable (verbally) and still make sense Tweet
comments powered by Disqus