├── graphics ├── book-cover.JPG └── cover-art.png ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── .gitattributes ├── ascii-arrows ├── app ├── src │ ├── test │ │ └── java │ │ │ └── dop │ │ │ ├── chapter04 │ │ │ ├── SealingExample.java │ │ │ └── Listings.java │ │ │ ├── chapter08 │ │ │ └── example-graph.dot │ │ │ ├── chapter05 │ │ │ └── the │ │ │ │ └── existing │ │ │ │ └── world │ │ │ │ ├── Annotations.java │ │ │ │ ├── Repositories.java │ │ │ │ ├── Services.java │ │ │ │ └── Entities.java │ │ │ ├── chapter06 │ │ │ ├── the │ │ │ │ └── implementation │ │ │ │ │ ├── Facade.java │ │ │ │ │ ├── Types.java │ │ │ │ │ ├── Core.java │ │ │ │ │ └── Service.java │ │ │ └── Listings.java │ │ │ ├── chapter01 │ │ │ └── Listings.java │ │ │ └── chapter03 │ │ │ └── Listings.java │ └── main │ │ ├── resources │ │ └── log4j2.xml │ │ └── java │ │ └── dop │ │ └── App.java └── build.gradle.kts ├── settings.gradle.kts ├── .gitignore ├── LICENSE ├── gradlew.bat ├── README.md └── gradlew /graphics/book-cover.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriskiehl/Data-Oriented-Programming-In-Java-Book/HEAD/graphics/book-cover.JPG -------------------------------------------------------------------------------- /graphics/cover-art.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriskiehl/Data-Oriented-Programming-In-Java-Book/HEAD/graphics/cover-art.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriskiehl/Data-Oriented-Programming-In-Java-Book/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # Linux start script should use lf 5 | /gradlew text eol=lf 6 | 7 | # These are Windows script files and should use crlf 8 | *.bat text eol=crlf 9 | 10 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /ascii-arrows: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ◄───── ───► 5 | ◄────┐ ┌──► 6 | │ │ 7 | ◄────┘ └──► 8 | 9 | ┌──────────┐ 10 | │ │ 11 | └──────────┘ 12 | ▲ ▲ 13 | │ ── 14 | │ 15 | │ ┌───── 16 | ▼ 17 | 18 | ┌───── 19 | ▼ 20 | ▲ 21 | └── 22 | 23 | ▲ 24 | ──┘ 25 | 26 | -------------------------------------------------------------------------------- /app/src/test/java/dop/chapter04/SealingExample.java: -------------------------------------------------------------------------------- 1 | package dop.chapter04; 2 | 3 | /** 4 | * Supplement to dop/chapter04/Listings. 5 | */ 6 | public sealed interface SealingExample { 7 | record Foo() implements SealingExample {}; 8 | record Bar() implements SealingExample {}; 9 | record Baz() implements SealingExample {}; 10 | } 11 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | # This file was generated by the Gradle 'init' task. 2 | # https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format 3 | 4 | [versions] 5 | guava = "32.1.2-jre" 6 | junit-jupiter = "5.10.0" 7 | 8 | [libraries] 9 | guava = { module = "com.google.guava:guava", version.ref = "guava" } 10 | junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" } 11 | -------------------------------------------------------------------------------- /app/src/main/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | * 4 | * The settings file is used to specify which projects to include in your build. 5 | * For more detailed information on multi-project builds, please refer to https://docs.gradle.org/8.5/userguide/building_swift_projects.html in the Gradle documentation. 6 | */ 7 | 8 | plugins { 9 | // Apply the foojay-resolver plugin to allow automatic download of JDKs 10 | id("org.gradle.toolchains.foojay-resolver-convention") version "0.7.0" 11 | } 12 | 13 | rootProject.name = "dop" 14 | include("app") 15 | -------------------------------------------------------------------------------- /app/src/main/java/dop/App.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This Java source file was generated by the Gradle 'init' task. 3 | */ 4 | package dop; 5 | 6 | /** 7 | * Howdy! 8 | * 9 | * Thanks for checking out the repository for the book! 10 | * This is just a placeholder. I've put all the supporting 11 | * code for the book's listings in the test/ package. 12 | */ 13 | public class App { 14 | 15 | public static void main(String[] args) { 16 | System.out.println("Hello from Data Oriented Java!"); 17 | System.out.println("All the supporting material and listings can be found in the /tests package"); 18 | System.out.println("https://github.com/chriskiehl/Data-Oriented-Programming-In-Java-Book/tree/main/app/src/test/java/dop"); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/test/java/dop/chapter08/example-graph.dot: -------------------------------------------------------------------------------- 1 | digraph Rule { 2 | rankdir=TD; 3 | node_9d54a989 [label="And"] 4 | node_9d54a989 -> node_cf7ac4e3 5 | node_9d54a989 -> node_141bf586 6 | node_cf7ac4e3 [label="Or"] 7 | node_cf7ac4e3 -> node_a4bf6d1d 8 | node_cf7ac4e3 -> node_85ef509a 9 | node_a4bf6d1d [label="Or"] 10 | node_a4bf6d1d -> node_85ef8908 11 | node_a4bf6d1d -> node_85ef4003 12 | node_85ef8908 [label="COUNTRY=US"] 13 | node_85ef4003 [label="COUNTRY=BE"] 14 | node_85ef509a [label="COUNTRY=FR"] 15 | node_141bf586 [label="Or"] 16 | node_141bf586 -> node_930ad8e0 17 | node_141bf586 -> node_a5e4027c 18 | node_930ad8e0 [label="SEGMENT=public"] 19 | node_a5e4027c [label="Not"] 20 | node_a5e4027c -> node_b83f3b85 21 | node_b83f3b85 [label="REGION=LATAM"] 22 | } 23 | -------------------------------------------------------------------------------- /app/src/test/java/dop/chapter05/the/existing/world/Annotations.java: -------------------------------------------------------------------------------- 1 | package dop.chapter05.the.existing.world; 2 | 3 | /** 4 | * These don't do anything. They're inspired by things 5 | * like Spring and Hibernate, but the specifics are kept purposefully vague. 6 | * 7 | * The pressures ORM-like libraries place on us are the same regardless 8 | * the specific technology involved. 9 | */ 10 | public interface Annotations { 11 | public @interface Entity {} 12 | 13 | public @interface Id {} 14 | public @interface OneToMany {} 15 | public @interface OneToOne {} 16 | public @interface ManyToMany {} 17 | 18 | public @interface Transaction {} 19 | public @interface Service {} 20 | public @interface Controller {} 21 | 22 | public interface Repository{} 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | .idea/ 23 | 24 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 25 | hs_err_pid* 26 | replay_pid* 27 | 28 | .gradle 29 | **/build/ 30 | !src/**/build/ 31 | 32 | # Ignore Gradle GUI config 33 | gradle-app.setting 34 | 35 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 36 | !gradle-wrapper.jar 37 | 38 | # Avoid ignore Gradle wrappper properties 39 | !gradle-wrapper.properties 40 | 41 | # Cache of project 42 | .gradletasknamecache 43 | 44 | # Eclipse Gradle plugin generated files 45 | # Eclipse Core 46 | .project 47 | # JDT-specific (Eclipse Java Development Tools) 48 | .classpath 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Chris 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/src/test/java/dop/chapter05/the/existing/world/Repositories.java: -------------------------------------------------------------------------------- 1 | package dop.chapter05.the.existing.world; 2 | 3 | 4 | import dop.chapter05.the.existing.world.Entities.Customer; 5 | import dop.chapter05.the.existing.world.Entities.Invoice; 6 | import dop.chapter05.the.existing.world.Entities.Rules; 7 | 8 | import java.math.BigDecimal; 9 | import java.util.List; 10 | 11 | // A note on difference between the book and this repo: 12 | // These objects are never explicitly defined in the book. 13 | // We assume they're standard Data Access Objects / Repositories. 14 | // i.e. they have ORM style CRUD methods like findAll() get(), save() etc.. 15 | public class Repositories { 16 | 17 | public interface CustomerRepo { 18 | List findAll(); 19 | void save(Customer customer); 20 | 21 | } 22 | 23 | public interface InvoiceRepo { 24 | void save(Invoice invoice); 25 | List findInvoices(String customerId); 26 | } 27 | 28 | public interface RulesRepo { 29 | Rules loadDefaults(); 30 | } 31 | 32 | public interface FeesRepo { 33 | BigDecimal get(String countryCode); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/test/java/dop/chapter05/the/existing/world/Services.java: -------------------------------------------------------------------------------- 1 | package dop.chapter05.the.existing.world; 2 | 3 | import java.util.Optional; 4 | 5 | public class Services { 6 | public interface RatingsAPI { 7 | enum CustomerRating {GOOD, ACCEPTABLE, POOR} 8 | CustomerRating getRating(String customerId); 9 | } 10 | 11 | public interface ContractsAPI { 12 | enum PaymentTerms {NET_30, NET_60, END_OF_MONTH, DUE_ON_RECEIPT} 13 | PaymentTerms getPaymentTerms(String customerId); 14 | } 15 | 16 | public interface ApprovalsAPI { 17 | enum ApprovalStatus {PENDING, APPROVED, DENIED} 18 | record Approval(String id, ApprovalStatus status){} 19 | record CreateApprovalRequest(/*...*/) {} 20 | Approval createApproval(CreateApprovalRequest request); 21 | Optional getApproval(String approvalId); 22 | } 23 | 24 | public interface BillingAPI { 25 | enum Status {ACCEPTED, REJECTED} 26 | record SubmitInvoiceRequest(/*...*/) {} 27 | record BillingResponse( 28 | Status status, 29 | String invoiceId, 30 | String error 31 | ){} 32 | BillingResponse submit(SubmitInvoiceRequest request); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/test/java/dop/chapter06/the/implementation/Facade.java: -------------------------------------------------------------------------------- 1 | package dop.chapter06.the.implementation; 2 | 3 | import dop.chapter05.the.existing.world.Entities; 4 | import dop.chapter05.the.existing.world.Repositories; 5 | import dop.chapter05.the.existing.world.Services; 6 | 7 | import java.util.Objects; 8 | import java.util.Optional; 9 | import java.util.stream.Stream; 10 | 11 | 12 | public class Facade { 13 | private Services.RatingsAPI ratingsApi; 14 | private Services.ContractsAPI contractsApi; 15 | private Services.ApprovalsAPI approvalsApi; 16 | private Services.BillingAPI billingApi; 17 | private Repositories.CustomerRepo customerRepo; 18 | private Repositories.InvoiceRepo invoiceRepo; 19 | private Repositories.RulesRepo rulesRepo; 20 | private Repositories.FeesRepo feesRepo; 21 | 22 | public Stream findAll() { 23 | return customerRepo.findAll().stream().map(this::enrich); 24 | } 25 | 26 | private Types.EnrichedCustomer enrich(Entities.Customer customer) { 27 | String country = customer.getAddress().getCountry(); 28 | Types.Percent feePercentage = new Types.Percent(feesRepo.get(country).doubleValue(), 1.0); 29 | Services.RatingsAPI.CustomerRating rating = ratingsApi.getRating(customer.getId()); 30 | Services.ContractsAPI.PaymentTerms terms = contractsApi.getPaymentTerms(customer.getId()); 31 | String approvalId = customer.getApprovalId(); 32 | Optional status = Objects.isNull(approvalId) 33 | ? Optional.empty() 34 | : this.approvalsApi.getApproval(approvalId); 35 | 36 | return new Types.EnrichedCustomer( 37 | new Types.CustomerId(customer.getId()), 38 | customer.getAddress(), 39 | feePercentage, 40 | terms, 41 | rating, 42 | status 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | * 4 | * This generated file contains a sample Java application project to get you started. 5 | * For more details on building Java & JVM projects, please refer to https://docs.gradle.org/8.5/userguide/building_java_projects.html in the Gradle documentation. 6 | */ 7 | 8 | plugins { 9 | // Apply the application plugin to add support for building a CLI application in Java. 10 | application 11 | } 12 | 13 | repositories { 14 | // Use Maven Central for resolving dependencies. 15 | mavenCentral() 16 | } 17 | 18 | dependencies { 19 | // Use JUnit Jupiter for testing. 20 | testImplementation(libs.junit.jupiter) 21 | implementation("com.opencsv:opencsv:5.12.0") 22 | implementation("jakarta.validation:jakarta.validation-api:3.0.2") 23 | implementation("org.hibernate.validator:hibernate-validator:8.0.1.Final") 24 | implementation("org.glassfish:jakarta.el:4.0.2") 25 | implementation("guru.nidi:graphviz-java:0.18.1") 26 | implementation("org.apache.logging.log4j:log4j-api:2.24.1") 27 | implementation("org.apache.logging.log4j:log4j-core:2.24.1") 28 | 29 | implementation("guru.nidi:graphviz-java:0.18.1") 30 | implementation("org.apache.xmlgraphics:batik-transcoder:1.17") 31 | implementation("org.apache.xmlgraphics:batik-codec:1.17") 32 | 33 | testRuntimeOnly("org.junit.platform:junit-platform-launcher") 34 | implementation("com.fasterxml.jackson.core:jackson-databind:2.15.2") 35 | 36 | // This dependency is used by the application. 37 | implementation(libs.guava) 38 | 39 | implementation("org.projectlombok:lombok:1.18.34") 40 | compileOnly("org.projectlombok:lombok:1.18.34") 41 | annotationProcessor("org.projectlombok:lombok:1.18.34") 42 | 43 | testImplementation("org.projectlombok:lombok:1.18.34") 44 | testAnnotationProcessor("org.projectlombok:lombok:1.18.34") 45 | testImplementation("org.mockito:mockito-core:5.13.0") 46 | testImplementation("net.jqwik:jqwik:1.9.3") 47 | 48 | 49 | } 50 | 51 | // Apply a specific Java toolchain to ease working on different environments. 52 | java { 53 | toolchain { 54 | languageVersion.set(JavaLanguageVersion.of(21)) 55 | } 56 | } 57 | 58 | 59 | 60 | application { 61 | // Define the main class for the application. 62 | mainClass.set("dop.App") 63 | } 64 | 65 | tasks.named("test") { 66 | // Use JUnit Platform for unit tests. 67 | useJUnitPlatform() 68 | 69 | testLogging { 70 | showStandardStreams = true 71 | } 72 | } 73 | 74 | 75 | tasks.withType().configureEach { 76 | //enable compilation in a separate daemon process 77 | options.encoding = "UTF-8" 78 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /app/src/test/java/dop/chapter06/the/implementation/Types.java: -------------------------------------------------------------------------------- 1 | package dop.chapter06.the.implementation; 2 | 3 | import dop.chapter05.the.existing.world.Entities; 4 | import dop.chapter05.the.existing.world.Services; 5 | import dop.chapter06.the.implementation.Types.Lifecycle.Billed; 6 | import dop.chapter06.the.implementation.Types.Lifecycle.Draft; 7 | import dop.chapter06.the.implementation.Types.Lifecycle.Rejected; 8 | import dop.chapter06.the.implementation.Types.Lifecycle.UnderReview; 9 | 10 | import java.math.BigDecimal; 11 | import java.time.LocalDate; 12 | import java.util.List; 13 | import java.util.Optional; 14 | import java.util.stream.Collector; 15 | import java.util.stream.Collectors; 16 | 17 | public class Types { 18 | 19 | public record USD(BigDecimal value) { 20 | public USD multiply(BigDecimal amount){ 21 | return new USD(value.multiply(amount)); 22 | } 23 | public static USD zero() {return new USD(BigDecimal.ZERO);} 24 | public static USD add(USD a, USD b) { return new USD(a.value().add(b.value()));} 25 | 26 | public static Collector summing() { 27 | return Collectors.reducing(USD.zero(), USD::add); 28 | } 29 | } 30 | 31 | public record Percent(double numerator, double denominator) { 32 | public Percent { 33 | if (numerator > denominator) { 34 | throw new IllegalArgumentException( 35 | "Percentages are 0..1 and must be expressed " + 36 | "as a proper fraction. e.g. 1/100"); 37 | } 38 | } 39 | BigDecimal decimalValue() { 40 | return BigDecimal.valueOf(numerator / denominator); 41 | } 42 | } 43 | public record CustomerId(String value){} 44 | public record PastDue(Entities.Invoice invoice) {} 45 | public record InvoiceId(String value){} 46 | public record Reason(String value){} 47 | public sealed interface Lifecycle { 48 | record Draft() implements Lifecycle{} 49 | record UnderReview(String id) implements Lifecycle {} 50 | record Billed(InvoiceId id) implements Lifecycle {} 51 | record Rejected(Reason why) implements Lifecycle {} 52 | } 53 | 54 | 55 | public record LateFee( 56 | State state, 57 | EnrichedCustomer customer, 58 | USD total, 59 | LocalDate invoiceDate, 60 | LocalDate dueDate, 61 | List includedInFee 62 | ){ 63 | public LateFee markBilled(InvoiceId id) { 64 | return new LateFee<>(new Billed(id), customer, total, invoiceDate, dueDate, includedInFee); 65 | } 66 | 67 | public LateFee markNotBilled(Reason reason) { 68 | return new LateFee<>(new Rejected(reason), customer, total, invoiceDate, dueDate, includedInFee); 69 | } 70 | 71 | public LateFee markAsBeingReviewed(String approvalId) { 72 | return new LateFee<>(new UnderReview(approvalId), customer, total, invoiceDate, dueDate, includedInFee); 73 | } 74 | 75 | public LateFee inState(A evidence) { 76 | return new LateFee<>(evidence, customer, total, invoiceDate, dueDate, includedInFee); 77 | } 78 | } 79 | 80 | 81 | public sealed interface ReviewedFee { 82 | record Billable(LateFee latefee) implements ReviewedFee {} 83 | record NeedsApproval(LateFee latefee) implements ReviewedFee {} 84 | record NotBillable(LateFee latefee, Reason reason) implements ReviewedFee {} 85 | } 86 | 87 | public record EnrichedCustomer( 88 | CustomerId id, 89 | Entities.Address address, 90 | Percent feePercentage, 91 | Services.ContractsAPI.PaymentTerms terms, 92 | Services.RatingsAPI.CustomerRating rating, 93 | Optional approval 94 | ) {} 95 | } 96 | -------------------------------------------------------------------------------- /app/src/test/java/dop/chapter05/the/existing/world/Entities.java: -------------------------------------------------------------------------------- 1 | package dop.chapter05.the.existing.world; 2 | 3 | import dop.chapter05.the.existing.world.Annotations.Entity; 4 | import dop.chapter05.the.existing.world.Annotations.Id; 5 | import dop.chapter05.the.existing.world.Annotations.ManyToMany; 6 | import dop.chapter05.the.existing.world.Annotations.OneToOne; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Data; 9 | import lombok.NoArgsConstructor; 10 | 11 | import javax.annotation.Nullable; 12 | import java.math.BigDecimal; 13 | import java.time.LocalDate; 14 | import java.util.Currency; 15 | import java.util.List; 16 | import java.util.UUID; 17 | 18 | /** 19 | * In chapter 5, we explore what data oriented programming looks 20 | * like inside a messy, real-world app with lots of prior decisions. 21 | * 22 | * These entities are some of those "prior decisions" we explore 23 | * in the book. They're meant to capture a familiar style of modeling 24 | * that we might find in frameworks like Spring or Hibernate. 25 | * 26 | * The chapter explores their implication, and how we can be data-oriented 27 | * even if the starting point is less than ideal. 28 | * 29 | * :::Implementation Note::: 30 | * Lombok is used throughout to avoid cluttering up everything 31 | * with manual getters / setters. 32 | */ 33 | public class Entities { 34 | 35 | 36 | @Entity 37 | @lombok.Getter 38 | @lombok.Setter 39 | public static class Invoice { 40 | @Id 41 | String invoiceId; 42 | String customerId; 43 | @Annotations.OneToMany 44 | List lineItems; 45 | InvoiceStatus status; 46 | LocalDate invoiceDate; 47 | LocalDate dueDate; 48 | InvoiceType invoiceType; // ◄───┐ When this is set to LATEFEE, it's expected 49 | @Nullable // │ that the audit info is populated. 50 | AuditInfo auditInfo; // ◄───┘ Otherwise, it should be null. 51 | 52 | public static String tempId() { 53 | return "TEMP_ID::" + UUID.randomUUID(); 54 | } 55 | 56 | public static boolean isTempId(String invoiceId) { 57 | return invoiceId.contains("TEMP_ID::"); 58 | } 59 | } 60 | 61 | 62 | public enum InvoiceType {LATEFEE, STANDARD}; 63 | 64 | 65 | // Invoices have a very simple lifecycle on the surface. 66 | // They start as OPEN, then become CLOSED once paid. 67 | public enum InvoiceStatus {OPEN, CLOSED} 68 | 69 | @Entity 70 | @Data 71 | @AllArgsConstructor 72 | public static class LineItem { 73 | @Id 74 | @Nullable 75 | String id; 76 | String description; 77 | BigDecimal charges; // ◄──┐ Keeping with the theme of "modeling based on whatever 78 | Currency currency; // │ the ORM makes easiest, the Entities use built in 79 | } // │ data types rather than more data-oriented ones (like USD or Money) 80 | 81 | 82 | 83 | @Entity 84 | @Data 85 | @AllArgsConstructor 86 | public static class AuditInfo { 87 | @Id 88 | @OneToOne 89 | String invoiceId; 90 | @ManyToMany 91 | List includedInFee; 92 | @Nullable 93 | String reason; // ◄───┐ This will only be defined when a Fee cannot 94 | // │ be billed for some reason. 95 | } 96 | 97 | @Entity 98 | @lombok.Getter 99 | @lombok.Setter 100 | @AllArgsConstructor 101 | @NoArgsConstructor 102 | public static class Customer { 103 | @Id 104 | String id; 105 | Address address; 106 | @Nullable 107 | String approvalId; // ◄────┐ Approvals are managed in another system, but 108 | } // we track that state on the customer Entity. 109 | // Doing so is "free" with our magical ORM. 110 | 111 | 112 | @lombok.Getter 113 | public static class Address { 114 | String line1; 115 | String city; 116 | String country; 117 | // and so on 118 | } 119 | 120 | // Below here are the "minor" entities in our scenario. 121 | // They're things that are Entities only in that they're 122 | // saved and loaded from the (pretend) database and thus 123 | // designed around the ORM's convenience. 124 | @Data 125 | public static class Rules { 126 | BigDecimal minimumFeeThreshold; 127 | BigDecimal maximumFeeThreshold; 128 | 129 | } 130 | 131 | 132 | 133 | 134 | } 135 | -------------------------------------------------------------------------------- /app/src/test/java/dop/chapter06/the/implementation/Core.java: -------------------------------------------------------------------------------- 1 | package dop.chapter06.the.implementation; 2 | 3 | import dop.chapter05.the.existing.world.Entities.*; 4 | import dop.chapter05.the.existing.world.Services.ApprovalsAPI.ApprovalStatus; 5 | import dop.chapter05.the.existing.world.Services.ContractsAPI.PaymentTerms; 6 | import dop.chapter05.the.existing.world.Services.RatingsAPI.CustomerRating; 7 | import dop.chapter06.the.implementation.Types.*; 8 | import dop.chapter06.the.implementation.Types.Lifecycle.Draft; 9 | import dop.chapter06.the.implementation.Types.ReviewedFee.Billable; 10 | import dop.chapter06.the.implementation.Types.ReviewedFee.NeedsApproval; 11 | import dop.chapter06.the.implementation.Types.ReviewedFee.NotBillable; 12 | 13 | import java.time.LocalDate; 14 | import java.time.temporal.TemporalAdjuster; 15 | import java.time.temporal.TemporalAdjusters; 16 | import java.util.List; 17 | 18 | import static dop.chapter05.the.existing.world.Entities.InvoiceStatus.OPEN; 19 | import static dop.chapter05.the.existing.world.Entities.InvoiceType.STANDARD; 20 | import static java.time.temporal.ChronoUnit.DAYS; 21 | 22 | public class Core { 23 | 24 | 25 | static LateFee buildDraft(LocalDate today, EnrichedCustomer customer, List invoices) { 26 | return new LateFee<>( 27 | new Draft(), 28 | customer, 29 | computeFee(invoices, customer.feePercentage()), 30 | today, 31 | dueDate(today, customer.terms()), 32 | invoices 33 | ); 34 | } 35 | 36 | static USD computeFee(List pastDue, Percent percentage) { 37 | return computeTotal(pastDue).multiply(percentage.decimalValue()); 38 | } 39 | 40 | 41 | static USD computeTotal(List invoices) { 42 | return invoices.stream().map(PastDue::invoice) 43 | .flatMap(x -> x.getLineItems().stream()) 44 | .map(Core::unsafeGetChargesInUSD) 45 | .reduce(USD.zero(), USD::add); 46 | } 47 | 48 | static USD unsafeGetChargesInUSD(LineItem lineItem) throws IllegalArgumentException { 49 | if (!lineItem.getCurrency().getCurrencyCode().equals("USD")) { 50 | throw new IllegalArgumentException("Kaboom"); 51 | } else { 52 | return new USD(lineItem.getCharges()); 53 | } 54 | } 55 | 56 | static LocalDate dueDate(LocalDate today, PaymentTerms terms) { 57 | return switch (terms) { 58 | case PaymentTerms.NET_30 -> today.plusDays(30); 59 | case PaymentTerms.NET_60 -> today.plusDays(60); 60 | case PaymentTerms.DUE_ON_RECEIPT -> today; 61 | case PaymentTerms.END_OF_MONTH -> today.with(TemporalAdjusters.lastDayOfMonth()); 62 | }; 63 | } 64 | 65 | public static ReviewedFee assessDraft(Rules rules, LateFee draft) { 66 | return switch (assessTotal(rules, draft.total())) { 67 | case Assessment.WITHIN_RANGE -> new Billable(draft); 68 | case Assessment.BELOW_MINIMUM -> new NotBillable(draft, new Reason("Below threshold")); 69 | case Assessment.ABOVE_MAXIMUM -> draft.customer().approval().isEmpty() 70 | ? new NeedsApproval(draft) 71 | : switch (draft.customer().approval().get().status()) { 72 | case ApprovalStatus.APPROVED -> new Billable(draft); 73 | case ApprovalStatus.PENDING -> new NotBillable(draft, new Reason("Pending decision")); 74 | case ApprovalStatus.DENIED -> new NotBillable(draft, new Reason("exempt from large fees")); 75 | }; 76 | }; 77 | } 78 | 79 | 80 | private enum Assessment {ABOVE_MAXIMUM, BELOW_MINIMUM, WITHIN_RANGE} 81 | 82 | static Assessment assessTotal(Rules rules, USD total) { 83 | if (total.value().compareTo(rules.getMinimumFeeThreshold()) < 0) { 84 | return Assessment.BELOW_MINIMUM; 85 | } else if (total.value().compareTo(rules.getMaximumFeeThreshold()) > 0) { 86 | return Assessment.ABOVE_MAXIMUM; 87 | } else { 88 | return Assessment.WITHIN_RANGE; 89 | } 90 | } 91 | 92 | public static List collectPastDue(EnrichedCustomer customer, LocalDate today, List invoices) { 93 | return invoices.stream() 94 | .filter(invoice -> isPastDue(invoice, customer.rating(), today)) 95 | .map(PastDue::new) // Capturing what we know as this new type (and quarantining its mutability) 96 | // we're now "out" of the existing world and into ours. 97 | .toList(); 98 | } 99 | 100 | static boolean isPastDue(Invoice invoice, CustomerRating rating, LocalDate today) { 101 | return invoice.getInvoiceType().equals(STANDARD) 102 | && invoice.getStatus().equals(OPEN) 103 | && today.isAfter(invoice.getDueDate().with(gracePeriod(rating))); 104 | } 105 | 106 | static TemporalAdjuster gracePeriod(CustomerRating rating) { 107 | return switch (rating) { 108 | case CustomerRating.GOOD -> date -> date.plus(60, DAYS); 109 | case CustomerRating.ACCEPTABLE -> date -> date.plus(30, DAYS); 110 | case CustomerRating.POOR -> TemporalAdjusters.lastDayOfMonth(); 111 | }; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /app/src/test/java/dop/chapter06/the/implementation/Service.java: -------------------------------------------------------------------------------- 1 | package dop.chapter06.the.implementation; 2 | 3 | import dop.chapter05.the.existing.world.Annotations.Transaction; 4 | import dop.chapter05.the.existing.world.Entities; 5 | import dop.chapter05.the.existing.world.Repositories; 6 | import dop.chapter05.the.existing.world.Services; 7 | import dop.chapter06.the.implementation.Types.*; 8 | import dop.chapter06.the.implementation.Types.Lifecycle.Billed; 9 | import dop.chapter06.the.implementation.Types.Lifecycle.Draft; 10 | import dop.chapter06.the.implementation.Types.Lifecycle.UnderReview; 11 | import dop.chapter06.the.implementation.Types.ReviewedFee.Billable; 12 | import dop.chapter06.the.implementation.Types.ReviewedFee.NeedsApproval; 13 | import dop.chapter06.the.implementation.Types.ReviewedFee.NotBillable; 14 | 15 | import java.time.LocalDate; 16 | import java.util.Currency; 17 | import java.util.List; 18 | import java.util.stream.Stream; 19 | 20 | public class Service { 21 | private Services.ApprovalsAPI approvalsApi; 22 | private Services.BillingAPI billingApi; 23 | private Repositories.CustomerRepo customerRepo; 24 | private Repositories.InvoiceRepo invoiceRepo; 25 | private Facade facade; 26 | private Repositories.RulesRepo rulesRepo; 27 | 28 | record InvoicingData( 29 | LocalDate currentDate, 30 | Types.EnrichedCustomer customer, 31 | List invoices, 32 | Entities.Rules rules 33 | ){} 34 | 35 | public void processLatefees() { 36 | // ---- NON-DETERMINISTIC SHELL -------- 37 | loadInvoicingData().forEach(data -> { 38 | // ---- DETERMINISTIC CORE -------- 39 | LocalDate today = data.currentDate(); 40 | Types.EnrichedCustomer customer = data.customer(); 41 | List pastDue = Core.collectPastDue(customer, today, data.invoices()); 42 | LateFee draft = Core.buildDraft(today, customer, pastDue); 43 | ReviewedFee reviewed = Core.assessDraft(data.rules(), draft); 44 | // ---- NON-DETERMINISTIC SHELL -------- 45 | LateFee latefee = switch (reviewed) { 46 | case Billable b -> this.submitBill(b); 47 | case NeedsApproval a -> this.startApproval(a); 48 | case NotBillable nb -> nb.latefee().markNotBilled(nb.reason()); 49 | }; 50 | this.save(latefee); 51 | }); 52 | } 53 | 54 | public LateFee submitBill(Billable billable) { 55 | Services.BillingAPI.BillingResponse response = this.billingApi.submit( 56 | new Services.BillingAPI.SubmitInvoiceRequest(/*ignored*/)); 57 | return switch (response.status()) { 58 | case ACCEPTED -> billable.latefee().markBilled(new InvoiceId(response.invoiceId())); 59 | case REJECTED -> billable.latefee().markNotBilled(new Reason(response.error())); 60 | }; 61 | } 62 | 63 | public LateFee startApproval(NeedsApproval needsApproval) { 64 | Services.ApprovalsAPI.Approval approval = this.approvalsApi.createApproval( 65 | new Services.ApprovalsAPI.CreateApprovalRequest(/*ignored*/)); 66 | return needsApproval.latefee().markAsBeingReviewed(approval.id()); 67 | } 68 | 69 | 70 | @Transaction 71 | private void save(LateFee latefee) { 72 | invoiceRepo.save(toInvoice(latefee)); 73 | if (latefee.state() instanceof UnderReview review) { 74 | customerRepo.save(toCustomer(latefee.inState(review))); 75 | } 76 | } 77 | 78 | 79 | Stream loadInvoicingData() { 80 | LocalDate today = LocalDate.now(); 81 | Entities.Rules rules = rulesRepo.loadDefaults(); 82 | return facade.findAll().map(customer -> 83 | new InvoicingData( 84 | today, 85 | customer, 86 | invoiceRepo.findInvoices(customer.id().value()), 87 | rules 88 | ) 89 | ); 90 | } 91 | 92 | /** 93 | * Here's the exchange between our two worlds. 94 | * We have to convert from our domain specific "Late Fee" back 95 | * to the OMR friendly Invoice. 96 | */ 97 | static Entities.Invoice toInvoice(LateFee latefee) { 98 | Entities.Invoice invoice = new Entities.Invoice(); 99 | invoice.setInvoiceId(switch (latefee.state()) { 100 | case Billed(var id) -> id.value(); 101 | default -> Entities.Invoice.tempId(); 102 | }); 103 | invoice.setCustomerId(latefee.customer().id().value()); 104 | invoice.setLineItems(List.of(new Entities.LineItem( 105 | null, 106 | "Late Fee", 107 | latefee.total().value(), 108 | Currency.getInstance("USD") 109 | ))); 110 | invoice.setStatus(Entities.InvoiceStatus.OPEN); 111 | invoice.setInvoiceDate(latefee.invoiceDate()); 112 | invoice.setDueDate(latefee.dueDate()); 113 | invoice.setInvoiceType(Entities.InvoiceType.LATEFEE); 114 | invoice.setAuditInfo(new Entities.AuditInfo( 115 | null, 116 | latefee.includedInFee().stream().map(PastDue::invoice).toList(), 117 | switch(latefee.state()) { 118 | case Lifecycle.Rejected(var why) -> why.value(); 119 | case UnderReview(String approvalId) -> "..."; 120 | default -> null; 121 | } 122 | )); 123 | return invoice; 124 | } 125 | 126 | Entities.Customer toCustomer(LateFee 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 | --------------------------------------------------------------------------------