Graduate Program KB

Supple Design

The code is constantly changing, and needs to be refactored with new model insights. It needs to be designed for developers to be able to work on it. If developers aren't confident about predicting the implications of a computation they will duplicate it by writing their own. Duplication is also forced by large monolithic parts that can't be recombined. On the other hand, a pile of small parts is hard to keep track of.

Once you make all the implicit concepts in your domain explicit you have the raw material - but you still need to combine it.

Two roles that need to be served by the design

  1. Application layer usage code
  2. Other domain code

The code must be designed, both so that the client can flexibly use a small set of concepts for a range of scenarios, and so that it can be easily developed. The design must be easy to understand - revealing the same model the client uses. Effects of the code must be obvious.

Intention revealing interfaces

Code that expresses a rule without state it makes us derive it ourselves from a step by step procedure. Need a clear connection to the model to understand the effects of the code. If the client can't understand what the code will do from reading the interface, they need to read the implementation and value of encapsulation is lost.

  • Program elements need to be given names that reflect the concept they represent.
  • Classes and operations should be named to describe their effect and purpose without referencing how they work.
  • Names should come from the ubiquitous language.
  • Tests should be written before the implementation - think like the client.
  • All complexity should be encapsulated behind abstract interfaces that describe intentions.

Refactoring: A Paint-Mixing Application

A program for paint stores which shows customers the result of mixing paints. The initial design has a single Paint class:

paint class

public void paint(Paint paint) {
  v = v + paint.getV() // After mixing volume is summed
  // Colour mixing logic, ends with new results for r,b,y values
}

Write an exploratory test

public void testPaint() {
Paint yellow = new Paint(100.0, 0, 50, 0);
Paint blue = new Paint(100.0, 0, 0, 50);

yellow.paint(blue);

assertEquals(200.0, yellow.getV(), 0.01);
assertEquals(25, yellow.getB());
assertEquals(25, yellow.getY());
assertEquals(0, yellow.getR());
}

Now we can look at the interface like a user. The code doesn't tell us what it's doing, we can write the interface we want

public void testPaint() {
Paint yellow = new Paint(100.0, 0, 50, 0);
Paint blue = new Paint(100.0, 0, 0, 50);

yellow.mixIn(blue);

assertEquals(200.0, yellow.getVolume(), 0.01);
assertEquals(25, yellow.getBlue());
assertEquals(25, yellow.getYellow());
assertEquals(0, yellow.getRed());
}

paint class with better names

Side effect free functions

There are two general kinds of operations

  1. Commands - modifiers, change the system
  2. Queries - obtain information

A side effect in any effect on the state of the system - here he narrows it down to only changes that will affect future operations

Most operations call other operations which in turn call more. The client developer may not anticipate or intend changes from further down the chain.

Interactions of multiple rules of compositions of calculations become difficult to predict. Without being able to predict consequences the developer is limited in what they can combine.

Operations that return results without side effects are called functions (in contrast to procedures). Functions can call other functions without worrying about the effects of nesting.

  • Separate queries from commands
  • Methods that cause changes should not return domain data
  • All queries and calculations should be in methods without side effects
  • There are often alternative designs that do not require existing objects to be modified
    • Can create and return a new value object, unlike entities with life cycles
    • Value objects are also immutable with implies that all their operations are functions

Refactoring the paint mixing application again

public void mixIn(Paint other) {
  volume = volume.plus(other.getVolume());
  // Mixing logic
}

Design does follow the rule of separating modification from querying. But why does the volume of the paint increase in mixIn without the other decreasing? This doesn't seem logical.


First we should factor out our color into a value object to encapsulate the calculation. We could call it colour, but from knowledge crunching we know that it's different for paint than RGB lights - call it PigmentColor

paint class with color factored out

public class PigmentColor {
  public PigmentColor mixedWith(PigmentColor other, double ratio) {
    // color mixing logic
  }
}

public class Paint {
  public void mixIn(Paint other) {
    volume = volume + other.getVolume();
    double ratio = other.getVolume / volume;
    pigmentColor = pigmentColor.mixedWith(other.pigmentColor(), ratio)
  }
}

All the complex mixing logic is now a side effect free function, but we still have the question of the volume

Assertions

Side effect free functions help with the queries, but we still have to have some functions which modify state.

Two classes that implement the same interface can have different side effects. When side effects are implicit the only way to understand a program is to trace the execution through branching paths in the implementation.

Intention revealing interfaces help, but only partly solve the problem. Design by contract is the next step, creating assertions about classes which the developer can assume are true.

  • Post-conditions describe the side effects of an operation.
  • Pre-conditions are conditions that must be satisfied for the post-conditions to hold.
  • Class invariants make assertions about the state of the object after any operation.
    • Invariants can also be used for aggregates.
  • Effects of delegations should be covered by the assertions.

  • State post conditions of operations and invariants of classes and aggregates.
  • Write automated unit tests for them.
  • Write them into documentation or diagrams.
  • Create models that lead the developer to infer the intended assertions.

Paint mixing

Our general understanding of physics would indicate that increasing the volume of a paint, should decrease the volume of the other paint. We can start by stating the current post-condition


After p1.mixIn(p2):  p1.volume is increased by amount of p2.volume  p2.volume is unchanged


Problem is these concepts don't match how developers would think about paint. We could modify the argument (p2), but changing an argument is a particularly risky side effect.

But why was it written this way?

At the end of the program it reposts the list of paints that were added. The purpose of the application is to figure out how much of the paint to put in the mixture. If we set p2's volume to 0 that information is gone.

Awkwardness usually comes from missing concepts - try separating the two responsibilities we are giving Paint.

paint class with multiple responsibilities factored out

public void testMixingVolume() {
  PigmentColor yellow = new PigmentColor(0, 50, 0);
  PigmentColor blue = new PigmentColor(0, 0, 50);

  StockPaint paint1 = new StockPaint(1.0, yellow);
  StockPaint paint2 = new StockPaint(1.5, blue);
  MixedPaint mix = new MixedPaint()

  mix.mixIn(paint1);
  mix.mixIn(paint2);
  assertEquals(2.5, mix.getVolume(), 0.01)
}

Conceptual contours

When elements of a model or design are embedded in a monolithic construct the functionality gets duplicated. The interface doesn't cover all use cases. They are hard to understand because concepts are mixed together.

Breaking down classes and methods can complicate the client - forcing it to combine a bunch of small pieces for simple behaviour. Concepts that should be in the domain could be lost, or reimplemented in the application layer.


There is a logical consistency in most domains, or they wouldn't even work outside the code. If we find a model that works well for one part of the domain, it's likely to work well for others.

Sometimes it's not easy to adapt to the new change in which case we refactor towards deeper insight.

Conceptual contours emerge as the code is adapted.


High cohesion and low coupling apply to concepts as well as code.

  • Each object should ve a single complete concept.
  • Break data and procedures at conceptually meaningful points, use your intuition and think about the domain.
  • Observe the areas of change and stability through multiple refactorings and look for the conceptual contours that explain where everything is split.
  • When successive refactors are localised instead of affecting the entire code base it's an indicator of a good model.
    • Requiring extensive changes is a sign that the model needs refining

Accruals

In Chapter 9 a loan tracking system was refactored through deeper insight.

  • Schedules which were hidden in the calculator classes were split into discrete classes for fees and interest.
  • Payments which were separate for fees and interest which were separate are now lumped together.

Accruals


Accruals refactored

The change the developers can predict are new Accrual Schedules - a model was chosen to make it easy to add new schedules.

But is this a conceptual contour?

There are no guarantees about an unanticipated change, but developers think it improves the odds.

Accrual schedule

Unanticipated change

New requirement details rules for early and late payments.

Virtually the same rules applied to payments on interest and payments on fees.

In the new model elements would connect neatly to the same payment class.

In the old model it would need to be duplicated over the fee and interest calculators.

Payment policy

Standalone Classes

  • Interdependencies make models and design hard to understand
  • Every association is a dependency, to understand a class you will need to understand what it is attached to
  • E.g.) With 1 dependency you have to think about 2 classes at the same time, and also any dependencies those classes may have
  • Modules and Aggregates try to reduce the number of interdependencies
  • Even with modules, the more dependencies are added, so will the mental load for interpreting the design
  • Refined models are distilled so that connections between concepts are fundamentals
  • Additional concepts that have to be considered to understand an object contributes to mental overload
  • Every dependency is a suspect of contributing to mental load unless proven
  • Model and design choices can reduce the number of dependencies, often to 0
  • Eliminating all other concepts not relating to the class creates a self contained class
  • Every self contained class eases the burden of understanding a module
  • Not getting rid of all dependencies but all non-essential dependencies
  • E.g.) Paint is related to colour, but colour and pigment can be used without paint
  • Low coupling reduces conceptual overload, and Standalone Classes is an example of low coupling

Closure of Operations

  • Can't remove 100% of dependencies, which isn't a bad thing
  • Use operations whose return type is the same type as the arguments, so that the operation is closed under the set of these types
  • Closed operation provides a high level interface without introducing dependency on other concepts
  • Most used with value objects because Entities life cycle plays a significant role

Declarative Design

  • Many motivations for declarative design and indicates a way to write a program as an executable specification
  • Declaration language is not expressive enough to do everything needed
  • Merging generated code with handwritten code cripples the iterative cycle

Domain Specific Languages

  • Sometimes a declarative approach
  • Client code is written to tailor a particular model of a Domain
  • E.g.) Shipping systems language might include terms like cargo and route
    • Program contains library of classes that provide implementations for those terms
  • Programs can be expressive and is a strong connection with the Ubiquitous Language
  • However there are some drawbacks with domain specific languages
    • Developers need to modify grammar declarations, language interpreting features and any class libraries to refine the model
    • It is good to learn advanced technology and concepts, but will also need to consider the skills of the team and any future teams
    • Difficult to refactor client code to match a revised model and its domain language

Declarative Style of Design

  • When the design contains techniques; intention revealing interfaces, side effect free functions and assertions. The design becomes more declarative.
  • A supple design can make it possible for the client code to use a declarative style of design

Extending Specifications in a Declarative style

  • Specification is an adaptation of Predicates
Combining Specifications using logical operators
  • Since specifications are an example of predicates, predicates can be combined and used with the operations 'AND', 'OR', and 'NOT'.
  • These operations are closed under predicates, specifications follows the closure of operations
  • Useful to create an abstract class or interface that can be used for Specifications of all types
public interface Specification
{
    boolean isSatisfiedBy(Object candidate);
}
  • This abstraction uses a guard clause which doesn't affect the functionality

  • E.g.) Container Specification from Chapter 9

public class ContainerSpecification implements Specification{
    private ContainerFeature requiredFeature;

    public ContainerSpecification(ContainerFeature required){
        requiredFeature = required;
    }

    boolean isSatisfiedBy(Object candidate){
        if(!candidate instanceof Container) return false;

        return (Container)candidate.getFeatures().contains(requiredFeature);
    }
}

public interface Specification
{
    boolean isSatisfiedBy(Object candidate);

    Specification and(Specification other);
    Specification or(Specification other);
    Specification not();
}
  • Some container specifications required ventilated containers and some armoured containers, and some needed both. This can be done by adding new methods below
    Specification ventilated = new ContainerSpecification(VENTILATED)
    Specification armoured = new ContainerSpecification(ARMOURED)
    Specification both = ventilated.and(armoured)

    // Cheap container with no special features
    Specification cheap = (ventilated.not()).and(armoured.not())
  • You can build complex specifications out of simple elements which makes the code more expressive.
  • The combinations are also written in a declarative style
  • Like any other pattern, there are many ways to implement specifications
  • Below is an example of a specification, and there could be many situations that this specification is not suited for
// composite design of specification
public abstract class AbstractSpecification implements Specification {

    public Specification and(Specification other){
        return new AndSpecification(this,other);
    }

    public Specification or(Specification other) {
        return new OrSpecification(this,other)
    }

    public Specification not() {
        return new NotSpecification(this)
    }
}

public class AndSpecification extends AbstractSpecification {
    Specification one;
    Specification other;

    public AndSpecification(Specification x, Specification y){
        one = x;
        other = y;
    }

    public boolean isSatisfiedBy(Object candidate){
        return one.isSatisfiedBy(candidate) && other.isSatisfiedBy(candidate)
    }

    public class OrSpecification extends AbstractSpecification{
        Specification one;
        Specification other;

        public OrSpecification(Specification x, Specification y){
            one = x;
            other = y;
        }

        public boolean isSatisfiedBy(Object candidate){
            return one.isSatisfiedBy(candidate) || other.isSatisfiedBy(candidate)
        }
    }

    public class NotSpecification extends AbstractSpecification {
        Specification wrapped;

        public NotSpecification(Specification x){
            wrapped = x
        }

        public boolean isSatisfiedBy(Object candidate){
            return !wrapped.isSatisfiedBy(candidate)
        }
    }
}
  • Importance is having a model capture the key concepts of a domain
  • This specification example may be overkill for many cases, you might only just need the 'AND'
  • Even though this specification is simple, it may not be pratical in different situations
    public class MinimumAgeSpecification {
        int threshhold;

        public boolean isSatisfiedBy(Person candidate){
            return candidate.getAge() >= threshhold;
        }

        public boolean subsumes(MinimumAgeSpecification other) {
            return threshold >= other.getThreshold();
        }
    }
  • This is to check if the new spec meets all the conditions of the old one
  • New specification subsumes the old specification. Meaning that a candidate that satisfy the new specification should satisfy the old specification

Angles of Attack

  • Difficult to take a huge system and make it supple
  • Have to break it down into smaller pieces and make those supple first
  • Couple of approaches:
    • Carving off subdomains
    • Seperating complex tasks out into another module
    • Better to make a big impact in one area, rather than making a small impact in multiple areas
    • Drawing on established formalisms
    • Can use established concepts in the domain to create a tighter framework
    • E.g.) Accounting has many established concepts that you can use to create a deep model
    • Many times maths can be pulled out into a seperate module