latefee) {
127 | return new Entities.Customer(
128 | latefee.customer().id().value(),
129 | latefee.customer().address(),
130 | latefee.state().id()
131 | );
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Data Oriented Programming in Java
2 |
3 | Source code for the book Data Oriented Programming in Java (by me! Chris Kiehl!)
4 |
5 |
6 |
7 |
8 |
9 | * [Get the book here!](https://mng.bz/BgQv)
10 | * ISBN: 9781633436930
11 |
12 | > [!Note]
13 | > This book is in Early Access while I continue to work on it. This repository will be updated as new chapters are released.
14 |
15 | This book is a distillation of everything I’ve learned about what effective development looks like in Java (so far!). It’s what’s left over after years of experimenting, getting things wrong (often catastrophically), and
16 | slowly having anything resembling “devotion to a single paradigm” beat out of me by the great humbling filter that is reality.
17 |
18 | Data-orientation doesn't replace object orientation. The two work together and enhance each other. DoP is born from a very simple idea, and one that people have been repeatedly rediscovering since the dawn of computing: “representation is the essence of programming”. Programs that are organized around the data they manage tend to be simpler, smaller, and significantly easier understand. When we do a really good job of capturing the data in our domain, the rest of the system tends to fall into place in a way which can feel like it’s writing itself.
19 |
20 | ## Getting Started with this project
21 |
22 | To download a copy of this repository, click on the [Download ZIP](https://github.com/chriskiehl/Data-Oriented-Programming-In-Java-Book/archive/refs/heads/main.zip) button or execute the following command in your terminal:
23 |
24 | ```
25 | git clone https://github.com/chriskiehl/Data-Oriented-Programming-In-Java-Book.git
26 | ```
27 |
28 | (If you downloaded the code bundle from the Manning website, please consider visiting the official code repository on GitHub at https://github.com/chriskiehl/Data-Oriented-Programming-In-Java-Book for the latest updates.)
29 |
30 | The project is built with [Gradle](https://gradle.org/).
31 |
32 | ```
33 | gradle build
34 | ```
35 |
36 | ### Running the code
37 |
38 | The `tests/` package houses all of the runnable code. You can run all the tests in a class with this command:
39 |
40 | ```
41 | gradle test --tests 'path.to.test.Class'
42 | ```
43 | e.g.
44 | ```
45 | gradle test --tests 'dop.chapter02.Listings'
46 | ```
47 |
48 | You can also run individual tests by specifying the method.
49 |
50 | ```
51 | gradle test --tests 'dop.chapter02.Listings.listing_2_1'
52 | ```
53 |
54 |
55 |
56 | ### How to use this repository
57 |
58 | Each chapter in the book has an associated package in the `src/test/` directory. Most of the examples aren't necessarily things that we'll run. They're primarily for study. We'll look at them and go "Hmm. Interesting." DoP is a book that's about design decisions and how they affect our code. We're striving to make incorrect states impossible to express or compile. Thus, a lot of the examples are exploring how code changes (or _disappears_ entirely) when we get our modeling right.
59 |
60 | **Listings in the Book vs Code**
61 |
62 | Each listing in the book will have a corresponding example in the code. The Javadoc will describe which listing the code applies to.
63 |
64 | ```
65 | /**
66 | * ───────────────────────────────────────────────────────
67 | * Listing 1.1
68 | * ───────────────────────────────────────────────────────
69 | *
70 | * Here's an example of how we might traditionally model
71 | * data "as data" using a Java object.
72 | * ───────────────────────────────────────────────────────
73 | */
74 | ```
75 |
76 | Sometimes, separate listings in the book will be combined into one example in the code.
77 |
78 | ```
79 | /**
80 | * ───────────────────────────────────────────────────────
81 | * Listings 1.5 through 1.9
82 | * ───────────────────────────────────────────────────────
83 | * Representation affects our ability to understand the code
84 | * as a whole. [...]
85 | * ───────────────────────────────────────────────────────
86 | */
87 | ```
88 |
89 | > [!Note]
90 | > The class names in the code will often differ from the class names used in the book. Java doesn't let us redefine classes over and over again (which we do in the book as we refactor), so we 'cheat' by appending a qualifying suffix. For instance, `ScheduledTask` in listing A might become `ScheduledTaskV2` or `ScheduledTaskWithBetterOOP` in a subsequent example code. The listing numbers in the Javadoc will always tie to the Listing numbers in the book.
91 |
92 |
93 | **Character Encodings**
94 |
95 | Make sure your IDE / Text editor is configured for UTF-8 character encoding (Windows tends to default to other encodings). Many of the in-code diagrams leverage the utf-8 charset.
96 |
97 | Example utf-8 diagram:
98 | ```
99 | // An informational black hole!
100 | //
101 | // ┌──────── It returns nothing!
102 | // ▼
103 | // void reschedule( ) { // ◄─────────────────────────────────┐
104 | // ... ▲ │ Compare how very different
105 | // } └────── It takes nothing! │ these two methods are in
106 | // │ terms of what they convey
107 | RetryDecision reschedule(FailedTask failedTask) { // ◄───┘ to us as readers
108 | // ...
109 | }
110 | ```
111 |
112 |
113 | **Classes inside of methods**
114 |
115 | > [!Note]
116 | > Some listings will have classes defined in the body of the listing. These are only there because Java doesn't allow us to define records with forward references directly in a method's body, whereas doing it inside a class is fine. Thus, the kinda wonky occasional pattern of class(method(class (record, record, record, etc.)))
117 |
118 |
119 | ```
120 | class Chapter05 {
121 | void listing5_13() {
122 | class __ { ◄─────────────────────────────────┐ This nested class is here becuase Java doesn't support
123 | │ certain forward references / type hierarchies if you try
124 | // ... │ to define them directly in the body of the method.
125 | // rest of example code │ Wrapping all the definitions in their own class allows
126 | // ... │ things to (mostly) behave as expected.
127 | }
128 | }
129 | }
130 | ```
131 |
132 |
133 | ## Table of Contents
134 |
135 | | Chapter | Code Listings |
136 | |-----------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
137 | | Chapter 01 - Data Oriented Programming | [Listings.java](https://github.com/chriskiehl/Data-Oriented-Programming-In-Java-Book/blob/main/app/src/test/java/dop/chapter01/Listings.java) |
138 | | Chapter 02 - Data, Identity, and Values | [Listings.java](https://github.com/chriskiehl/Data-Oriented-Programming-In-Java-Book/blob/main/app/src/test/java/dop/chapter02/Listings.java) |
139 | | Chapter 03 - Data and Meaning | [Listings.java](https://github.com/chriskiehl/Data-Oriented-Programming-In-Java-Book/blob/main/app/src/test/java/dop/chapter03/Listings.java) |
140 | | Chapter 04 - Representation is the Essence of Programming | [Listings.java](https://github.com/chriskiehl/Data-Oriented-Programming-In-Java-Book/blob/main/app/src/test/java/dop/chapter04/Listings.java) |
141 | | Chapter 05 - Modeling Domain Behaviors | [Listings.java](https://github.com/chriskiehl/Data-Oriented-Programming-In-Java-Book/blob/main/app/src/test/java/dop/chapter05/Listings.java) |
142 | | Chapter 06 - Implementing the domain model | [Listings.java](https://github.com/chriskiehl/Data-Oriented-Programming-In-Java-Book/blob/main/app/src/test/java/dop/chapter06/Listings.java) |
143 | | Chapter 07 - Guiding the design with properties | Coming soon! |
144 | | Chapter 08 - Business Rules as Data | [Listings.java](https://github.com/chriskiehl/Data-Oriented-Programming-In-Java-Book/blob/main/app/src/test/java/dop/chapter08/Listings.java) |
145 |
146 |
147 |
148 | ## Questions and Feedback
149 |
150 | I'd love to hear any and all feedback. You can leave comments in the [Manning forum](https://livebook.manning.com/forum?product=kiehl&page=1). I'm also very responsive to emails. If you have a question about the repo, feel free to write me at me@chriskiehl.com.
151 |
152 |
153 |
154 |
155 |
156 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | # This is normally unused
84 | # shellcheck disable=SC2034
85 | APP_BASE_NAME=${0##*/}
86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
88 |
89 | # Use the maximum available, or set MAX_FD != -1 to use that value.
90 | MAX_FD=maximum
91 |
92 | warn () {
93 | echo "$*"
94 | } >&2
95 |
96 | die () {
97 | echo
98 | echo "$*"
99 | echo
100 | exit 1
101 | } >&2
102 |
103 | # OS specific support (must be 'true' or 'false').
104 | cygwin=false
105 | msys=false
106 | darwin=false
107 | nonstop=false
108 | case "$( uname )" in #(
109 | CYGWIN* ) cygwin=true ;; #(
110 | Darwin* ) darwin=true ;; #(
111 | MSYS* | MINGW* ) msys=true ;; #(
112 | NONSTOP* ) nonstop=true ;;
113 | esac
114 |
115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
116 |
117 |
118 | # Determine the Java command to use to start the JVM.
119 | if [ -n "$JAVA_HOME" ] ; then
120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
121 | # IBM's JDK on AIX uses strange locations for the executables
122 | JAVACMD=$JAVA_HOME/jre/sh/java
123 | else
124 | JAVACMD=$JAVA_HOME/bin/java
125 | fi
126 | if [ ! -x "$JAVACMD" ] ; then
127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
128 |
129 | Please set the JAVA_HOME variable in your environment to match the
130 | location of your Java installation."
131 | fi
132 | else
133 | JAVACMD=java
134 | if ! command -v java >/dev/null 2>&1
135 | then
136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 | fi
142 |
143 | # Increase the maximum file descriptors if we can.
144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
145 | case $MAX_FD in #(
146 | max*)
147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
148 | # shellcheck disable=SC2039,SC3045
149 | MAX_FD=$( ulimit -H -n ) ||
150 | warn "Could not query maximum file descriptor limit"
151 | esac
152 | case $MAX_FD in #(
153 | '' | soft) :;; #(
154 | *)
155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
156 | # shellcheck disable=SC2039,SC3045
157 | ulimit -n "$MAX_FD" ||
158 | warn "Could not set maximum file descriptor limit to $MAX_FD"
159 | esac
160 | fi
161 |
162 | # Collect all arguments for the java command, stacking in reverse order:
163 | # * args from the command line
164 | # * the main class name
165 | # * -classpath
166 | # * -D...appname settings
167 | # * --module-path (only if needed)
168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
169 |
170 | # For Cygwin or MSYS, switch paths to Windows format before running java
171 | if "$cygwin" || "$msys" ; then
172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
174 |
175 | JAVACMD=$( cygpath --unix "$JAVACMD" )
176 |
177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
178 | for arg do
179 | if
180 | case $arg in #(
181 | -*) false ;; # don't mess with options #(
182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
183 | [ -e "$t" ] ;; #(
184 | *) false ;;
185 | esac
186 | then
187 | arg=$( cygpath --path --ignore --mixed "$arg" )
188 | fi
189 | # Roll the args list around exactly as many times as the number of
190 | # args, so each arg winds up back in the position where it started, but
191 | # possibly modified.
192 | #
193 | # NB: a `for` loop captures its iteration list before it begins, so
194 | # changing the positional parameters here affects neither the number of
195 | # iterations, nor the values presented in `arg`.
196 | shift # remove old arg
197 | set -- "$@" "$arg" # push replacement arg
198 | done
199 | fi
200 |
201 |
202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
204 |
205 | # Collect all arguments for the java command:
206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
207 | # and any embedded shellness will be escaped.
208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
209 | # treated as '${Hostname}' itself on the command line.
210 |
211 | set -- \
212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
213 | -classpath "$CLASSPATH" \
214 | org.gradle.wrapper.GradleWrapperMain \
215 | "$@"
216 |
217 | # Stop when "xargs" is not available.
218 | if ! command -v xargs >/dev/null 2>&1
219 | then
220 | die "xargs is not available"
221 | fi
222 |
223 | # Use "xargs" to parse quoted args.
224 | #
225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
226 | #
227 | # In Bash we could simply go:
228 | #
229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
230 | # set -- "${ARGS[@]}" "$@"
231 | #
232 | # but POSIX shell has neither arrays nor command substitution, so instead we
233 | # post-process each arg (as a line of input to sed) to backslash-escape any
234 | # character that might be a shell metacharacter, then use eval to reverse
235 | # that process (while maintaining the separation between arguments), and wrap
236 | # the whole thing up as a single "set" statement.
237 | #
238 | # This will of course break if any of these variables contains a newline or
239 | # an unmatched quote.
240 | #
241 |
242 | eval "set -- $(
243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
244 | xargs -n1 |
245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
246 | tr '\n' ' '
247 | )" '"$@"'
248 |
249 | exec "$JAVACMD" "$@"
250 |
--------------------------------------------------------------------------------
/app/src/test/java/dop/chapter01/Listings.java:
--------------------------------------------------------------------------------
1 | package dop.chapter01;
2 |
3 | import dop.chapter01.Listings.RetryDecision.ReattemptLater;
4 | import dop.chapter01.Listings.RetryDecision.RetryImmediately;
5 | import org.junit.jupiter.api.Test;
6 |
7 | import java.time.LocalDateTime;
8 | import java.util.List;
9 | import java.util.Objects;
10 | import java.util.UUID;
11 |
12 | import static dop.chapter01.Listings.RetryDecision.*;
13 | import static java.time.LocalDateTime.now;
14 |
15 | /**
16 | * Chapter 01 is a whirlwind tour of the main ideas of data-oriented
17 | * programming. It can all be summed up in a single phrase from Fred
18 | * Brooks: "representation is the essence of programming."
19 | */
20 | public class Listings {
21 |
22 |
23 | /**
24 | * ───────────────────────────────────────────────────────
25 | * Listing 1.1
26 | * ───────────────────────────────────────────────────────
27 | *
28 | * Here's an example of how we might traditionally model
29 | * data "as data" using a Java object.
30 | * ───────────────────────────────────────────────────────
31 | */
32 | @Test
33 | public void listing_1_1() {
34 | class Point {
35 | private final double x; // ◄── The object is entirely defined by these attributes.
36 | private final double y; // ◄── It has no behaviors. It has no hidden state. It's "just" data.
37 |
38 | // We omit everything below in the chapter for brevity. But we need
39 | // all this stuff in order to get an object in Java to behave like a
40 | // value rather than an identity (we'll explore that idea in detail
41 | // in chapter 02!)
42 | public Point(double x, double y) {
43 | this.x = x;
44 | this.y = y;
45 | }
46 |
47 | // Equality is very important when modeling data, but that's a topic
48 | // for chapter 02 ^_^
49 | @Override
50 | public boolean equals(Object o) {
51 | if (this == o) return true;
52 | if (o == null || getClass() != o.getClass()) return false;
53 | Point point = (Point) o;
54 | return Double.compare(x, point.x) == 0 && Double.compare(y, point.y) == 0;
55 | }
56 |
57 | @Override
58 | public int hashCode() {
59 | return Objects.hash(x, y);
60 | }
61 |
62 | // Any accessors we define manually will be done without the
63 | // Java Bean style `get` prefix. This is so that our transition to
64 | // records is easy.
65 | public double x() {
66 | return x;
67 | }
68 |
69 | public double y() {
70 | return y;
71 | }
72 | }
73 | }
74 |
75 |
76 | /**
77 | * ───────────────────────────────────────────────────────
78 | * Listing 1.2
79 | * ───────────────────────────────────────────────────────
80 | * This example is all about the ambiguity that most of our
81 | * representations carry with them. Checkout this attribute
82 | * we've called ID. What goes there? What does the data type
83 | * of String communicate to us as we read the code? Nothing!
84 | * String could be any infinite number of things.
85 | * ───────────────────────────────────────────────────────
86 | */
87 | static class AmbiguousRepresentationExample {
88 | String id; // ◄──┐ DoP is largely just the act of noticing
89 | // │ that code like this is extremely vague.
90 | }
91 |
92 |
93 | /**
94 | * ───────────────────────────────────────────────────────
95 | * Listing 1.3
96 | * ───────────────────────────────────────────────────────
97 | * The representation we've picked for the ID, despite being
98 | * trivial and one line, captures the soul of data oriented
99 | * programming. Our representation **creates** the possibility
100 | * for invalid program states. There are more wrong ways to create
101 | * this thing we've called ID than there are correct ones!
102 | * In fact, we still have no idea what ID really means. All we
103 | * can do with the current code is guess.
104 | * ───────────────────────────────────────────────────────
105 | */
106 | public static void wrongWaysOfCreatingAnId() {
107 | // Every one of these is valid from the perspective of
108 | // how we've represented the idea in the type system.
109 | // Every one of these is invalid because they're all values
110 | // which don't belong to our domain (which hasn't been
111 | // communicated to us in the code)
112 | String id;
113 | id = "not-a-valid-uuid";
114 | id = "Hello World!";
115 | id = "2024-05-04";
116 | id = "2024-05-04";
117 | id = "1010011001011011";
118 | }
119 |
120 |
121 | /**
122 | * ───────────────────────────────────────────────────────
123 | * Listing 1.4
124 | * ───────────────────────────────────────────────────────
125 | * Here's the magic of representation. I don't have to tell
126 | * you out of band what that ID is supposed to be. You don't
127 | * have to read an internal wiki, or ask a coworker, or look
128 | * inside the database. We can make the code itself communicate
129 | * what ID means by picking a better representation.
130 | * ───────────────────────────────────────────────────────
131 | */
132 | static class ImprovedRepresentation {
133 | UUID id; // ◄───┐ THIS tells us exactly what that ID should be! A UUID.
134 | // │ Not an arbitrary string. Not a Product Code or SKU.
135 | // │ ID is a UUID. Try to give it anything else and your
136 | // │ code won't compile.
137 | }
138 |
139 |
140 | /**
141 | * ───────────────────────────────────────────────────────
142 | * Listings 1.5 & 1.6
143 | * ───────────────────────────────────────────────────────
144 | * Representation affects our ability to understand the code
145 | * as a whole. The class below, ScheduledTask, is an example
146 | * I stole (after simplifying and anonymizing) from a project
147 | * I worked on. Without knowing anything other than that fact
148 | * that it deals with scheduling (which we can tell from its
149 | * name), the challenge we take on in the chapter is simply
150 | * trying to understand what the heck the `reschedule() method
151 | * is trying to do.
152 | * ───────────────────────────────────────────────────────
153 | */
154 | static class ScheduledTask {
155 | private LocalDateTime scheduledAt;
156 | private int attempts;
157 |
158 |
159 | //────────────────────────────────────────────────────────────────┐
160 | void reschedule() { //│ Checkout this method. What does it do?
161 | if (this.someSuperComplexCondition()) { //│ Or, more specifically, what does it mean?
162 | this.setScheduledAt(now().plusSeconds(this.delay())); //│ It clearly assigns some values to some
163 | this.setAttempts(this.attempts() + 1); //│ variables, but... we as newcomers to this
164 | } else if (this.someOtherComplexCondition()) { //│ code, what information can we extract from
165 | this.setScheduledAt(this.standardInterval()); //│ just this method?
166 | this.setAttempts(0); //│
167 | } else { //│
168 | this.setScheduledAt(null); //│
169 | this.setAttempts(0); //│
170 | } //│
171 | } //│
172 | // ───────────────────────────────────────────────────────────────┘
173 |
174 | //───────────────────────────────────────────────────┐
175 | boolean someSuperComplexCondition() { // │ Note!
176 | return false; // │ These are just here so the code will
177 | } // │ compile. They return fixed junk values.
178 | boolean someOtherComplexCondition() { // │ They should be ignored
179 | return false; // │ for the purposes of the exercise.
180 | } // │
181 | int delay() { // │
182 | return 0; // │
183 | } // │
184 | // │
185 | private LocalDateTime standardInterval() { // │
186 | return now(); // │
187 | } // │
188 | //───────────────────────────────────────────────────┘
189 |
190 | LocalDateTime scheduledAt() {
191 | return scheduledAt;
192 | }
193 |
194 | int attempts() {
195 | return attempts;
196 | }
197 |
198 | public void setScheduledAt(LocalDateTime scheduledAt) {
199 | this.scheduledAt = scheduledAt;
200 | }
201 |
202 | public void setAttempts(int attempts) {
203 | this.attempts = attempts;
204 | }
205 | }
206 |
207 |
208 | /**
209 | * ───────────────────────────────────────────────────────
210 | * Listings 1.7
211 | * ───────────────────────────────────────────────────────
212 | * The problem with the example above (listing 1.5 & 1.6) is
213 | * that we can't tell what any of it means. The code doesn't
214 | * tell us why it's setting those variables. Instead, we have
215 | * to piece it together "non-locally" by hunting down clues in
216 | * other parts of the codebase.
217 | * ───────────────────────────────────────────────────────
218 | */
219 | static class Scheduler {
220 | List tasks;
221 |
222 | // (Imagine a bunch of other methods here...)
223 |
224 | // If we're lucky, we might eventually stumble on one that
225 | // explains what the heck a particular state means.
226 | // In this case, we figure out that if we set scheduledAt to
227 | // null, that implicitly means that the scheduler should remove
228 | // this tasks and give up on it.
229 | private void pruneTasks() {
230 | this.tasks.removeIf((task) -> task.scheduledAt() == null);
231 | }
232 | }
233 |
234 |
235 | /**
236 | * (This modeling is not shown in the book for brevity)
237 | * We're creating a family of related data types. The mechanics of this
238 | * construct will be covered in Chapters 3 and 4.
239 | */
240 | sealed interface RetryDecision {
241 | record RetryImmediately(LocalDateTime next, int attemptsSoFar) implements RetryDecision {
242 | }
243 |
244 | record ReattemptLater(LocalDateTime next) implements RetryDecision {
245 | }
246 |
247 | record Abandoned() implements RetryDecision {
248 | }
249 | }
250 |
251 |
252 | /**
253 | * ───────────────────────────────────────────────────────
254 | * Listings 1.8
255 | * ───────────────────────────────────────────────────────
256 | * The thing we strive for in data-oriented programming is
257 | * to be able to communicate effectively within the code.
258 | * We want to use a representation that tells any reader
259 | * exactly what we're trying to accomplish. Those opaque
260 | * variable assignments can be made clear by giving them
261 | * names. Naming is a magical thing with tons of power!
262 | * ───────────────────────────────────────────────────────
263 | */
264 | static class ScheduledTaskV2 {
265 | // We've replaced the ambiguous instance variables with
266 | // a new descriptive data type.
267 | private RetryDecision status; // ◄── NEW!
268 |
269 |
270 | // Checkout how different this code *feels*.
271 | // it now tells us exactly what it does.
272 | // It chooses between 1 of 3 possible actions:
273 | // * Retry Immediately
274 | // * Attempt this later
275 | // * Abandon it entirely
276 | void reschedule() {
277 | if (this.someSuperComplexCondition()) {
278 | this.setStatus(new RetryImmediately( // ◄── NEW!
279 | now().plusSeconds(this.delay()),
280 | this.attempts(status) + 1
281 | ));
282 | } else if (this.someOtherComplexCondition()) {
283 | this.setStatus(new ReattemptLater(this.standardInterval())); // ◄── NEW!
284 | } else {
285 | this.setStatus(new Abandoned()); // ◄── NEW!
286 | }
287 | }
288 |
289 | //───────────────────────────────────────────────────┐
290 | boolean someSuperComplexCondition() { // │ Note!
291 | return false; // │ These are just here so the code will
292 | } // │ compile. They return fixed junk values
293 | boolean someOtherComplexCondition() { // │ because they're supposed to be ignored
294 | return false; // │ for the purposes of the exercise.
295 | } // │
296 | int delay() { // │
297 | return 0; // │
298 | } // │
299 | private LocalDateTime standardInterval() { // │
300 | return now(); // │
301 | } // │
302 | private int attempts(RetryDecision decision) { // │
303 | return 1; // │
304 | } // │
305 | //───────────────────────────────────────────────────┘
306 |
307 | private void setStatus(RetryDecision decision) {
308 | this.status = decision;
309 | }
310 |
311 | public RetryDecision status() {
312 | return this.status;
313 | }
314 | }
315 |
316 |
317 | /**
318 | * ───────────────────────────────────────────────────────
319 | * Listings 1.9 & 1.10
320 | * ───────────────────────────────────────────────────────
321 | * Good modeling has a simplifying effect on the entire
322 | * codebase. We can refactor other parts of the code to
323 | * use the domain concepts. It replaces "hmm... Well, null
324 | * must mean that..." style reasoning with concrete, declarative
325 | * code that tells you *exactly* what it means.
326 | * ───────────────────────────────────────────────────────
327 | */
328 | static class SchedulerV2 {
329 | List tasks;
330 |
331 | // (Imagine a bunch of other methods here...)
332 |
333 | // Refactoring to use our explicit data type
334 | private void pruneTasks() {
335 | // Don't let this instanceof scare you off!
336 | // This would be "bad" when doing OOP and dealing with objects (with
337 | // identities), but we're not doing OOP. We're programming with data.
338 | this.tasks.removeIf((task) -> task.status() instanceof Abandoned);
339 | // Compare this to the original version!
340 | // [Original]: this.tasks.removeIf((task) -> task.scheduledAt() == null);
341 | }
342 | }
343 |
344 |
345 | /**
346 | * ───────────────────────────────────────────────────────
347 | * Listings 1.11
348 | * ───────────────────────────────────────────────────────
349 | * You might argue that the problem with the original code
350 | * was that it leaked information. It didn't define domain
351 | * level API methods for consumers. Fair! Let's see what
352 | * that looks like.
353 | * ───────────────────────────────────────────────────────
354 | */
355 | static class ScheduledTaskWithBetterOOP {
356 | private LocalDateTime scheduledAt; // we might keep the design of
357 | private int attempts; // these attributes the same
358 |
359 |
360 | void reschedule() {
361 | // body omitted for brevity
362 | }
363 |
364 | //──────────────────────────────────────┐
365 | public boolean isAbandoned() { //│ And use this to hide the implementation details while
366 | return this.scheduledAt == null; //│ also clarifying what the state assignments mean.
367 | } //│ A nice improvement!
368 | //──────────────────────────────────────┘
369 | }
370 |
371 |
372 | /**
373 | * ───────────────────────────────────────────────────────
374 | * Listings 1.12
375 | * ───────────────────────────────────────────────────────
376 | * The improvements from Listing 1.11 ripple outward in a
377 | * similar way to the improvements we made in listing 1.9 & 1.10.
378 | * ───────────────────────────────────────────────────────
379 | */
380 | static class SchedulerV3 {
381 | List tasks;
382 |
383 | // (Imagine a bunch of other methods here...)
384 |
385 | private void pruneTasks() {
386 | // The API method nicely clarifies what the state of the
387 | // task means. This is much better than an ambiguous null check.
388 | this.tasks.removeIf((task) -> task.isAbandoned());
389 | }
390 | }
391 |
392 |
393 | /**
394 | * ───────────────────────────────────────────────────────
395 | * Listings 1.13
396 | * ───────────────────────────────────────────────────────
397 | * Luckily, it's not one or the other. It's not OOP vs DoP.
398 | * We can combine the strengths of both approaches.
399 | * ───────────────────────────────────────────────────────
400 | */
401 |
402 |
403 | class ScheduledTaskWithBestOfBothWorlds {
404 | private RetryDecision status;
405 |
406 | void reschedule() {
407 | /// ...
408 | }
409 |
410 | public boolean isAbandoned() {
411 | // By combing the approaches, we get a nice internal
412 | // representation to program against. We can still use
413 | // OOP to control the interfaces. Further, doesn't this
414 | // code feel almost like it's writing itself? Of course
415 | // we'd expose this method from our object -- it's a
416 | // core idea we uncovered while modeling the data!
417 | return this.status instanceof Abandoned;
418 | }
419 | }
420 |
421 |
422 | /**
423 | * ───────────────────────────────────────────────────────
424 | * Listings 1.14 & 1.15
425 | * ───────────────────────────────────────────────────────
426 | * We've made a lot of improvements to the implementation, but
427 | * the method signature is still super vague.
428 | * ───────────────────────────────────────────────────────
429 | */
430 | class FixingScheduledTasksRemainingAmbiguity {
431 | private RetryDecision status;
432 |
433 | // An informational black hole!
434 | //
435 | // ┌──────── It returns nothing!
436 | // ▼
437 | void reschedule( ) { // ◄─────────────────────────────────┐
438 | // ... ▲ │ Compare how very different
439 | } // └────── It takes nothing! │ these two methods are in
440 | // │ terms of what they convey
441 | RetryDecision rescheduleV2(FailedTask failedTask) { // ◄───┘ to us as readers
442 | // ▲ ▲
443 | // │ └── takes a failed task
444 | // │
445 | // └── and tells us what to do with it
446 |
447 | return null; // {We'll implement this in a followup example}
448 | }
449 | }
450 |
451 | static class FailedTask {
452 | // blank. Here just to enable
453 | // compilation of listing 1.14 above
454 | // The fact that it tells us quite a bit without
455 | // us implementing anything is pretty nice feature!
456 | }
457 |
458 |
459 | /**
460 | * ───────────────────────────────────────────────────────
461 | * Listings 1.16
462 | * ───────────────────────────────────────────────────────
463 | * Letting the data types guide our refactoring of the
464 | * reschedule method.
465 | * ───────────────────────────────────────────────────────
466 | */
467 | class DataDrivenRefactoringOfScheduledTask {
468 | private RetryDecision status;
469 |
470 | // What's powerful about this refactoring is that it makes our
471 | // code describe exactly what it is to everyone who reads it.
472 | // No wikis or external 'design docs' needed. There's no secret
473 | // institutional knowledge. The code teaches us what we need to
474 | // know about how retries are handled in this domain.
475 | RetryDecision reschedule(FailedTask failedTask) {
476 | return switch (failedTask) {
477 | case FailedTask task when someSuperComplexCondition(task) ->
478 | new RetryImmediately(now(), attempts(status) + 1);
479 | case FailedTask task when someOtherComplexCondition(task) ->
480 | new ReattemptLater(this.standardInterval());
481 | default -> new Abandoned();
482 | };
483 | }
484 |
485 | // As with the prior listing. The implementation for all
486 | // the methods below are just placeholders. We don't care
487 | // (or even *want* to care) about their implementation details.
488 | // We should be able to reason "above" the details by looking
489 | // at the high level data types in the code.
490 | //───────────────────────────────────────────────────────┐
491 | boolean someSuperComplexCondition(FailedTask task) { // │
492 | return false; // │
493 | } // │
494 | boolean someOtherComplexCondition(FailedTask task) { // │
495 | return false; // │
496 | } // │
497 | int delay() { // │
498 | return 0; // │
499 | } // │
500 | private LocalDateTime standardInterval() { // │
501 | return now(); // │
502 | } // │
503 | private int attempts(RetryDecision decision) { // │
504 | return 1; // │
505 | } // │
506 | //───────────────────────────────────────────────────────┘
507 | }
508 |
509 |
510 |
511 | /**
512 | * ───────────────────────────────────────────────────────
513 | * Listings 1.17
514 | * ───────────────────────────────────────────────────────
515 | * The more we listen to the data, the more it will shape
516 | * how we write our code. "Design" becomes simply noticing
517 | * when we can make our code clearer.
518 | * ───────────────────────────────────────────────────────
519 | */
520 | // Note:
521 | // This code for this listing is commented out as it relies on
522 | // syntax that will not compile in Java in order to clarify the
523 | // output of the method. In latest chapters, we'll learn how to
524 | // model this kind of output in Java.
525 | // static class SketchingTheRunMethodForTheScheduler {
526 | //
527 | //
528 | // Scheduled Tasks ──────────────────────┐
529 | // │
530 | // ▼
531 | // private run(ScheduledTask task) {
532 | // ... ────────────────────────────
533 | // } ▲
534 | // │
535 | // │
536 | // └─────── Get turned into either Completed
537 | // Tasks or Failed tasks
538 | // }
539 | }
540 |
--------------------------------------------------------------------------------
/app/src/test/java/dop/chapter03/Listings.java:
--------------------------------------------------------------------------------
1 | package dop.chapter03;
2 |
3 | import org.junit.jupiter.api.Assertions;
4 | import org.junit.jupiter.api.Test;
5 |
6 | import java.util.List;
7 | import java.util.Map;
8 | import java.util.UUID;
9 | import java.util.function.BiFunction;
10 | import java.util.stream.Collectors;
11 |
12 | import static java.util.stream.Collectors.*;
13 |
14 |
15 | /**
16 | * Chapter 3 is about starting to explore the semantics
17 | * that govern the data within a domain. It looks at the
18 | * gaps that usually exist between what we "know" in our
19 | * heads about the things we're modeling, versus how much
20 | * of that knowledge actually ends up in the code (very
21 | * little).
22 | *
23 | * This chapter will give you the tools to see "through"
24 | * your programs into the underlying sets of values that
25 | * it denotes.
26 | */
27 | public class Listings {
28 |
29 |
30 | /**
31 | * ───────────────────────────────────────────────────────
32 | * Listings 3.1
33 | * ───────────────────────────────────────────────────────
34 | * We begin where all good software books begin: woodworking.
35 | * The best part of woodworking is the last step of applying the
36 | * finish. One of my favorite finishes is a type of drying oil
37 | * called Tung oil. I geek out over this stuff and have been
38 | * slowly collecting data around its drying behavior.
39 | *
40 | * In real life, nobody lets me talk about this because it's too
41 | * boring. But you're stuck reading my book, so I'm not going to
42 | * waste my one chance to drone on about my data on oil curing rates.
43 | *
44 | * It's interesting. I swear.
45 | * ───────────────────────────────────────────────────────
46 | */
47 | void listing_3_1() {
48 | // Here's our data in Json form. We're going to turn it
49 | // into Java and see what we can learn along the way.
50 | // {
51 | // "sampleId”: "UV-1",
52 | // "day": 3, ◄─── Tung oil cures painfully slow. It's measured in full days.
53 | // "contactAngle": 17.4 ◄──┐ We can't see it curing, but we can measure it! This is
54 | // } │ the angle that a droplet of water forms on the surface of the wood
55 | }
56 |
57 |
58 |
59 | /**
60 | * ───────────────────────────────────────────────────────
61 | * Listings 3.2
62 | * ───────────────────────────────────────────────────────
63 | * Our first stab at representing this in Java might be a
64 | * mechanical translation. text-like things in the json
65 | * becomes Strings in Java. Numbers in the Json become ints
66 | * or doubles in Java.
67 | * ───────────────────────────────────────────────────────
68 | */
69 | void listing_3_2() {
70 | record Measurement(
71 | String sampleId, // ◄───┐
72 | int daysElapsed, // │ A very JSON-like modeling
73 | double contactAngle // │
74 | ) {}
75 | }
76 |
77 |
78 | /**
79 | * ───────────────────────────────────────────────────────
80 | * Listings 3.3
81 | * ───────────────────────────────────────────────────────
82 | * Taking a closer look at our data. Each of these fields
83 | * is about something. It has a meaning within its domain.
84 | * Have we captured that meaning in our code? Let's look
85 | * at the data again.
86 | * ───────────────────────────────────────────────────────
87 | */
88 | void listing_3_3() {
89 | // {
90 | // "sampleId”: "UV-1",
91 | // "day": 3, // ◄─── What the heck kind of measurement is "days"?
92 | // "contactAngle": 17.4 // ◄─── 17.4... *what*?
93 | // }
94 | }
95 |
96 |
97 |
98 | /**
99 | * ───────────────────────────────────────────────────────
100 | * Listings 3.4 through 3.6
101 | * ───────────────────────────────────────────────────────
102 | * This is a fun listing, because it gets to a very important
103 | * part of the software development process: stepping away from
104 | * the software. We're going to just sketch out what we know
105 | * about the stuff which makes up our domain.
106 | * ───────────────────────────────────────────────────────
107 | */
108 | void listing_3_4_through_3_6() {
109 | // You could do this with pen and paper, or on a whiteboard,
110 | // or in a comment like we do here. The important part is
111 | // giving yourself some space to think about what you're
112 | // trying to model before you let the quirks and limitations
113 | // of a programming language start influencing your thinking.
114 |
115 | /**
116 | * SampleID:
117 | * Alpha-numeric + special characters ◄───┐ As a first stab, this isn't bad, but it's
118 | * Globally unique │ still a bit ambiguous. Do the characters matter at all?
119 | *
120 | * Days Elapsed:
121 | * A positive integer (0 inclusive). ◄────┐ The person taking the measurements (me) is
122 | * Not strictly daily. Will be Sparse. │ lazy. "days" is a hand-wavy unit of time, but about
123 | * │ the highest fidelity I could muster.
124 | *
125 | * Contact Angle: ◄────┐
126 | * degrees. Half open interval [0.0, 360) │ Now we know what those numbers in the JSON
127 | * Precision of 100th of a degree │ are meant to be!
128 | *
129 | */
130 | }
131 |
132 |
133 |
134 | /**
135 | * ───────────────────────────────────────────────────────
136 | * Listings 3.7
137 | * ───────────────────────────────────────────────────────
138 | * Let's take another stab at understanding Sample ID
139 | * ───────────────────────────────────────────────────────
140 | */
141 | void listing_3_7() {
142 |
143 | /**
144 | * SampleID:
145 | * A sequence of characters satisfying the regex /[A-Z]+-\d+/
146 | * Globally unique.
147 | * ▲
148 | * └─── This is an improved statement. Now we know the *shape* of
149 | * the IDs. Can we be more precise that this?
150 | *
151 | * (other fields elided for brevity)
152 | *
153 | */
154 | }
155 |
156 |
157 |
158 |
159 | /**
160 | * ───────────────────────────────────────────────────────
161 | * Listings 3.8
162 | * ───────────────────────────────────────────────────────
163 | * If we keep digging on that Sample ID, we can eventually
164 | * get to the bottom of it. IDs are one of my favorite things
165 | * to harp on because they so often contain hidden domain
166 | * information. That domain info ends up lost behind a generic
167 | * "anything goes here" type like "String".
168 | * ───────────────────────────────────────────────────────
169 | */
170 | void listing_3_8() {
171 | /**
172 | * CuringMethod: ◄────┐
173 | * One of: (AIR, UV, HEAT) │ Totally new domain information!
174 | * │
175 | * SampleNumber: ◄────┘
176 | * A positive integer (0 inclusve)
177 | *
178 | * SampleID:
179 | * The pair: (CuringMethod, SampleNumber) ◄─── Now *this* is a precise definition!
180 | * Globally unique.
181 | *
182 | * (other fields elided for brevity)
183 | *
184 | */
185 | }
186 |
187 |
188 |
189 | /**
190 | * ───────────────────────────────────────────────────────
191 | * Listings 3.9 to 3.12
192 | * ───────────────────────────────────────────────────────
193 | * What happens in so much Java code is that we forget to take
194 | * what we've learned about our domain and *actually* express it
195 | * in our code. Instead, it just lives in our heads. The code is
196 | * left to fend for itself. That leads to situations where our
197 | * model be used to create invalid data (rather than guide us
198 | * towards the right path).
199 | * ───────────────────────────────────────────────────────
200 | */
201 | void listing_3_9_to_3_12() {
202 | /** ─┐
203 | * An individual observation tracking how water contact │ What we often try to do in our
204 | * angles on a surface changes as oil curing progresses by day │ code is put what we know about the
205 | * │ domain into the javadoc and variable
206 | * @param sampleId │ names.
207 | * A pair (CuringMethod, positive int) represented │
208 | * as a String of the form "{curingMethod}-{number}" │ It looks professional, and it's better
209 | * CuringMethod will be one of {AIR, UV, HEAT} │ than nothing, but it has a lot of
210 | * @param daysElapsed │ limitations. The biggest one being that
211 | * A positive integer (0..n) │ it depends on people both reading it (which
212 | * @param contactAngle │ is rare) and honoring it (also rare).
213 | * Water contact angle measured in degrees │
214 | * ranging from 0.0 (inclusive) to 360 (non-inclusive) ┘ Nothing enforces it.
215 | */
216 | record Measurement(
217 | String sampleId,
218 | Integer daysElapsed,
219 | double contactAngleDegrees // ◄──┐ Here's an example of trying to use variable
220 | ) {} // │ names to encode what something means (degrees)
221 |
222 | new Measurement(
223 | UUID.randomUUID().toString(), // ◄──┐ Despite those variable names and a bunch of
224 | -32, // │ extensive doc strings, anybody can still march into
225 | 9129.912 // │ our codebase and complete invalid data.
226 | ); // │ This breaks every invariant we know our data to have!
227 | }
228 |
229 |
230 |
231 | /**
232 | * ───────────────────────────────────────────────────────
233 | * Listings 3.13, 3.15
234 | * ───────────────────────────────────────────────────────
235 | * Our first stab at enforcing what our data means will be
236 | * a big improvement from where we started (we'll prevent bad
237 | * states from being created), but we'll still end up with
238 | * something that's a bit "off." It's "forgetful"
239 | * ───────────────────────────────────────────────────────
240 | */
241 | @Test
242 | void listing_3_13_to_3_15() {
243 |
244 | record Measurement(
245 | String sampleId,
246 | Integer daysElapsed,
247 | double contactAngle
248 | ) {
249 | Measurement {
250 | if (!sampleId.matches("(HEAT|AIR|UV)-\\d+")) { // ◄────┐ Validating that the Sample ID
251 | throw new IllegalArgumentException( // │ is in the right shape
252 | "Must honor the form {CuringMethod}-{number}" +
253 | "Where CuringMethod is one of (HEAT, AIR, UV), " +
254 | "and number is any positive integer"
255 | );
256 | }
257 | if (daysElapsed < 0) { // ◄────┐ And validating the remaining
258 | throw new IllegalArgumentException( // │ constraints.
259 | "Days elapsed cannot be less than 0!"); // │
260 | } // │
261 | if (!(Double.compare(contactAngle, 0.0) >= 0 // │
262 | && Double.compare(contactAngle, 360.0) < 0)) { // │
263 | throw new IllegalArgumentException(
264 | "Contact angle must be 0-360");
265 | }
266 | }
267 | }
268 |
269 | // This is a nice improvement.
270 | // If we try to create any data that's invalid for our domain
271 | // we'll get a helpful exception telling us where we went wrong.
272 | Assertions.assertThrows(IllegalArgumentException.class, () -> {
273 | new Measurement("1", -12, 360.2);
274 | });
275 |
276 |
277 | // However, this approach is "forgetful"
278 | // As soon as we're done with constructing the object, we forget
279 | // all the meaning we just ascribed to those values.
280 |
281 | // Here's what I mean:
282 | Measurement measurement = new Measurement("HEAT-01", 12, 108.2);
283 | double angle = measurement.contactAngle();
284 | // ▲
285 | // └─ this is back to being "just" a double.
286 | //
287 | // This "forgetfulness" of our data type has a massive effect
288 | // on how we reason about our programs.
289 |
290 | // For instance, as we read this code, lets notice how strongly
291 | // we can reason about what kind of thing we're dealing with.
292 | List measurements = List.of( // ◄────┐
293 | new Measurement("UV-1", 1, 46.24), // │ Right here, we're probably 100% sure
294 | new Measurement("UV-1", 4, 47.02), // │ what this code means and represents.
295 | // ...
296 | new Measurement("UV-2", 30, 86.42)
297 | );
298 |
299 | Map> bySampleId = measurements.stream() // ◄──┐
300 | .collect(groupingBy( // │ But then it gets transformed. We have to
301 | Measurement::sampleId, // │ pay close attention to understand that
302 | mapping(Measurement::contactAngle, // │ those doubles in the map still represent Degrees
303 | Collectors.toList())));
304 |
305 | // Comparing the first and last samples in each
306 | // group to see how much things changes while curing
307 | List totalChanges = bySampleId.values() // ◄──┐ More transformations. More distance. Do these
308 | .stream() // │ still represent degrees...?
309 | .map(x -> x.getLast() - x.getFirst())
310 | .toList();
311 |
312 | // computing some summary stats
313 | double averageChange = totalChanges.stream()
314 | .collect(averagingDouble(_angle -> _angle));
315 | // Note: the below is commented out simply because these methods
316 | // don't exist in scope.
317 | //
318 | // By the time we’re down here, what these doubles represent is very blurry.
319 | // Still Degrees? The only way to understand this code is by working your way
320 | // backwards through the call stack to mentally track how the meaning of the
321 | // data changed as it was transformed.
322 | // double median = calculateMedian(totalChanges);
323 | // double p25 = percentile(totalChanges, 25);
324 | // double p75 = percentile(totalChanges, 75);
325 | // double p99 = percentile(totalChanges, 99);
326 | }
327 |
328 |
329 |
330 |
331 |
332 | /**
333 | * ───────────────────────────────────────────────────────
334 | * Listings 3.16 through 3.18
335 | * ───────────────────────────────────────────────────────
336 | * The problem with the design in the previous listing is that
337 | * it is missing type information. Inside of the measurement
338 | * class we just had "naked" values like double and string.
339 | * Those primitive types can't enforce what they are -- because
340 | * they can be anything! We need types that capture the ideas
341 | * in our domain.
342 | * ───────────────────────────────────────────────────────
343 | */
344 | void listing_3_16_to_3_18() {
345 | // Degrees is a core idea in our domain. It has a semantics.
346 | // A set of rules which make it, *it*.
347 | record Degrees(double value) {
348 | Degrees {
349 | // These are the same checks from listing 3.13, but now
350 | // used to guard our domain specific type
351 | if (!(Double.compare(value, 0.0) >= 0
352 | && Double.compare(value, 360.0) < 0)) {
353 | throw new IllegalArgumentException("Invalid angle");
354 | }
355 | }
356 | }
357 | // We can refactor the measurement data type to use
358 | // our new Degree type.
359 | record Measurement(
360 | String sampleId,
361 | Integer daysElapsed,
362 | Degrees contactAngle // Nice!
363 | ) {}
364 |
365 | // And this yields something really cool.
366 | // The code no longer "forgets" what it is.
367 | Measurement measurement = new Measurement("HEAT-01", 12, new Degrees(108.2));
368 | Degrees angle = measurement.contactAngle();
369 | // ▲
370 | // └─ Look at this! We still know exactly what it is.
371 | // Previously, we'd end up with a plain double that could
372 | // be confused for anything.
373 |
374 | // (Note: here just as a placeholder to make the example work)
375 | BiFunction minus = (a, b) -> new Degrees(a.value() - b.value());
376 |
377 | // Let's look at that same transform from the previous listing, but
378 | // now using our new type.
379 | List measurements = List.of(
380 | new Measurement("UV-1", 1, new Degrees(46.24)),
381 | new Measurement("UV-1", 4, new Degrees(47.02)),
382 | // ...
383 | new Measurement("UV-2", 30, new Degrees(86.42))
384 | );
385 |
386 | // ┌─── A No more guesswork. The types are unambiguous
387 | // ▼
388 | Map> bySampleId = measurements.stream()
389 | .collect(groupingBy(Measurement::sampleId,
390 | mapping(Measurement::contactAngle, Collectors.toList())));
391 |
392 | // ┌─── Even as we transform and reshape the data
393 | // ▼ we don't lose track of what it is were dealing with.
394 | List totalChanges = bySampleId.values()
395 | .stream()
396 | .map(x -> minus.apply(x.getLast(), x.getFirst()))
397 | .toList();// ▲
398 | // └─ Check this out. We're doing math on Degrees. That might
399 | // produce things that aren't valid degrees. The data type forces
400 | // us to consider these cases. Even if we don't it still watches
401 | // our back and will raise an error if we unknowingly drift away
402 | // from our domain of degrees.
403 | }
404 |
405 |
406 |
407 | /**
408 | * ───────────────────────────────────────────────────────
409 | * Listings 3.19 to 3.20
410 | * ───────────────────────────────────────────────────────
411 | * We can apply this idea to all the data in our domain.
412 | * We can make what we're talking about unambiguous.
413 | * ───────────────────────────────────────────────────────
414 | */
415 | void listing_3_19_to_3_20() {
416 | // Here's the Degrees implementation from the previous listing.
417 | record Degrees(double value) {
418 | Degrees {
419 | if (!(Double.compare(value, 0.0) >= 0
420 | && Double.compare(value, 360.0) < 0)) {
421 | throw new IllegalArgumentException("Invalid angle");
422 | }
423 | }
424 | }
425 |
426 | // ┌ Here's a new data type that captures the fact that
427 | // │ we're only talking about integers >= 0
428 | // ▼
429 | record PositiveInt(int value) {
430 | PositiveInt {
431 | if (value < 0) {
432 | throw new IllegalArgumentException(
433 | "Hey! No negatives allowed!"
434 | );
435 | }
436 | }
437 | }
438 | // We can stick this new type on our data model.
439 | record Measurement(
440 | String sampleId,
441 | PositiveInt daysElapsed, // ◄────┐ These changes have a compounding effect on our
442 | Degrees contactAngle // │ understanding. Now at a glance, we can tell
443 | ) {} // │ exactly what these Measurement attributes *mean*.
444 | }
445 |
446 |
447 |
448 |
449 |
450 | /**
451 | * ───────────────────────────────────────────────────────
452 | * Listings 3.21 through 3.25
453 | * ───────────────────────────────────────────────────────
454 | * The most important part of this process is making sure we
455 | * don't accidentally slip into creating types "mechanically".
456 | * We want to remain thoughtful about what we're communicating
457 | * about the system. We want to make that our representation
458 | * captures the core ideas of what we're modeling.
459 | * ───────────────────────────────────────────────────────
460 | */
461 | void listing_3_21_to_3_25() {
462 | // Here's the Degrees implementation from the previous listing.
463 | record Degrees(double value) {
464 | Degrees {
465 | if (!(Double.compare(value, 0.0) >= 0
466 | && Double.compare(value, 360.0) < 0)) {
467 | throw new IllegalArgumentException("Invalid angle");
468 | }
469 | }
470 | }
471 |
472 | // This is from the previous listing as well
473 | record PositiveInt(int value) {
474 | PositiveInt {
475 | if (value < 0) {
476 | throw new IllegalArgumentException(
477 | "Hey! No negatives allowed!"
478 | );
479 | }
480 | }
481 | }
482 |
483 | // ┌ This is the next logical data type to introduce, but... does
484 | // │ it really capture what it means to be a Sample ID in our domain?
485 | // ▼
486 | record SampleId(String value) {
487 | SampleId {
488 | if (!value.matches("(HEAT|AIR|UV)-\\d+")) { // │ This is all great validation. It
489 | throw new IllegalArgumentException( // │ enforces what we know about the shape
490 | "Must honor the form {CuringMethod}-{number}" + // │ of the IDs inside of that String.
491 | "Where CuringMethod is one of (HEAT, AIR, UV), " + // │ However, there are some problems.
492 | "and number is any positive integer"
493 | );
494 | }
495 | }
496 | }
497 |
498 | record Measurement(
499 | SampleId sampleId, // ◄────┐ What's wrong with this modeling?
500 | PositiveInt daysElapsed, // │ Let's take it for a spin and see how it feels.
501 | Degrees contactAngle
502 | ) {}
503 |
504 | // What if we wanted to do something super basic, say, bucket all the
505 | // measurements by their curing method.
506 | List measurements = List.of(); // (we don't need any items for the example to work)
507 | measurements.stream()
508 | .collect(groupingBy(m -> m.sampleId()/* ??? */));
509 | // ▲
510 | // │ Gah! We're back in that world where we "forget"
511 | // │ what our code is. We know the shape of the string
512 | // │ during validation in the constructor, but out here
513 | // │ it's "just" another string.
514 | // └─
515 |
516 | measurements.stream()
517 | .collect(groupingBy(
518 | m -> m.sampleId().value().split("-")[0]
519 | )); // ▲
520 | // └─ We are guaranteed that the string will be in a known shape, so
521 | // we *could* "safely" access its individual pieces ("safely" here
522 | // used very loosely and with disregard for potential future change)
523 |
524 | // ┌ One option would be to steal some ideas from OOP and "hide" the internal
525 | // │ details behind public methods.
526 | // ▼
527 | record SampleIdV2(String value) {
528 | SampleIdV2 {
529 | if (!value.matches("(HEAT|AIR|UV)-\\d+")) {
530 | throw new IllegalArgumentException(
531 | "Must honor the form {CuringMethod}-{number}" +
532 | "Where CuringMethod is one of (HEAT, AIR, UV), " +
533 | "and number is any positive integer"
534 | );
535 | }
536 | }
537 | public String curingMethod() {
538 | return this.value().split("-")[0]; // This gives the curing method without leaking "how"
539 | }
540 |
541 | public String sampleNumber() {
542 | return this.value().split("-")[1]; // ditto for the sample number.
543 | }
544 | }
545 |
546 | // This feels like progress, but we can again do the very simple gut check
547 | // of just seeing what happens when we poke the data type.
548 | String method = new SampleIdV2("HEAT-1").curingMethod();
549 | // ▲
550 | // └─ Ugh! We're back to just a plain string disconnected from its domain.
551 | }
552 |
553 |
554 |
555 | /**
556 | * ───────────────────────────────────────────────────────
557 | * Listings 3.26 through 3.27
558 | * ───────────────────────────────────────────────────────
559 | * Back to the drawing board. What is it that we're trying to
560 | * represent?
561 | * ───────────────────────────────────────────────────────
562 | */
563 | void listing_3_26_to_3_27() {
564 | // ┌─ Revisiting what we know about the domain
565 | // │ independent of our code
566 | /** ▼
567 | * CuringMethod:
568 | * One of: (AIR, UV, HEAT)
569 | *
570 | * SampleID:
571 | * The pair: (CuringMethod, positive integer (0 inclusive))
572 | * Globally unique.
573 | */
574 |
575 | // A sample ID isn't a string (despite the fact that it might be
576 | // serialized that way on the way into our system). The sample ID
577 | // is made up of multiple pieces of information. Each has it's own
578 | // constraints and things that make it *it*.
579 | record PositiveInt(int value){
580 | // constructor omitted for brevity
581 | }
582 |
583 | enum CuringMethod {HEAT, AIR, UV} // this is important info! It's these three things
584 | // *and nothing else* (a very important idea in modeling).
585 |
586 | // We can combine these into a refined representation for SampleID.
587 | record SampleId(
588 | CuringMethod method,
589 | PositiveInt sampleNum
590 | ) {
591 | // (Empty!)
592 | // ▲
593 | // └── Check out that the body of sample ID is now empty. We don't have
594 | // to validate anything here. It's entirely described (and made safe) by
595 | // the data types which it's built upon.
596 | }
597 |
598 | // With this, our code no longer "forgets" its meaning.
599 | // Everything is well typed and descriptive.
600 | CuringMethod method = new SampleId(CuringMethod.HEAT, new PositiveInt(1)).method();
601 | }
602 |
603 |
604 |
605 |
606 |
607 | /**
608 | * ───────────────────────────────────────────────────────
609 | * Listings 3.8 through 3.9
610 | * ───────────────────────────────────────────────────────
611 | * Modeling isn't just informational. It can prevent very real
612 | * bugs from even being possible. Ambiguity is a dangerous thing
613 | * to have in a code base. People come from different backgrounds.
614 | * Codebases change hands. What's "obvious" to one group will be
615 | * not at all "obvious" to another.
616 | * ───────────────────────────────────────────────────────
617 | */
618 | void listing_3_28() {
619 | // (Note: as usual, the below is only defined as an inline lambda in order to keep
620 | // each listing isolated)
621 | //
622 | BiFunction computeFee = (Double total, Double feePercent) -> {
623 | return total * feePercent;
624 | }; // ▲
625 | // │
626 | // └── What kind of tests would you write to 'prove' this does the right thing?
627 |
628 | // You can't write any! Whether it does the right thing or not depends entirely on the
629 | // caller knowing how to use it correctly. For instance, how should that fee percentage
630 | // be represented? 0.10? 10.0? The "obvious" way will vary from person to person!
631 |
632 | // We can completely eliminate this ambiguity and potential customer impacting bug
633 | // through better modeling. What does it *mean* to be a percentage? A value between 0..1?
634 | // 1..100? A rational?
635 |
636 | // We can use the design of our code to ensure that *only* correct notion of percents
637 | // can be created. We don't need to rely on secret team knowledge or everyone having the
638 | // same notion of "the obvious way" -- we make it absolutely explicit in code.
639 | record Percent(double numerator, double denominator) {
640 | Percent {
641 | if (numerator > denominator) {
642 | throw new IllegalArgumentException(
643 | "Percentages are 0..1 and must be expressed " +
644 | "as a proper fraction. e.g. 1/100");
645 | }
646 | }
647 | }
648 | // Any departure from what percentages mean to us gets halted with an error (rather than
649 | // propagated out to very angry and confused customers)
650 | }
651 |
652 |
653 |
654 | }
655 |
--------------------------------------------------------------------------------
/app/src/test/java/dop/chapter04/Listings.java:
--------------------------------------------------------------------------------
1 | package dop.chapter04;
2 |
3 |
4 | import org.junit.jupiter.api.Assertions;
5 | import org.junit.jupiter.api.Test;
6 |
7 | import java.time.Instant;
8 | import java.util.List;
9 | import java.util.Map;
10 | import java.util.function.Function;
11 |
12 | /**
13 | * Chapter 4 builds on top of chapter 3's exploration of
14 | * learning to see through our code into what it actually
15 | * means and communicates. This is a fun one, because it
16 | * walks through the design process in all of its messy
17 | * glory. We'll make mistakes, refine, refactor -- and even
18 | * go back to the drawing board a few time.
19 | *
20 | * What I hope to show is that focusing on the data and getting its
21 | * representation pinned down *before* we start worrying about
22 | * its behaviors makes any mistakes low cost and fast to correct.
23 | * This approach enables rapid prototyping and immediate feedback.
24 | * We can learn from our mistakes before we start pouring concrete
25 | * in the form of implementation code.
26 | */
27 | public class Listings {
28 |
29 | /**
30 | * ───────────────────────────────────────────────────────
31 | * Listings 4.1
32 | * ───────────────────────────────────────────────────────
33 | * We're going to model a checklist! How could anyone possibly
34 | * screw up making a checklist? I'm pretty sure I accidentally
35 | * found all the ways.
36 | *
37 | * We might start with the "obvious" approach:
38 | * ───────────────────────────────────────────────────────
39 | */
40 | public void listing4_1() {
41 | // ┌── A named "thing to do"
42 | // ▼
43 | record Step(String name, boolean isComplete) {
44 | }
45 | // ▲
46 | // └── And it's either done or not
47 | record Checklist(List steps) {
48 | }
49 | // ▲
50 | // └─ A checklist is just a collection of steps.
51 | // If all the steps are complete. The checklist is complete.
52 | // Easy Peasy?
53 | //
54 | // ...Done?
55 | }
56 |
57 |
58 | /**
59 | * ───────────────────────────────────────────────────────
60 | * Listings 4.2 through 4.5
61 | * ───────────────────────────────────────────────────────
62 | * We have to force ourselves to *not* jump straight to the
63 | * code! Code is a terrible medium for thinking. It forces us
64 | * to think "in Java," which is extremely limited and has
65 | * nothing to do with the thing we're modeling.
66 | *
67 | * Instead, we want to do the exercise from the previous chapter.
68 | * Step away from the code, and think about what it is we're
69 | * trying to model.
70 | * ───────────────────────────────────────────────────────
71 | */
72 | public void listing4_2_to_4_5() {
73 | // Once we sketch out what the data in our domain is, we can
74 | // start with a process very similar to OOP: pick out the nouns.
75 | //
76 | // All models are an approximation. The challenge when designing
77 | // is figuring out which properties capture the essence of the
78 | // thing we're modeling.
79 | //
80 | // The good news is that we don't have to get it right the first time!
81 | // This is just a starting point. DoP makes it easy to iterate on
82 | // these choices. So, we'll loop through, pick out some things that
83 | // look important, and then refine from there.
84 | /**
85 | * Template:
86 | * ┌── Seems Important!
87 | * ┌───────────┐
88 | * A named collection of things to do
89 | * ▲
90 | * └── Important!
91 | *
92 | * Instance:
93 | *
94 | * ┌─── Important! It's how we can identity different "runs" of a checklist
95 | * ▼
96 | * A named run-through of a template
97 | * ┌─── Time is an interesting part of the data
98 | * ▼
99 | * at a particular point in time where
100 | * we complete the things to do and
101 | * ▲
102 | * └── Another interesting piece. "Complete" paired with "point in time"
103 | * has very interesting implications for our modeling.
104 | * record the results.
105 | */
106 | }
107 |
108 |
109 | /**
110 | * ───────────────────────────────────────────────────────
111 | * Listings 4.6 through 4.8
112 | * ───────────────────────────────────────────────────────
113 | * Here's our second stab at turning what we know about our
114 | * data into code.
115 | *
116 | * Once we've got the sketch in place, we start the important
117 | * part of the design process: taking a step back to look at
118 | * what our sketch of the data model *means*. What does it
119 | * communicate? What does it enable us to "say" with the code?
120 | * ───────────────────────────────────────────────────────
121 | */
122 | public void listing4_6_to_4_7() {
123 |
124 |
125 | // Reusing the model from our initial stab at this in listing 4.1 just to kick things off
126 | record Step(String name, boolean isCompleted) {
127 | }
128 |
129 | // Note this refinement from the initial stab at this.
130 | // Steps live on a "Template", rather than what we ambiguously
131 | // called just a “Checklist” before.
132 | record Template(String name, List steps) {
133 | }
134 |
135 | // [IGNORE] (We haven't modeled this yet!)
136 | // This is here just to make the example below compilable.
137 | record Instance(List steps) {}
138 | // [END IGNORE]
139 |
140 | // The quick check we can always perform while design is seeing
141 | // if our code lets us express anything weird.
142 | Template travelPrep = new Template(
143 | "International Travel Checklist",
144 | List.of(
145 | new Step("Passport", false),
146 | new Step("Toothbrush", false),
147 | new Step("bring socks", true)
148 | ) // ▲
149 | // └─ What the heck does it mean if items on a template are pre-set
150 | // to true? This step is already always completed? All socks are
151 | // already pre-packed for all trips?
152 | //
153 | // Our data model allows us to express a bunch of nonsense!
154 | );
155 |
156 | // This is a dangerous part in the design process that we have to train
157 | // ourselves to notice. The "programmer" part of our brain -- the one that
158 | // thinks in code, might look at this modeling error and start down an
159 | // innocent sounding, but critically damaging thought pattern: the one that
160 | // usually starts with "well, we could just..."
161 | //
162 | // For example:
163 | //
164 | // "We could just..." use a defensive check or transform during construction
165 | // to make sure that no funky states are created.
166 | new Instance(travelPrep.steps().stream()
167 | .map(step -> new Step(step.name(), false))
168 | .toList()); // └── Defensively resetting all the steps "just in case"
169 | // when we create instances of our checklist.
170 |
171 | // This works under exactly one condition: you remember to do it.
172 |
173 | // We can do better. We can make the code enforce its own meaning and
174 | // help us, the fallible programmers, not make mistakes.
175 | }
176 |
177 |
178 | /**
179 | * ───────────────────────────────────────────────────────
180 | * Listings 4.9
181 | * ───────────────────────────────────────────────────────
182 | * Here's the messiness of the design process (which is both
183 | * expected, normal, and OK). We don't know where this concept
184 | * of `isCompleted` goes yet, but we *do* know from the previous
185 | * listing that it doesn't belong on `Step`.
186 | * ───────────────────────────────────────────────────────
187 | */
188 | public void listing4_9() {
189 | record Step(String name /* (removed) */ ) {
190 | } // ▲
191 | // └─ No more isCompleted here.
192 | // We have to figure out where it goes.
193 | }
194 |
195 |
196 | /**
197 | * ───────────────────────────────────────────────────────
198 | * Listings 4.10 through 4.13
199 | * ───────────────────────────────────────────────────────
200 | * We still don't know where we should track the idea of
201 | * completing a step, but that's not a problem. We can keep
202 | * exploring the modeling of the data. Maybe were it goes will
203 | * naturally emerge as we fill out the rest of the domain model.
204 | *
205 | * This big point I'll keep belaboring: we're designing. This is
206 | * a loose, iterative, and exploratory process! Don't think of it
207 | * as "writing code" just yet. We're using code as a tool for
208 | * sketching. We'll constantly be taking a step back and looking
209 | * at what our choices mean and then revising from there.
210 | * ───────────────────────────────────────────────────────
211 | */
212 | public void listing4_10_to_4_13() {
213 | // Step and Template were defined in previous listings
214 | record Step(String name) {}
215 | record Template(String name, List steps) {}
216 | // Here's a sketch of how we might model instances of
217 | // a checklist.
218 | record Instance(
219 | String name,
220 | Instant date, // Instance nicely captures the "Point in time"
221 | // detail from our requirements (Listing 4.5)
222 | Template template
223 | // Putting the whole template on the Instance is one of the nearly
224 | // infinite ways we could approach modeling this data in our code.
225 | // It's the one we'll stick with for the sake of the book and the
226 | // flow of its examples. We use it as part of the "natural key" (to
227 | // use some database lingo) which uniquely identifies an Instance.
228 | // Alternative modelings are left as an exercise to the reader ^_^
229 | ){}
230 |
231 | // Now -- the big remaining design question is where we put that core idea
232 | // that steps in our checklist get completed.
233 | // Rather than trying to fit it into any one of our existing models, we might
234 | // introduce a *new* data type that specifically exists to track whether a step is completed.
235 | record Status(Template template, Step step, boolean isCompleted){}
236 | // ▲ ▲ ▲
237 | // │ └────────────────┘
238 | // │ └─── Tracks the step and its completed status
239 | // │
240 | // │
241 | // │
242 | // └ Putting template here serves the same purpose as putting
243 | // it on the Instance data type: it's acting as an identifying key.
244 |
245 |
246 | // This design of pulling out the statuses on their own would
247 | // make tracking and updating them a breeze. We could imagine
248 | // a little in-memory data structure.
249 | class MyCoolChecklistInMemoryStorage {
250 | private Map statuses;
251 | }
252 | // (My continued belaboring) all of this is still just a sketch.
253 | // We're seeing how each of these decisions feels and how its effects
254 | // ripple outward through the code.
255 | }
256 |
257 |
258 |
259 | /**
260 | * ───────────────────────────────────────────────────────
261 | * Listings 4.14 through 4.16
262 | * ───────────────────────────────────────────────────────
263 | * A new requirement appears!
264 | * We want to be able to track who performed an individual step
265 | * in the checklist. This should be an easy addition given our
266 | * modeling, right...?
267 | * ───────────────────────────────────────────────────────
268 | */
269 | public void listing4_14_to_4_16() {
270 | // Step, Template, and Instance were defined in previous listings
271 | record Step(String name) {}
272 | record Template(String name, List steps) {}
273 | record Instance(String name, Instant date, Template template){}
274 |
275 | // Minimally viable user type.
276 | // It's not that interesting to our example, so we
277 | // keep it pretty bare bones.
278 | record User(String value){}
279 |
280 | record Status(
281 | Template template,
282 | Step step,
283 | boolean isCompleted,
284 | User completedBy, // ◄────┐
285 | Instant completedOn // │ Just plug these in here...?
286 | ){}
287 |
288 | // Here's where we take another step back
289 | // and perform our gut check: does this modeling
290 | // let us express anything weird?
291 | // Let's try creating a new Status that hasn't been
292 | // completed yet.
293 |
294 | /**
295 | * (Note: commented out because the example would not compile)
296 | *
297 | * new Status(
298 | * template,
299 | * step,
300 | * false,
301 | * ??? ◄──┐
302 | * ??? ◄─── Uhh... what goes here? We don't *have* a user yet
303 | * because nobody has completed the step yet!
304 | * );
305 | */
306 |
307 | // Another dangerous "Java" thought might pop into your head that begins
308 | // with "well we could just..." and ends with "use nulls"
309 | //
310 | // We've got to push those thoughts down! Our *modeling* is incorrect.
311 | // Patching it over with nulls only introduces more problems. Check this out:
312 | Template template = new Template("Cool Template", List.of(new Step("Step 1")));
313 |
314 | // We can break causality itself!
315 | new Status(
316 | template,
317 | template.steps().getFirst(),
318 | false, // ◄───┐ Our code allows us to say something nonsensical.
319 | new User("Bob"), // ┘ We've added "who did it" before "it" was done!
320 | Instant.now()
321 | );
322 |
323 | // The inverse is true as well.
324 | new Status(
325 | template,
326 | template.steps().getFirst(),
327 | true, // ◄──── Now our step is marked as complete
328 | null, // ◄───┐ But who did it (and when!) is completely
329 | null // ┘ missing.
330 | );
331 |
332 | // The design of our data model **created** these invalid states.
333 | }
334 |
335 |
336 | /**
337 | * ───────────────────────────────────────────────────────
338 | * Listings 4.17 through 4.18
339 | * ───────────────────────────────────────────────────────
340 | * Defensive coding to the rescue?
341 | * ───────────────────────────────────────────────────────
342 | */
343 | @Test
344 | public void listing4_17_to_4_18() {
345 | // All defined in previous listings
346 | record Step(String name) {}
347 | record Template(String name, List steps) {}
348 | record Instance(String name, Instant date, Template template){}
349 | record User(String value){}
350 |
351 | record Status(
352 | Template template,
353 | Step step,
354 | boolean isCompleted,
355 | User completedBy,
356 | Instant completedOn
357 | ){
358 | Status {
359 | // this is tedious to write, but it gets the job done.
360 | // We're kind of back on track. We've "prevented" invalid
361 | // data from being created.
362 | if (isCompleted && (completedBy == null || completedOn == null)) {
363 | throw new IllegalArgumentException(
364 | "completedBy and completedOn cannot be null " +
365 | "when isCompleted is true"
366 | );
367 | }
368 | if (!isCompleted && (completedBy != null || completedOn != null )) {
369 | throw new IllegalArgumentException(
370 | "completedBy and completedOn cannot be populated " +
371 | "when isCompleted is false"
372 | );
373 | }
374 | }
375 | }
376 |
377 | Assertions.assertThrows(IllegalArgumentException.class, () -> {
378 | Template template = new Template("Cool Template", List.of(new Step("Step 1")));
379 | new Status(
380 | template,
381 | template.steps().getFirst(),
382 | false,
383 | null, // Now any attempts at creating invalid states will be rejected
384 | Instant.now()
385 | );
386 | });
387 | }
388 |
389 |
390 |
391 | /**
392 | * ───────────────────────────────────────────────────────
393 | * Listings 4.19 through 4.23
394 | * ───────────────────────────────────────────────────────
395 | * More requirements! Steps can be skipped!
396 | * Skipping steps on a checklist is a big deal in rocketry.
397 | * We have to know who did them, when, and *why*.
398 | *
399 | * Should be a small lift, right? After all, we already figured
400 | * out how to track when the steps got completed. Doing the
401 | * same thing for skipped should be a breeze.
402 | * ───────────────────────────────────────────────────────
403 | */
404 | @Test
405 | public void listing4_19_4_23() {
406 | // All defined in previous listings
407 | record Step(String name) {}
408 | record Template(String name, List steps) {}
409 | record Instance(String name, Instant date, Template template){}
410 | record User(String value){}
411 |
412 | record Status(
413 | Template template,
414 | Step step,
415 | boolean isCompleted,
416 | User completedBy,
417 | Instant completedOn,
418 | Boolean isSkipped, // ◄──┐
419 | User skippedBy, // │ Copy/pasting our existing modeling.
420 | Instant skippedOn, // │ We get another flag, another user, another
421 | String rationale // │ timestamp, plus a new field for tracking why
422 | // │ the step was skipped.
423 | ){
424 | // Fun exercise for the reader:
425 | // Try to write constructor validation which can catch every illegal state.
426 | //
427 | // I initially had this in the book to point out how egregious the validation
428 | // becomes, but it grew beyond something which would fit on a page. Also, worth
429 | // noting: I repeatedly got that validation wrong. I'd miss a case, or mix up
430 | // two cases, disable things which should be allowed -- it's the kind of validation
431 | // that makes you sit there and work through "Ok, so when x is set, y and z should
432 | // NOT be set... but when... then..."
433 | }
434 |
435 | // What surely hops out with the modeling above is that it isn't "DRY".
436 | // We might try to refactor it "as code" -- meaning, ignoring what the
437 | // meaning of the underlying data is (what we're trying to capture) and
438 | // instead refactoring "mechanically" -- manipulating the symbols to factor
439 | // out the duplication.
440 |
441 | // For instance, we can refactor the multiple booleans into
442 | // a single Enum. Nice!
443 | enum State {NOT_STARTED, COMPLETED, SKIPPED};
444 | // Ditto for the non-DRY user definitions.
445 | record StatusV2(
446 | String name,
447 | State state,
448 | User actionedBy, // We factor them out into a shared "actionedBy"
449 | Instant actionedOn, // shape that's contextual based on the current state.
450 | User confirmedBy,
451 | String rational
452 | ) {
453 | StatusV2 {
454 | // But, ugh.. this hasn't actually made our lives that much easier.
455 | // Our code is still allowed to express nonsensical states. Which means
456 | // we're still on the hook for defending against them. And that remains
457 | // extremely tedious and error prone.
458 | //
459 | if (state.equals(State.NOT_STARTED)) { // The implementation for each case is left
460 | // ... as an exercise to the reader.
461 | }
462 | if (state.equals(State.COMPLETED)) {
463 | // ...
464 | }
465 | if (state.equals(State.SKIPPED)) {
466 | // ...
467 | }
468 | // This only gets worse as our requirements get more complex.
469 | // Imagine adding a Blocked or InProgress state. Each one will
470 | // require thinking *very* hard about the validation you and
471 | // what it means for existing states.
472 | }
473 | }
474 |
475 | // The woes with the design go beyond the difficulty in validating it.
476 | // We have that ongoing problem where our data is "forgetful". Aside from
477 | // when we're validating it, we have no idea what status it's in.
478 | // So it again falls to the frail humans working on the code to remember
479 | // that (a) all of those statuses exist, (b) only some of them apply to
480 | // certain behaviors in the system, and (c) we have to *remember* to check
481 | // before we do anything.
482 | Function doSomethingWithCompleted = (StatusV2 status) -> {
483 | if (!status.state().equals(State.COMPLETED)) {
484 | // If we remember to do this, then we know that
485 | // we can safely read the actionedBy/On attributes
486 | // without a Null Pointer getting thrown.
487 | }
488 | // otherwise, we have to throw an error.
489 | throw new IllegalArgumentException("Expected completed");
490 | };
491 |
492 | // The problem with our current "refactored" model is that it hasn't
493 | // really solved any of the problems. The way we've modeled the code
494 | // *creates* invalid states and potential bugs. We have to expend tons
495 | // of effort fighting against the monster we created.
496 | }
497 |
498 |
499 |
500 | /**
501 | * ───────────────────────────────────────────────────────
502 | * Listings 4.24 through 4.31
503 | * ───────────────────────────────────────────────────────
504 | * Obvious things which maybe aren't so obvious: there's an
505 | * implicit AND between everything in a record.
506 | * ───────────────────────────────────────────────────────
507 | */
508 | @Test
509 | public void listing4_24_to_4_31() {
510 | // All defined in previous listings
511 | record Step(String name) {}
512 | record Template(String name, List steps) {}
513 | record Instance(String name, Instant date, Template template){}
514 | record User(String value){}
515 | enum State {NOT_STARTED, COMPLETED, SKIPPED};
516 |
517 | record Status( // ◄─────────────────┐
518 | String name, // │
519 | // (AND) // │ When we define a data type, we’re saying it’s made up of
520 | State state, // │ attribute 1 AND attribute 2 AND ... AND ... AND ...AND.
521 | // (AND) // │
522 | User actionedBy, // │
523 | // (AND) // │
524 | Instant actionedOn, // │
525 | // (AND)
526 | User confirmedBy,
527 | // (AND)
528 | String rational
529 | ) {}
530 |
531 | // This ANDing is the source of our code's lying.
532 | // It's saying that a status is **always** name AND state AND actionedBy AND ...
533 | // But that's not true.
534 | // Attributes like `actionedBy` and `confirmedBy` are only available **sometimes**
535 |
536 | // A big part of DoP is retraining ourselves to read the code for exactly what it
537 | // says. The code is directly lying to us, but we're used to mentally "patching"
538 | // around those lies.
539 |
540 | // STARTING FRESH.
541 | // Let's revert back to before we did our "refactoring"
542 | record OriginalStatusModel(
543 | Template template,
544 | Step step,
545 | boolean isCompleted,
546 | User completedBy,
547 | Instant completedOn
548 | ){}
549 |
550 |
551 | //
552 | // We'll rebuild the Status data type piece by piece, at each
553 | // step we're going to force ourselves to read the code for exactly
554 | // what it says. We'll explcitly pause to notice what the implicit ANDs
555 | // are doing to our data model.
556 |
557 | record StatusV1(
558 | String name, // │
559 | // (AND) │ So far so good. These ANDs make sense
560 | Step step // │
561 | ) {}
562 |
563 | // Adding our next attribute back in:
564 | record StatusV2(
565 | String name, // │
566 | // (AND) // │
567 | Step step, // │
568 | // (AND) // │ This one feels a little weird, but it still
569 | boolean isCompleted // │ seems reasonable overall
570 | ) {}
571 |
572 | // If we keep going, we slam into a problem
573 | record StatusV3(
574 | String name, // │
575 | // (AND) // │
576 | Step step, // │
577 | // (AND) // │
578 | boolean isCompleted, // │
579 | // (AND) // │ Right here we hit a hard wall. This attribute cannot be ANDed
580 | User completedBy // │ with the rest, because it’s only defined *sometimes*
581 | ) {}
582 |
583 | // This is where we really have to read the code for exactly what's there.
584 | // What about this combination of attributes makes the modeling "wrong"?
585 | // Anything? Nothing?
586 | //
587 | // The friction is between what we're trying to model (some kind of generic
588 | // "status" data type) and what the code, as we've written it, actually represents.
589 | // This current collection of attributes is fine to AND together -- as long as we
590 | // listen to what their meaning tells us.
591 | //
592 | // This combination of attributes doesn't describe a Status that can be Not started
593 | // OR completed, it specifically describes something that's Completed -- that's why
594 | // the attributes are there, after all.
595 | //
596 | // Listening to what the attributes mean, we can adjust our naming of the record:
597 | //
598 | record Completed(
599 | String name, // │ Look how cohesive these attribute are
600 | // (AND) // │ now that we've scoped them to exactly what
601 | Step step, // │ they "wanted" to represent: a step that
602 | // (AND) // │ has been completed.
603 | User completedBy, // │
604 | // (AND) // │ We also simplified the model. We no longer
605 | Instant completedOn // │ need the isCompleted boolean. This *is* completed!
606 | ) {}
607 |
608 | // This "aha!" moment is the best part of the design process.
609 | // If the above only and exclusively represents Completed steps, then we
610 | // also need to model steps before they're completed. i.e.
611 |
612 | record NotStarted(
613 | Template template, // │
614 | // (AND) // │ There is no mention of completedBy here, because it’s not completed!
615 | Step step // │ It’s Not Started!
616 | ){}
617 |
618 | // Now adding skipped back into the model is *actually* simple.
619 | // This is what good modeling feels like. It feels smooth. Without friction.
620 | record Skipped(
621 | Template template,
622 | // (AND)
623 | Step step,
624 | // (AND)
625 | User skippedBy,
626 | // (AND)
627 | Instant skippedOn,
628 | // (AND)
629 | String rationale
630 | // (AND)
631 | ){}
632 | }
633 |
634 |
635 | /**
636 | * ───────────────────────────────────────────────────────
637 | * Listings 4.32 through 4.34
638 | * ───────────────────────────────────────────────────────
639 | * From AND, AND, AND to OR, OR, OR
640 | * ───────────────────────────────────────────────────────
641 | */
642 | @Test
643 | public void listing4_32() {
644 | // All defined in previous listings
645 | record Step(String name) {}
646 | record Template(String name, List steps) {}
647 | record Instance(String name, Instant date, Template template){}
648 | record User(String value){}
649 |
650 | // records cannot extend classes, so we tie them
651 | // together with an interface.
652 | interface StepState { }
653 |
654 | record NotStarted(
655 | Template template,
656 | Step step
657 | ) implements StepState {} // Each record type implements this interface
658 |
659 | record Completed(
660 | Template template,
661 | Step step,
662 | User completedBy,
663 | Instant completedOn
664 | ) implements StepState {} // Here, too
665 |
666 | record Skipped(
667 | Template template,
668 | Step step,
669 | User skippedBy,
670 | Instant skippedOn,
671 | String rationale
672 | ) implements StepState {} // and here.
673 |
674 |
675 | // Now we can use this interface to unite the disparate types.
676 | // All of them belong to / are "about" this idea of Step Statuses.
677 | Template template = new Template("Howdy", List.of(
678 | new Step("1"),
679 | new Step("2")
680 | ));
681 | Step step = template.steps().getFirst();
682 |
683 | // ┌─ We can assign the Completed data type to StepStatus
684 | // │ because the interface unites them under the same "family"
685 | // │
686 | // ▼ │
687 | StepState completed = new Completed( // ◄───┘
688 | template,
689 | step,
690 | new User("Bob"),
691 | Instant.now()
692 | );
693 |
694 | // This modeling lets us express *choice* within Java.
695 | // A StepStatus is either NotStarted, Completed, or Skipped.
696 | }
697 |
698 |
699 | /**
700 | * ───────────────────────────────────────────────────────
701 | * Listings 4.35
702 | * ───────────────────────────────────────────────────────
703 | * Expressing OR with records and interfaces is like having
704 | * super powered enums. The two modeling ideas are very similar.
705 | * ───────────────────────────────────────────────────────
706 | */
707 | @Test
708 | public void listing4_35() {
709 | enum StepStatus {
710 | NotStarted,
711 | Completed,
712 | Skipped;
713 | }
714 |
715 | interface StepState {} // An alternative way of modeling the
716 | record NotStarted() implements StepState {} // idea of mutual exclusivity.
717 | record Completed() implements StepState {} // Thinking of them *as* fancy enums can be
718 | record Skipped() implements StepState {} // a really useful mental model.
719 | }
720 |
721 |
722 |
723 | /**
724 | * ───────────────────────────────────────────────────────
725 | * Listings 4.36 through 4.40
726 | * ───────────────────────────────────────────────────────
727 | * A very important part of modeling is being able to say
728 | * what something *isn't*. This is the central value proposition
729 | * of enums. It allows us to model a closed domain.
730 | *
731 | * A gap with how we've modeled the StepState so far is that it
732 | * is *not* closed. It can be freely extended by anyone. Sometimes
733 | * this is what we want. Open to extension is a fantastic principle
734 | * for library development. However, just as often, we do not
735 | * want our model extended. There are only two booleans. There
736 | * are only 4 card suits. In our domain, there are only three
737 | * states a checlist step can be: NotStarted, Completed, or Skipped.
738 | * ───────────────────────────────────────────────────────
739 | */
740 | @Test
741 | public void listing4_36_to_4_40() {
742 | interface StepState {}
743 | record NotStarted() implements StepState {}
744 | record Completed() implements StepState {}
745 | record Skipped() implements StepState {}
746 |
747 | // The problem here is that this isn't closed. It's completely
748 | // open to anyone who wants to extend the interface.
749 | // Are these members of our domain?
750 | record Blocked() implements StepState {}
751 | record Paused() implements StepState {}
752 | record Started() implements StepState {}
753 | // They might be valid in some *other* domain, but they aren't
754 | // valid in ours.
755 | //
756 | // This is the role of the `sealed` modifier.
757 | // We can tell Java that we only want to permit certain
758 | // data types to implement our interface.
759 |
760 | // Note: sealing doesn't work locally inside a method.
761 | // So, it's commented out here. Checkout the supplementary
762 | // file `test.dop.chapter03.SealingExample` to see it in action
763 | /* sealed */ interface StepStateV2 {}
764 | record NotStartedV2() implements StepStateV2 {}
765 | record CompletedV2() implements StepStateV2 {}
766 | record SkippedV2() implements StepStateV2 {}
767 | }
768 |
769 | }
770 |
771 |
--------------------------------------------------------------------------------
/app/src/test/java/dop/chapter06/Listings.java:
--------------------------------------------------------------------------------
1 | package dop.chapter06;
2 |
3 | import dop.chapter05.the.existing.world.Entities;
4 | import dop.chapter05.the.existing.world.Entities.Invoice;
5 | import dop.chapter05.the.existing.world.Entities.LineItem;
6 | import dop.chapter05.the.existing.world.Services;
7 | import dop.chapter05.the.existing.world.Services.ApprovalsAPI.ApprovalStatus;
8 | import dop.chapter05.the.existing.world.Services.ContractsAPI;
9 | import dop.chapter05.the.existing.world.Services.ContractsAPI.PaymentTerms;
10 | import dop.chapter05.the.existing.world.Services.RatingsAPI.CustomerRating;
11 | import dop.chapter06.the.implementation.Core;
12 | import dop.chapter06.the.implementation.Service;
13 | import dop.chapter06.the.implementation.Types;
14 | import dop.chapter06.the.implementation.Types.*;
15 | import dop.chapter06.the.implementation.Types.Lifecycle.Draft;
16 | import dop.chapter06.the.implementation.Types.ReviewedFee.Billable;
17 | import dop.chapter06.the.implementation.Types.ReviewedFee.NotBillable;
18 | import org.junit.jupiter.api.Assertions;
19 | import org.junit.jupiter.api.Test;
20 | import org.mockito.MockedStatic;
21 |
22 | import java.time.LocalDate;
23 | import java.time.temporal.TemporalAdjuster;
24 | import java.time.temporal.TemporalAdjusters;
25 | import java.util.*;
26 | import java.util.function.Function;
27 | import java.util.stream.Stream;
28 |
29 | import static dop.chapter05.the.existing.world.Entities.InvoiceStatus.OPEN;
30 | import static dop.chapter05.the.existing.world.Entities.InvoiceType.STANDARD;
31 | import static dop.chapter05.the.existing.world.Services.ApprovalsAPI.ApprovalStatus.*;
32 | import static java.time.temporal.ChronoUnit.DAYS;
33 | import static java.time.temporal.TemporalAdjusters.lastDayOfMonth;
34 | import static org.mockito.Mockito.*;
35 |
36 | /**
37 | * Chapter 6 walks through implementing the domain model
38 | * we came up with in chapter 5. However, that'd be pretty
39 | * boring on its own, so that chapter is *really* about
40 | * the choices we make while designing. It's about functions!
41 | * And determinism! And testability! And a whole host of other
42 | * things! It's a fun one.
43 | */
44 | public class Listings {
45 |
46 | /**
47 | * ───────────────────────────────────────────────────────
48 | * Listing 6.1 through 6.3
49 | * ───────────────────────────────────────────────────────
50 | * We kick off this chapter in a predicable place: semantics.
51 | * Step one is defining what we mean when we say "function."
52 | */
53 | @Test
54 | public void listing6_1_to_6_3() {
55 | class Example {
56 | public Integer plusOne(Integer x) { // ◄───┐ Is this a function? A method? Both?
57 | return x + 1;
58 | }
59 |
60 | Function plusSomething = // ◄───┐ What about this one?
61 | (x) -> x + new Random().nextInt(); // │
62 | }
63 |
64 | // Everything in Java is technically a method -- even those
65 | // things we call "anonymous functions"
66 |
67 | Stream.of(1,2,3).map((x) -> x + 1).toList();
68 | // └────────────┘
69 | // ▲
70 | // └─────────────────────────────────┐
71 | Stream.of(1,2,3).map(new Function() { // │ They de-sugar to classes with
72 | @Override // │ methods behind the scenes.
73 | public Integer apply(Integer integer) { // ◄───────────┘
74 | return integer + 1;
75 | }
76 | }).toList();
77 |
78 |
79 | class Example2 {
80 | // Semantics wise. We'd say this method is acting
81 | // as a function because it's **deterministic**
82 | public Integer plusOne(Integer x) {
83 | return x + 1;
84 | }
85 | // Whereas this one -- despite being called a Function
86 | // in Java -- does not meet our semantic meaning due to
87 | // its reliance on non-deterministic Randomness.
88 | Function plusSomething =
89 | (x) -> x + new Random().nextInt();
90 | }
91 | }
92 |
93 |
94 | /**
95 | * ───────────────────────────────────────────────────────
96 | * Listing 6.4
97 | * ───────────────────────────────────────────────────────
98 | * "Is this deterministic?" is an easy question we can ask
99 | * about any method we write.
100 | */
101 | @Test
102 | public void listing6_4() {
103 |
104 |
105 | class Example {
106 | ContractsAPI contractsAPI;
107 |
108 | //┌───────────────────────────────┐
109 | //│ Is this method deterministic? │
110 | //└───────────────────────────────┘
111 | private LocalDate figureOutDueDate() {
112 | // Nope.
113 | // We're coupled to the environment. We'll get a different answer
114 | // each time we run this.
115 | LocalDate today = LocalDate.now();
116 | // We're also dependent on the whims of some system totally
117 | // outside our own. ─┐
118 | // ▼
119 | PaymentTerms terms = contractsAPI.getPaymentTerms("some customer ID");
120 | switch (terms) {
121 | case PaymentTerms.NET_30:
122 | return today.plusDays(30);
123 | default:
124 | // (other cases skipped for brevity)
125 | return today;
126 | }
127 | }
128 | }
129 | }
130 |
131 |
132 | /**
133 | * ───────────────────────────────────────────────────────
134 | * Listing 6.5
135 | * ───────────────────────────────────────────────────────
136 | * Non-deterministic code is more tedious to test.
137 | * We have to control for all the non-deterministic things.
138 | */
139 | @Test
140 | void listing6_5() {
141 | // We need all of this setup before we can test anything.
142 | ContractsAPI mockApi = mock(ContractsAPI.class);
143 | when(mockApi.getPaymentTerms(anyString())).thenReturn(PaymentTerms.NET_30);
144 | LocalDate date = LocalDate.of(2021, 1, 1);
145 | try (MockedStatic dateMock = mockStatic(LocalDate.class)) {
146 | dateMock.when(LocalDate::now).thenReturn(date);
147 | // we can finally start constructing the objects
148 | // with these mocks down here.
149 | }
150 | // Maybe out here we'll finally get around to asserting something...?
151 | }
152 |
153 |
154 | /**
155 | * ───────────────────────────────────────────────────────
156 | * Listing 6.6 through 6.8
157 | * ───────────────────────────────────────────────────────
158 | * The convention we use throughout the book is to make
159 | * methods static whenever we intend for them to be deterministic
160 | * functions.
161 | *
162 | * Functions take everything they need as input and do absolutely
163 | * nothing else other than use those inputs to compute an output.
164 | */
165 | @Test
166 | void listing6_6_to_6_8() {
167 | class Example {
168 | // ┌───────────────────────────────┐
169 | // │ Now this is deterministic! │
170 | // └───────────────────────────────┘
171 | //
172 | // ┌── Note that we've made it static!
173 | // ▼
174 | private static LocalDate figureOutDueDate(
175 | LocalDate today, // ┐ Everything we need is
176 | PaymentTerms terms // ┘ passed in as an argument.
177 | ) {
178 | //┌─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┐
179 | //│ We've removed the old connection │
180 | //│ to the outside world. │
181 | //└─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┘
182 | switch (terms) {
183 | case PaymentTerms.NET_30:
184 | return today.plusDays(30);
185 | default:
186 | // (other cases skipped for brevity)
187 | return today;
188 | }
189 | }
190 |
191 | // Testing is now dead simple.
192 | // No mocking. No test doubles. No controlling for
193 | // the whims of stuff outside our function.
194 | public void testFigureOutDueDate() {
195 | // ┌───── Inputs deterministically produce outputs.
196 | // ▼
197 | var result = figureOutDueDate(
198 | // ┌──────────────────────┐
199 | LocalDate.of(2026,1,1),
200 | PaymentTerms.NET_30
201 | ); // └──────────────────────┘
202 | // Everything we need to test is right here!
203 | // Outside world need not apply. We deal in
204 | // plain immutable data.
205 | }
206 | }
207 | }
208 |
209 |
210 | /**
211 | * ───────────────────────────────────────────────────────
212 | * Listing 6.9 through 6.10
213 | * ───────────────────────────────────────────────────────
214 | * The static convention one is useful in codebases because
215 | * it adds a bit of friction. Instance methods can act as
216 | * pure deterministic functions, but it takes discipline to
217 | * keep them that way (easier said than done on large teams)
218 | *
219 | * static tilts the scales in our favor towards functions
220 | * remaining functions over the long haul.
221 | */
222 | @Test
223 | void listing6_9_to_6_10() {
224 | class Example {
225 | ContractsAPI contractsAPI;
226 | // ┌── static acts as a protective barrier
227 | // ▼
228 | private static LocalDate figureOutDueDate(LocalDate today, PaymentTerms terms) {
229 | // contractsAPI ◄──── Even if we wanted to use that contractsAPI
230 | // on the instance we can't. The static keeps
231 | // us isolated (at least enough to make doing
232 | // the 'wrong' thing annoying).
233 |
234 | switch (terms) {
235 | case PaymentTerms.NET_30:
236 | return today.plusDays(30);
237 | default:
238 | // (other cases skipped for brevity)
239 | return today;
240 | }
241 | }
242 | }
243 | }
244 |
245 |
246 | /**
247 | * ───────────────────────────────────────────────────────
248 | * Listing 6.11 through 6.14
249 | * ───────────────────────────────────────────────────────
250 | * An interesting question: how many possible implementations
251 | * could we write for a given type signature?
252 | *
253 | * This will feel weird if you've never thought about it
254 | * before. Learning to think about what our code really
255 | * says -- or, more importantly, **enables** -- is a valuable
256 | * design skill.
257 | */
258 | @Test
259 | void listing6_11_to_6_14() {
260 |
261 | enum People {Bob, Mary}
262 | enum Jobs {Chef, Engineer}
263 |
264 | class SomeClass {
265 | // [Hidden] // ◄──── Assume all kinds of instance state here
266 |
267 | // How many different implementations could we write
268 | // inside of this method?
269 | People someMethod(Jobs job) {
270 | // this.setCombobulator("large");
271 | // universe.runSimulation(job);
272 | // collapseSingularity(this)
273 | return null; // ◄── This null isn't in the book. It's only
274 | // here so the code will compile. Pretend
275 | // ▲ the entire implementation is hidden.
276 | // │
277 | // │
278 | }// The answer is infinite.
279 | // Methods are allowed to do anything.
280 | }
281 |
282 | class __ {
283 | // ┌───── But what if we make that same method static?
284 | // ▼
285 | static People someMethod(Jobs job) {
286 | // [hidden]
287 | return null; // (again, ignore this null.)
288 | }
289 | // This won't seem important at first, but its implications
290 | // are far-reaching.
291 | // There is now a finite number of ways this method
292 | // could be implemented. And it's entirely determined
293 | // by the types we choose.
294 | //
295 | // We can enumerate them all!
296 | static People optionOne(Jobs job) {
297 | return switch (job) {
298 | case Chef -> People.Bob;
299 | case Engineer -> People.Bob;
300 | };
301 | }
302 |
303 | static People optionTwo(Jobs job) {
304 | return switch (job) {
305 | case Chef -> People.Mary;
306 | case Engineer -> People.Mary;
307 | };
308 | }
309 |
310 | static People optionThree(Jobs job) {
311 | return switch (job) {
312 | case Chef -> People.Bob;
313 | case Engineer -> People.Mary;
314 | };
315 | }
316 |
317 | static People optionFour(Jobs job) {
318 | return switch (job) {
319 | case Chef -> People.Mary;
320 | case Engineer -> People.Bob;
321 | };
322 | }
323 |
324 | // For some fun background that's not in the book
325 | // The number of possible implementations, or, in
326 | // math speak, the number of ways of mapping from one
327 | // set to another, is computed by
328 | // |InputType|^|OutputType|
329 | // i.e. the cardinality of the set of values in the input
330 | // type raised to the cardinality of the output type.
331 | //
332 | // You can see this in the example above. 2^2 = 4 possible
333 | // implementations this function can have.
334 | }
335 | }
336 |
337 |
338 | /**
339 | * ───────────────────────────────────────────────────────
340 | * Listing 6.15
341 | * ───────────────────────────────────────────────────────
342 | * Deterministic functions join together to form...
343 | *
344 | * ...
345 | *
346 | * another deterministic function!
347 | */
348 | @Test
349 | void listing6_15() {
350 | Function square = (x) -> x * x;
351 | Function inc = (x) -> x + 1;
352 |
353 | // Functions compose together!
354 | Function incAndSquare =
355 | inc.andThen(square);
356 | // This example is different from the book because the
357 | // book cheats in order to show the composition in the
358 | // context of functions we've defined for our domain.
359 | }
360 |
361 | /**
362 | * ───────────────────────────────────────────────────────
363 | * Listing 6.16
364 | * ───────────────────────────────────────────────────────
365 | * Functions are just tables of data in disguise.
366 | */
367 | @Test
368 | void listing6_16() {
369 | class __{
370 | // determinism means that every output for this function
371 | // is already pre-defined.
372 | static int increment(int x) {
373 | return x + 1;
374 | }
375 |
376 | // We could even compute all of its answers ahead of time.
377 | static Map ANSWERS = new HashMap<>();
378 | // [NOTE] This different from the book in that we don't actually
379 | // crawl over every integer (which would be very slow and
380 | // consume a lot of memory).
381 | static Integer PRETEND_MIN_VALUE = -10;
382 | static Integer PRETEND_MAX_VALUE = 10;
383 | static {
384 | for (int i = PRETEND_MIN_VALUE; i < PRETEND_MAX_VALUE-1; i++) {
385 | ANSWERS.put(i, increment(i));
386 | }
387 | }
388 | // We end up with a lookup table that maps inputs to outputs.
389 | // | Input | Output |
390 | // | 1 | 2 |
391 | // | 2 | 3 |
392 | // etc..
393 | }
394 | }
395 |
396 | /**
397 | * ───────────────────────────────────────────────────────
398 | * Listing 6.17 - 2.23
399 | * ───────────────────────────────────────────────────────
400 | * Determinism makes the line between where functions end
401 | * and data begins blurry.
402 | *
403 | * We can view functions AS themselves data. In fact, doing
404 | * so can make a lot of awkward modeling problems become clear.
405 | *
406 | * This section works from these requirements:
407 | * ---------------------------------------------------------
408 | * The customer shall have a Grace Period
409 | * Customers in good standing receive a 60-day grace period
410 | * Customers in acceptable standing receive a 30-day grace period
411 | * Customers in poor standing must pay by end of month
412 | */
413 | @Test
414 | void listing6_17_to_6_23() {
415 | // [Note] (code is commented out since it relies on things that won't compile.)
416 | //
417 | // ┌───── If we wanted to implement grace period
418 | // │ as a deterministic function. What would it return?
419 | // ▼
420 | // static ??? gracePeriod(CustomerRating rating) { #A
421 | // ???
422 | // }
423 | //
424 | // We could try throwing a lot of "types" at it. Maybe we introduce
425 | // a Days data type?
426 | //
427 | // Days gracePeriod(CustomerRating rating) {
428 | // return switch(rating) {
429 | // case CustomerRating.GOOD -> new Days(60);
430 | // case CustomerRating.ACCEPTABLE -> new Days(30);
431 | // case CustomerRating.POOR -> ???
432 | // } ▲
433 | // } └── But we get stuck here because it
434 | // depends on something other than rating
435 | //
436 | class __ {
437 | // ┌───── The requirement is expressing a *relationship* between
438 | // │ two dates. The business rule is itself a function.
439 | // │
440 | // ▼
441 | static Function gracePeriod(CustomerRating rating) {
442 | return switch(rating) {
443 | case CustomerRating.GOOD -> date -> date.plusDays(60);
444 | case CustomerRating.ACCEPTABLE -> date -> date.plusDays(30);
445 | case CustomerRating.POOR -> date -> date.with(lastDayOfMonth());
446 | };
447 | }
448 |
449 | // ┌───── We don't have to define our own function.
450 | // │ Java has one built in.
451 | // │
452 | // ▼
453 | static TemporalAdjuster gracePeriodV2(CustomerRating rating) {
454 | return switch(rating) {
455 | case CustomerRating.GOOD -> date -> date.plus(60, DAYS);
456 | case CustomerRating.ACCEPTABLE -> date -> date.plus(30, DAYS);
457 | case CustomerRating.POOR -> lastDayOfMonth();
458 | };
459 | }
460 |
461 | // The reward for this modeling is code that reads exactly like the
462 | // requirements.
463 | static boolean isPastDue(
464 | LocalDate evaluationDate, Invoice invoice, CustomerRating rating) {
465 | return evaluationDate.isAfter(invoice.getDueDate().with(gracePeriodV2(rating)));
466 |
467 | }
468 |
469 | // BUT!
470 |
471 | // This is not to say there's one "right" way of modeling this requirement.
472 | // Equally fine would be something like this.
473 | // Instead of returning a function that produces data *later*, we pass in
474 | // more data so that it can compute the result *now*.
475 | static LocalDate mustHavePaidBy(Invoice invoice, CustomerRating rating) {
476 | return switch(rating) {
477 | case CustomerRating.GOOD -> invoice.getDueDate().plusDays(60);
478 | case CustomerRating.ACCEPTABLE -> invoice.getDueDate().plusDays(30);
479 | case CustomerRating.POOR -> invoice.getDueDate().with(lastDayOfMonth());
480 | };
481 | }
482 |
483 | // These are all fine approaches!
484 | }
485 | }
486 |
487 |
488 | /**
489 | * ───────────────────────────────────────────────────────
490 | * Listing 6.24
491 | * ───────────────────────────────────────────────────────
492 | * Don't drive yourself crazy over purity and referential
493 | * transparency. Close enough is good enough.
494 | */
495 | @Test
496 | void listing6_24() {
497 | class __ {
498 | //
499 | record PastDue(Invoice invoice) {}
500 | // ▲
501 | // └── We depend on a mutable identity object. We can never
502 | // truly be referential transparent because the "same" object
503 | // could lead to different results.
504 | //
505 | // But this is just being needlessly pedantic 99.999999999999% of the time.
506 | // As long as you're not sharing references around or performing mutation
507 | // the risk here is low enough to ignore.
508 | }
509 | }
510 |
511 |
512 | /**
513 | * ───────────────────────────────────────────────────────
514 | * Listing 6.25
515 | * ───────────────────────────────────────────────────────
516 | * Where do things go?
517 | * Divide them up by their determinism!
518 | */
519 | @Test
520 | void listing6_25() {
521 | /*
522 | Assume a file system like:
523 |
524 | com.dop.invoicing
525 | |- latefees
526 | | |- Core ◄─── We'll put all our deterministic code here.
527 | | |- Service
528 | | |- Types
529 | */
530 | }
531 |
532 | /**
533 | * ───────────────────────────────────────────────────────
534 | * Listing 6.26 through 6.29
535 | * ───────────────────────────────────────────────────────
536 | * Implementation begins!
537 | * This is where we'll start to see our modeling efforts begin
538 | * to pay us back. Most of the functions will just follow the
539 | * types we designed.
540 | */
541 | @Test
542 | void listing6_26_to_6_29() {
543 | class V1 {
544 | // Here's where we left on in Chapter 5.
545 | public static List collectPastDue(
546 | EnrichedCustomer customer,
547 | LocalDate today,
548 | List invoices) {
549 | // Implement me!
550 | return null;
551 | }
552 | }
553 |
554 | class V2 {
555 | static boolean TODO = true;
556 | // This is where all the function stuff we talked about comes into play.
557 | // Deterministic functions can only do what their types say.
558 | // That's all they can do.
559 | // Which means that our implementation just follows the types we designed.
560 | public static List collectPastDue(
561 | EnrichedCustomer customer,
562 | LocalDate today,
563 | List invoices) {
564 | // everything other than the filter is just doing what the type
565 | // signature says.
566 | return invoices.stream()
567 | // .filter(invoice -> ???) ◄─── We just have to decide what goes here.
568 | .map(PastDue::new)
569 | .toList();
570 | }
571 | }
572 | class V3 {
573 | public static List collectPastDue(
574 | EnrichedCustomer customer,
575 | LocalDate today,
576 | List invoices) {
577 | return invoices.stream()
578 | // ┌──── Adding in the filter implementation
579 | // ▼
580 | .filter(invoice -> isPastDue(invoice, customer.rating(), today))
581 | .map(PastDue::new)
582 | .toList();
583 | }
584 |
585 | static boolean isPastDue(Invoice invoice, CustomerRating rating, LocalDate today) {
586 | return invoice.getInvoiceType().equals(STANDARD)
587 | && invoice.getStatus().equals(OPEN)
588 | && today.isAfter(invoice.getDueDate().with(gracePeriod(rating)));
589 | // └────────────────────────────────────────────────────────────┘
590 | // │
591 | // └─ Note how much this reads like the requirement! Neat!
592 | }
593 |
594 | // (We defined this one a few listings ago.)
595 | static TemporalAdjuster gracePeriod(CustomerRating rating) {
596 | return switch(rating) {
597 | case CustomerRating.GOOD -> date -> date.plus(60, DAYS);
598 | case CustomerRating.ACCEPTABLE -> date -> date.plus(30, DAYS);
599 | case CustomerRating.POOR -> lastDayOfMonth();
600 | };
601 | }
602 | }
603 | }
604 |
605 | /**
606 | * ───────────────────────────────────────────────────────
607 | * Listing 6.30 through 6.32
608 | * ───────────────────────────────────────────────────────
609 | * "Just use maps"?
610 | */
611 | @Test
612 | void listing6_30_to_6_32() {
613 | class V1 {
614 | // A very popular recommendation for data-oriented programming
615 | // is the idea that you should "just use maps".
616 | //
617 | // When presented with an implementation like this:
618 | static TemporalAdjuster gracePeriod(CustomerRating rating) {
619 | return switch(rating) {
620 | case CustomerRating.GOOD -> date -> date.plus(60, DAYS);
621 | case CustomerRating.ACCEPTABLE -> date -> date.plus(30, DAYS);
622 | case CustomerRating.POOR -> lastDayOfMonth();
623 | };
624 | }
625 | // A natural question, given what we know about determinism, would
626 | // be why we need the function at all. Why not express this as _data_?
627 | static Map gracePeriodV2 = Map.of(
628 | CustomerRating.GOOD, date -> date.plus(60, DAYS),
629 | CustomerRating.ACCEPTABLE, date -> date.plus(30, DAYS),
630 | CustomerRating.POOR, TemporalAdjusters.lastDayOfMonth()
631 | );
632 |
633 | // We could refactor like this:
634 | static boolean isPastDueV2(Invoice invoice, CustomerRating rating, LocalDate today) {
635 | return invoice.getInvoiceType().equals(STANDARD)
636 | && invoice.getStatus().equals(OPEN)
637 | && today.isAfter(invoice.getDueDate().with(gracePeriodV2.get(rating)));
638 | // └───────────────────────────┘
639 | // ▲
640 | // Replaces a function call with a map lookup! ───┘
641 | }
642 |
643 | // But This is a dangerous refactor.
644 | // What algebraic types and pattern matching give is *exhaustiveness* at
645 | // compile time. The compiler knows if you've checked every case and will
646 | // tell you if you didn't.
647 | //
648 | // You "know" the map has everything in it *today*, but there's no way to
649 | // guarantee it tomorrow. Semvar is a lie. "Minor" version bumps break software
650 | // all the time.
651 | //
652 | // Since you can't be sure, and the compiler can't help, you have to defend.
653 | static boolean isPastDueV3(Invoice invoice, CustomerRating rating, LocalDate today) {
654 | TemporalAdjuster WHAT_GOES_HERE = TemporalAdjusters.firstDayOfMonth();
655 | return invoice.getInvoiceType().equals(STANDARD)
656 | && invoice.getStatus().equals(OPEN)
657 | && today.isAfter(invoice.getDueDate()
658 | .with(gracePeriodV2.getOrDefault(rating, WHAT_GOES_HERE)));
659 | // └───────────┘ └───────────┘
660 | // ▲ ▲
661 | // We're forced to do this ───┘ │
662 | // │
663 | // But notice that we're inventing solutions to ──┘
664 | // problems that ONLY exist because of our modeling
665 | // choices. The domain doesn't define a "default"
666 | // grace period.
667 | }
668 | // Good modeling should eliminate illegal states not introduce them!
669 | }
670 | }
671 |
672 | /**
673 | * ───────────────────────────────────────────────────────
674 | * Listing 6.31 through 6.38
675 | * ───────────────────────────────────────────────────────
676 | * The right type can reveal shortcomings in the design of the system.
677 | */
678 | @Test
679 | void listing6_31_to_6_38() {
680 | class V1 {
681 | // I'll keep drawing attention to it.
682 | // Checkout this type signature. It takes a list of invoices and
683 | // returns a single LateFee draft.
684 | // Our implementation is forced into being "small." Functions can't go off and do
685 | // anything they way. They map inputs to outputs.
686 | static LateFee buildDraft(LocalDate today, EnrichedCustomer customer, List invoices) {
687 | // Which means that this is pretty much the only implementation
688 | // that's even allowed by our types. It HAS to return this data type.
689 | return new LateFee<>( //─┐
690 | new Draft(), // │ And all of this is pre-ordained.
691 | customer, // │
692 | null, // │ The only thing left for us to do is implement the
693 | today, // │ thing the computes the total and the due dates.
694 | null, // │
695 | invoices //─┘
696 | );
697 | }
698 | }
699 | class V2 {
700 | // I'll keep drawing attention to it.
701 | // Checkout this type signature. It takes a list of invoices and
702 | // returns a single LateFee draft.
703 | // Our implementation is forced into being "small." Functions can't go off and do
704 | // anything they way. They map inputs to outputs.
705 | static LateFee buildDraft(LocalDate today, EnrichedCustomer customer, List invoices) {
706 | // Which means that this is pretty much the only implementation
707 | // that's even allowed by our types. It HAS to return this data type.
708 | return new LateFee<>( //─┐
709 | new Draft(), // │ And all of this is pre-ordained.
710 | customer, // │
711 | null, // │ The only thing left for us to do is implement the
712 | today, // │ thing the computes the total and the due dates.
713 | null, // │
714 | invoices //─┘
715 | );
716 | }
717 |
718 | // Implementing the due date is easy. If follows the requirements.
719 | static LocalDate dueDate(LocalDate today, PaymentTerms terms) {
720 | // Note that well typed functions are small! Often the first thing
721 | // we do is start returning data.
722 | return switch (terms) {
723 | case PaymentTerms.NET_30 -> today.plusDays(30);
724 | case PaymentTerms.NET_60 -> today.plusDays(60);
725 | case PaymentTerms.DUE_ON_RECEIPT -> today;
726 | case PaymentTerms.END_OF_MONTH -> today.with(lastDayOfMonth());
727 | };
728 | }
729 |
730 | // computing the total is far more interesting, because it holds something
731 | // that feels gross.
732 |
733 | // The "outside world" speaks BigDecimal and Currency.
734 | // Our world speaks USD (per the requirements).
735 | // ┌──────────────────────────────────────────────────────────────────┐
736 | // THE FACT THAT THIS FEELS AWFUL IS A FEATURE
737 | // └──────────────────────────────────────────────────────────────────┘
738 | //
739 | // We shouldn't do this conversion in "our world." In fact, we shouldn't
740 | // "do" it at all. In an ideal world, we'd enforce this USD invariant
741 | // on data as it enters our system -- a process far removed from our
742 | // feature.
743 | //
744 | // The types are telling us that something is wrong with the design **of the system**.
745 | //
746 | // We don't have to fix it now, but we should call it out.
747 | static USD unsafeGetChargesInUSD(LineItem lineItem) throws IllegalArgumentException {
748 | if (!lineItem.getCurrency().getCurrencyCode().equals("USD")) {
749 | // If this ever throws, the system as a whole is in a bad state.
750 | throw new IllegalArgumentException("Big scary message here");
751 | } else {
752 | return new USD(lineItem.getCharges());
753 | }
754 | }
755 |
756 | // Putting it all together, we get:
757 | static USD computeTotal(List invoices) {
758 | return invoices.stream().map(PastDue::invoice)
759 | .flatMap(x -> x.getLineItems().stream())
760 | .map(V2::unsafeGetChargesInUSD)
761 | .reduce(USD.zero(), USD::add);
762 | }
763 | }
764 | }
765 |
766 |
767 | /**
768 | * ───────────────────────────────────────────────────────
769 | * Listing 6.39 through 6.43
770 | * ───────────────────────────────────────────────────────
771 | * The Optional holy war
772 | */
773 | @Test
774 | void listing6_39_to_6_43() {
775 | // [note] listings 6.39 and 6.40 are skipped here
776 | // since they're covered in the implementation package.
777 | // see: dop.chapter06.the.implementation
778 | //
779 | // Instead, we'll focus on.... Optional!
780 | class __{
781 | // (ignore this. It's just here to power the below examples)
782 | static Optional tryToFindThing(String whatever) {
783 | return Optional.empty();
784 | }
785 |
786 | // Optionals are contentious because we can interact with them
787 | // both functionally and imperatively.
788 | //
789 | // Here's the imperative style
790 | static String imperativeExample(String thingId) {
791 | Optional maybeThing = tryToFindThing(thingId);
792 | return maybeThing.isPresent()
793 | ? maybeThing.get().toUpperCase()
794 | : "Nothing found!";
795 | }
796 | // Here's the functional approach.
797 | static String functionalExample(String thingId) {
798 | return tryToFindThing(thingId)
799 | .map(String::toUpperCase)
800 | .orElse("Nothing Found!");
801 | }
802 | // The question is: which is better?
803 |
804 | // Is this:
805 | static String example1(LateFee draft) {
806 | return draft.customer().approval().map(approval -> switch(approval.status()) {
807 | case APPROVED -> "new Billable(draft)"; // (stringified to mirror the shortened book example)
808 | case PENDING -> "new NotBillable(draft, ...)";
809 | case DENIED -> "new NotBillable(draft, ...)";
810 | }).orElse("new NeedsApproval(draft)");
811 | }
812 | // better than this?
813 | static String example2(LateFee draft) {
814 | return draft.customer().approval().isEmpty()
815 | ? "new NeedsApproval(draft)"
816 | : switch (draft.customer().approval().get().status()) {
817 | case APPROVED -> "new Billable(draft)";
818 | case PENDING -> "new NotBillable(draft, ...)";
819 | case DENIED -> "new NotBillable(draft, ...)";
820 | };
821 | }
822 | // Or are they just different?
823 | // The book makes an argument for each in different situations.
824 | }
825 | }
826 |
827 |
828 | /**
829 | * ───────────────────────────────────────────────────────
830 | * Listing 6.44 through 6.47
831 | * ───────────────────────────────────────────────────────
832 | * It's ok to introduce types! As many as you need!
833 | *
834 | * Clarity while reading > enjoyment while writing.
835 | */
836 | @Test
837 | void listing6_44() {
838 | class __ {
839 | // we being here.
840 | // This is an OK method, but it's visually assaulting. It's too dense to
841 | // understand without slowing down to study it.
842 | public static ReviewedFee assessDraft(Entities.Rules rules, LateFee draft) {
843 | if (draft.total().value().compareTo(rules.getMinimumFeeThreshold()) < 0) {
844 | return new NotBillable(draft, new Reason("Below threshold"));
845 | } else if (draft.total().value().compareTo(rules.getMaximumFeeThreshold()) > 0) {
846 | return draft.customer().approval().isEmpty()
847 | ? new ReviewedFee.NeedsApproval(draft)
848 | : switch (draft.customer().approval().get().status()) {
849 | case APPROVED -> new Billable(draft);
850 | case PENDING -> new NotBillable(draft, new Reason("Pending decision"));
851 | case DENIED -> new NotBillable(draft, new Reason("exempt from large fees"));
852 | };
853 | } else {
854 | return new Billable(draft);
855 | }
856 | }
857 | }
858 |
859 | // So what if we did this: separate where we make a decision from what we do with it.
860 | class V2 {
861 | // We can introduce our own private enum to explain the semantics behind
862 | // what the assessments mean.
863 | private enum Assessment {ABOVE_MAXIMUM, BELOW_MINIMUM, WITHIN_RANGE}
864 |
865 | // and we can use that while figuring out what's up with the total.
866 | // doing so visually (and cognitively!) simplifies the code. We only
867 | // worry about one thing at a time.
868 | static Assessment assessTotal(Entities.Rules rules, USD total) {
869 | if (total.value().compareTo(rules.getMinimumFeeThreshold()) < 0) {
870 | return Assessment.BELOW_MINIMUM;
871 | } else if (total.value().compareTo(rules.getMaximumFeeThreshold()) > 0) {
872 | return Assessment.ABOVE_MAXIMUM;
873 | } else {
874 | return Assessment.WITHIN_RANGE;
875 | }
876 | }
877 | // Which in turn simplifies this method.
878 | // It's been reduced down to pattern matching. This is quick to skim
879 | // and quick to understand.
880 | public static ReviewedFee assessDraft(Entities.Rules rules, LateFee draft) {
881 | return switch (assessTotal(rules, draft.total())) {
882 | case Assessment.WITHIN_RANGE -> new Billable(draft);
883 | case Assessment.BELOW_MINIMUM -> new NotBillable(draft, new Reason("Below threshold"));
884 | case Assessment.ABOVE_MAXIMUM -> draft.customer().approval().isEmpty()
885 | ? new ReviewedFee.NeedsApproval(draft)
886 | : switch (draft.customer().approval().get().status()) {
887 | case APPROVED -> new Billable(draft);
888 | case PENDING -> new NotBillable(draft, new Reason("Pending decision"));
889 | case DENIED -> new NotBillable(draft, new Reason("exempt from large fees"));
890 | };
891 | };
892 | }
893 | }
894 | }
895 |
896 | /**
897 | * ───────────────────────────────────────────────────────
898 | * Listing 6.48 - 6.55
899 | * ───────────────────────────────────────────────────────
900 | * The final listings in the book are tours of the final
901 | * implementation.
902 | *
903 | * Rather than duplicate them here, I've added the full
904 | * implementation to java/dop/chapter06/the/implementation.
905 | * You can browse all the source there in context.
906 | */
907 | @Test
908 | void listing6_48() {
909 | // See: java/dop/chapter06/the/implementation
910 | }
911 | }
912 |
--------------------------------------------------------------------------------