-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy path05-supple-design.dj
222 lines (172 loc) · 11.1 KB
/
05-supple-design.dj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# Supple Design
[^:therefore:]: *∴*{custom-style="center"}
To have a project accelerate as development proceeds---rather than get weighed
down by its own legacy---demands a design that is a pleasure to work with,
inviting to change. A _supple design._
Supple design is the complement to deep modeling.
Developers play two roles, each of which must be served by the design. The same
person might well play both roles---even switch back and forth in minutes---but
the relationship to the code is different nonetheless. One role is the developer
of a client, who weaves the domain objects into the application code or other
domain layer code, utilizing capabilities of the design. A supple design reveals
a deep underlying model that makes its potential clear. The client developer
can flexibly use a minimal set of loosely coupled concepts to express a range of
scenarios in the domain. Design elements fit together in a natural way with a
result that is predictable, clearly characterized, and robust.
Equally important, the design must serve the developer working to change it. To
be open to change, a design must be easy to understand, revealing that same
underlying model that the client developer is drawing on. It must follow the
contours of a deep model of the domain, so most changes bend the design at
flexible points. The effects of its code must be transparently obvious, so the
consequences of a change will be easy to anticipate.
- Making behavior obvious
- Reducing the cost of change
- Creating software developers to work with
## Intention-Revealing Interfaces
If a developer must consider the implementation of a component in order to use
it, the value of encapsulation is lost. If someone other than the original
developer must infer the purpose of an object or operation based on its
implementation, that new developer may infer a purpose that the operation or
class fulfills only by chance. If that was not the intent, the code may work for
the moment, but the conceptual basis of the design will have been corrupted, and
the two developers will be working at cross-purposes.
:therefore:
*Name classes and operations to describe their effect and purpose, without
reference to the means by which they do what they promise. This relieves the
client developer of the need to understand the internals. These names should
conform to the ubiquitous language so that team members can quickly infer their
meaning. Write a test for a behavior before creating it, to force your thinking
into client developer mode.*
## Side-Effect-Free Functions
Interactions of multiple rules or compositions of calculations become extremely
difficult to predict. The developer calling an operation must understand its
implementation and the implementation of all its delegations in order to
anticipate the result. The usefulness of any abstraction of interfaces is
limited if the developers are forced to pierce the veil. Without safely
predictable abstractions, the developers must limit the combinatory explosion,
placing a low ceiling on the richness of behavior that is feasible to build.
:therefore:
*Place as much of the logic of the program as possible into functions, operations
that return results with no observable side effects. Strictly segregate commands
(methods which result in modifications to observable state) into very simple
operations that do not return domain information. Further control side effects
by moving complex logic into value objects when a concept fitting the
responsibility presents itself.*
*All operations of a value object should be side-effect-free functions.*
## Assertions
When the side effects of operations are only defined implicitly by their
implementation, designs with a lot of delegation become a tangle of cause and
effect. The only way to understand a program is to trace execution through
branching paths. The value of encapsulation is lost. The necessity of tracing
concrete execution defeats abstraction.
:therefore:
*State post-conditions of operations and invariants of classes and aggregates. If
assertions cannot be coded directly in your programming language, write
automated unit tests for them. Write them into documentation or diagrams where
it fits the style of the project's development process.*
Seek models with coherent sets of concepts, which lead a developer to infer the
intended assertions, accelerating the learning curve and reducing the risk of
contradictory code.
Assertions define contracts of services and entity modifiers.
Assertions define invariants on aggregates.
## Standalone Classes
Even within a module, the difficulty of interpreting a design increases wildly
as dependencies are added. This adds to mental overload, limiting the design
complexity a developer can handle. Implicit concepts contribute to this load
even more than explicit references.
:therefore:
*Low coupling is fundamental to object design. When you can, go all the way.
Eliminate all other concepts from the picture. Then the class will be completely
self-contained and can be studied and understood alone. Every such
self-contained class significantly eases the burden of understanding a module.*
## Closure of Operations
Most interesting objects end up doing things that can't be characterized by
primitives alone.
:therefore:
*Where it fits, define an operation whose return type is the same as the type of
its argument(s). If the implementer has state that is used in the computation,
then the implementer is effectively an argument of the operation, so the
argument(s) and return value should be of the same type as the implementer. Such
an operation is closed under the set of instances of that type. A closed
operation provides a high-level interface without introducing any dependency on
other concepts.*
This pattern is most often applied to the operations of a value object. Because
the life cycle of an entity has significance in the domain, you can't just
conjure up a new one to answer a question. There are operations that are closed
under an entity type. You could ask an Employee object for its supervisor and
get back another Employee. But in general, entities are not the sort of concepts
that are likely to be the result of a computation. So, for the most part, this
is an opportunity to look for in the value objects.
You sometimes get halfway to this pattern. The argument matches the implementer,
but the return type is different, or the return type matches the receiver and
the argument is different. These operations are not closed, but they do give
some of the advantage of closure, in freeing the mind.
## Declarative Design
There can be no real guarantees in procedural software. To name just one way of
evading assertions, code could have additional side effects that were not
specifically excluded. No matter how model-driven our design is, we still end up
writing procedures to produce the effect of the conceptual interactions. And we
spend much of our time writing boilerplate code that doesn't really add any
meaning or behavior. Intention-revealing interfaces and the other patterns in
this chapter help, but they can never give conventional object-oriented programs
formal rigor.
These are some of the motivations behind declarative design. This term means
many things to many people, but usually it indicates a way to write a program,
or some part of a program, as a kind of executable specification. A very precise
description of properties actually controls the software. In its various forms,
this could be done through a reflection mechanism or at compile time through
code generation (producing conventional code automatically, based on the
declaration). This approach allows another developer to take the declaration at
face value. It is an absolute guarantee.
Many declarative approaches can be corrupted if the developers bypass them
intentionally or unintentionally. This is likely when the system is difficult to
use or overly restrictive. Everyone has to follow the rules of the framework in
order to get the benefits of a declarative program.
### A Declarative Style of Design
Once your design has intention-revealing interfaces, side-effect-free functions,
and assertions, you are edging into declarative territory. Many of the benefits
of declarative design are obtained once you have combinable elements that
communicate their meaning, and have characterized or obvious effects, or no
observable effects at all.
A supple design can make it possible for the client code to use a declarative
style of design. To illustrate, the next section will bring together some of
the patterns in this chapter to make the specification more supple and
declarative.
## Drawing on Established Formalisms
Creating a tight conceptual framework from scratch is something you can't do every day.
Sometimes you discover and refine one of these over the course of the life of a project. But
you can often use and adapt conceptual systems that are long established in your domain or
others, some of which have been refined and distilled over centuries. Many business
applications involve accounting, for example. Accounting defines a well-developed set of
entities and rules that make for an easy adaptation to a deep model and a supple design.
There are many such formalized conceptual frameworks, but my personal favorite is math. It
is surprising how useful it can be to pull out some twist on basic arithmetic. Many domains
include math somewhere. Look for it. Dig it out. Specialized math is clean, combinable by
clear rules, and people find it easy to understand.
A real-world example, "Shares Math," was discussed in Chapter 8 of the book, Domain-Driven
Design.
## Conceptual Contours
Sometimes people chop functionality fine to allow flexible combination.
Sometimes they lump it large to encapsulate complexity. Sometimes they seek a
consistent granularity, making all classes and operations to a similar scale.
These are oversimplifications that don't work well as general rules. But they
are motivated by basic problems.
When elements of a model or design are embedded in a monolithic construct, their
functionality gets duplicated. The external interface doesn't say everything a
client might care about. Their meaning is hard to understand, because different
concepts are mixed together.
Conversely, breaking down classes and methods can pointlessly complicate the
client, forcing client objects to understand how tiny pieces fit together.
Worse, a concept can be lost completely. Half of a uranium atom is not uranium.
And of course, it isn't just grain size that counts, but just where the grain
runs.
:therefore:
*Decompose design elements (operations, interfaces, classes, and aggregates) into
cohesive units, taking into consideration your intuition of the important
divisions in the domain. Observe the axes of change and stability through
successive refactorings and look for the underlying conceptual contours that
explain these shearing patterns. Align the model with the consistent aspects of
the domain that make it a viable area of knowledge in the first place.*
A supple design based on a deep model yields a simple set of interfaces that
combine logically to make sensible statements in the ubiquitous language, and
without the distraction and maintenance burden of irrelevant options.