cult3

6 Principles for Writing Maintainable Code

Dec 03, 2014

Table of contents:

  1. 1. Intention-Revealing Interfaces
  2. 2. Side-Effect-Free Functions
  3. 3. Assertions
  4. 4. Conceptual Contours
  5. 5. Standalone Classes
  6. 6. Closure of Operations
  7. Conclusion

One of the most beautiful things in software development is starting from a clean slate. I’m sure we’ve all worked on projects that are riddled with legacy code that is scary to touch. Starting from a clean slate is like a breath of fresh air.

All software development projects start with the best of intentions. No one sets out to write code that is scary to work with, difficult to understand or impossible to change.

However always with the best of intentions, even the most diligent developer can fall into the trap of writing code that is difficult to maintain.

There are many teachings in the world of software development that all focus on preventing us from falling into this trap. A lot of them even have easy to remember acronyms (DRY, SOLID, YAGNI).

With all of these patterns, principles and philosophies, you would think that as an industry we would have cracked this problem. In reality, we are all still writing crappy code today that we will have to maintain tomorrow.

In Domain-driven Design: Tackling Complexity in the Heart of Software, Eric Evans highlights a number of patterns and techniques to help to prevent you from falling into this trap.

One of the most important aspects of Domain Driven Development is the ability to constantly refactor your code through an iterative process of discovery. Writing rigid code that is not able to evolve will grind this process of discovery to a halt.

In today’s article we will be looking at the patterns from the book, and how to use them in your day-to-day projects.

1. Intention-Revealing Interfaces

An important lesson from Domain Driven Design is the encapsulation of Domain logic in objects. The business rules of the organisation should be enforced through objects that protect the invariants of the concept they are modelling.

The process of encapsulating your application’s business rules involves truly understanding the concept and how the business expects it to work at a fundamental level. Once this knowledge is encapsulated as an object, the outside world should not need to worry about the internal implementation of those business rules.

When working with Domain Objects, you shouldn’t need to know how the object is implemented, the interface should tell you everything you need to know. Even if you are the person who wrote that code, you shouldn’t be concerned with it’s implementation when you are consuming it in another part of the application.

However, if the interface is not explicit, you will need to start wading through the implementation of the object to understand how it works and how it should be used. If you need to open up a class and do this, the benefit of the encapsulation is lost.

This can lead to even bigger problems when you are using objects that have been written by another developer. If the interface is not explicit it can lead to objects being used for the wrong purposes or in the wrong ways. Whilst this might work to begin with, this misunderstanding is likely to surface later as fundamental design problems.

In order to avoid this problem, you should give your objects and methods names that truly reflect the concept they are modelling. By naming classes that infer their purpose and effect, you are writing an Intention-Revealing Interface.

The words you use to describe the objects and methods of your application should be directly derived from the Ubiquitous Language. Any nontechnical member of the team should be able to understand the object, it’s role and purpose within the application from it’s name and methods.

A good way to design an object is to start with it’s interface without worrying too much about the implementation. By starting from the outside and working inwards, you will end up with an object interface that is explicit as to the intention of the object. Also, writing the test before the implementation will force you think of the object as a consumer.

For example we might have the following File class and save() method:

// Create a new File
$file = new File();

// Save it!
$file->save();

As a consumer of this class, we shouldn’t need to know how the file is actually saved, or where it is saved to. The save() method is explicit as to what is going on so we don’t have to read through the source code to understand it.

2. Side-Effect-Free Functions

A side-effect is the consequence of an action that was not intended. When you call a method on an object, you should expect a certain outcome and nothing more. If that method invokes another method which results in something you weren’t expecting, we can say that the method has unintentional consequences.

When working with code you are usually either asking a question, or calling a command.

The result of asking a question should be an answer that has no side-effects. Calling a command should have a single outcome that is predictable due to the object’s Intention-Revealing Interface.

One way of mitigating against side-effects is through using Value Objects, rather than Entities. The benefit of using Value Objects over Entities is that a Value Object is immutable, whereas an Entity has a lifecycle. This means if the value of a Value Object changes, the whole object should be destroyed and replaced with a new object. For more on this, read What is the difference between Entities and Value Objects?.

Therefore, write methods that return results with no side-effects. Separate queries from commands and try to move Domain logic into immutable Value Objects. Try to write Side-Effect-Free Functions that are easy to understand, test and combine with other Side-Effect-Free Functions.

For example we might be working with the concept of Money inside an Ecommerce application. Money is the classic example of a Value Object. This means working with Money objects is very save because the object is immutable, and therefore, side-effect free:

// Add two Money values together
$one = new Money(1000, new Currency("USD"));
$two = new Money(500, new Currency("USD"));

$total = $one->add($two);

When we add $one and $two together a new $total object is created because Value Objects are immutable.

3. Assertions

Using Side-Effect-Free Functions ensures that invoking a command does not set off an unpredictable chain of events with unintended consequences. But all command will have a consequences, and so it is important to understand these consequences, particularly when it comes to Entities.

When using an object that implements an Intention-Revealing Interface, the developer should have a good idea of the purpose of the object and the method that is invoked.

However, two concrete implementations of an interface can have two different consequences of invoking the same method!

We need a way of determining the consequences of calling a method without having to understand the internal implementation of the object. When you trust the outcome of a method, you don’t need to be concerned with how it actually works.

Assertions are therefore important to assert the outcome of calling any particular method. Usually this is achieved through writing Unit Tests that describe pre and post conditions of calling a method and asserting that the correct outcome was returned.

A typical unit test will usually set up an expectation and then assert that the expectation is correct:

// Assert that a user is activated
public function should_active_user()
{
    $user = new User;
    $user->activate();

    $this->assertTrue($user->isActive());
}

4. Conceptual Contours

We all know that you shouldn’t lump too much responsibility into a single object or method. When an object or method assumes too much responsibility you end up with duplication, unpredictability and code that is difficult to maintain.

On the other hand, splitting code up into individual atomic units can also be troublesome. Whilst composition is usually a good thing, if you split a specific concept into individual units, it can be difficult to reassemble the parts to achieve the outcome you are looking for.

I’m sure we’ve all been in the situation where an object that we have written is becoming unwieldy due to the ever-changing requirements of the organisation we are building for. That beautiful object you written has suddenly become awkward now that it has to work in a different way.

Instead of fighting against this change, we should evolve our code to better fit the revealing model. By writing code that is consistent with the Domain Model our code will be better suited to the current and future problems we need to tackle. As more of the Domain Model is revealed, we should endeavour to ensure our code remains consistent in order to be better positioned for future discoveries.

Instead of trying to break down every object and method into atomic units, group functionality together that is meaningful to the Domain. Create whole values that encapsulate concepts of the Domain rather than breaking everything up into it’s smallest unit to be composed again later. If the smallest unit of a concept is not important to the Domain, it does not need to be a separate object.

This should result in interfaces and objects that are meaningful to the business and that can be combined to satisfy important concepts of the Domain.

You will only reach this state of enlightenment through a process of discovery and iterative refectoring as you uncover the intricacies of the domain you are modelling.

For example, perhaps date periods are important to your financial planning application. Instead of working with individual dates, you should define a Period object that is meaningful to the business:

// Create a new Period
$period = new Period("first day of 2014", "last day of 2014");

In this example we don’t have about the individual Date objects, we only care about the Period. We therefore don’t need the complexity of working with the individual objects.

5. Standalone Classes

When an object has a dependency of another object, you need to understand both objects and their relationship in order to fully understand the first object.

If the object that is a dependency also has dependencies, you need to understand the relationships between those nested dependencies just to understand that initial object.

This can quickly spiral out of control and make simple concepts too much for any single person to get their head around.

As the number of dependencies increases, so to does the complexity of the design.

Instead of allowing your classes to spiral out of control, you should attempt to distil the relationships between objects so they represent meaningful concepts to the Domain you are modelling.

Standalone Classes are ideal because they are not coupled to any other code and they can be understood in isolation. However self-contained classes are not always an achievable target.

You probably can’t limit all dependencies, but you can limit the non-essential ones. Look to only include dependencies that clarify the nature of the relationship between the two objects and model an important concept from the Domain.

Value Objects are a good choice for modelling dependencies because they are immutable. Modelling a concept as a Value Object instead of primitive strings and integers can often add clarity to an important concept.

For example, we might have a Customer that has a dependency of an Address. We can model the Address as a Value Object to encapsulate the contact details of the Customer. By using an immutable Value Object, we don’t need to be conceded with nested dependencies:

// Create an Address
$address = new Address("123 Sesame Street");

// Pass the Address as a dependency
$customer = new Customer("Mr. Snuffleupagus", $address);

6. Closure of Operations

In mathematics, a closed operation is where an operation on a member of a set always returns another member of the set. For example, 1 + 1 = 2. Both 1 and 2 are both numbers, and so by adding two numbers together, you are returned a number.

Methods that return the same type of output that it accepts as the input are often easier to understand and more predictable in usage. When you know that you will receive the same type output as the input you give an operation, you don’t need to worry about the implementation of the operation or introducing of any new concepts to the class.

A good example of this is when working with Collections. When you mutate a Collection, you should be returned a new Collection containing the mutated items:

$collection = new Collection(["Homer", "Marge", "Bart", "Lisa", "Maggie"]);

// Add the family name to each item:
$family = $collection->map(function ($person) {
    return "$person Simpson";
});

$family; // Collection
$family->get(0); // 'Homer Simpson'
$family->get(1); // 'Marge Simpson'
$family->get(2); // 'Bart Simpson'
$family->get(3); // 'Lisa Simpson'
$family->get(4); // 'Maggie Simpson'

In the example above, we map over each item in the Collection and we are returned a new Collection of the modified items.

Conclusion

The six principles that I’ve explained in this article are just some of the many good patterns and practices that are important to writing supple and maintainable software.

I think we’re all guilty of writing crappy code from time to time. It’s only human to take shortcuts or make unwise decisions when your back is against the wall.

However by learning good practices and techniques, you will find that the overall standard of your code will dramatically increase. When you are under pressure to ship, or the requirements have changed for the millionth time, you will have a foundation of good practices to fall back on.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.