Everything should be made as simple as possible, but not simpler.

 Albert Einstein

The second component of the FLUID methodology is lean implementation: to tailor the code on the size of the problem to be solved.

In the previous article, I have introduced the first of the FLUID components: flexibility. According to the methodology, flexibility is not absolute as it depends on the structure of the code and on the specific changes required. However, the article closed with the promise to show one way to minimise both the average and worst-case cost of change.

The secret lies in the second principle of FLUID:

Lean Implementation is minimal effective code per task performed.

Effective code refers to the parts of the code that have an active role in the completion of the task at hand. It excludes comments, boilerplates, headers, constants, forward declarations, function prototypes and so on, but includes interfaces, class declarations, function signatures and in general anything that has a direct connection with some operation actually performed by the program.

Task performed refers to anything that a program required to accomplish: update a database, summarise data, generate a certain output, serve a specific REST endpoint and so on.

Ideally, the leanest program would be a single sentence describing the task to be performed, for example “Sum up the column ‘cost’ in the database and warn admin@somewhere if it’s greater than 2000$”, or “Reply to this REST request with a list of active users and user IDs”; everything above that introduces a potential for rigidity that goes against the FLUID principles.

OOP vs Behaviour Oriented

To illustrate the concept, I show here a simple program that loads inventory items from a database. Among the various characteristics of the items, we focus only on the in-store security check that a store should apply to a product on display; we assume that the store holds only three product macro-categories, and the security check depends entirely on that.

The following is a lean implementation in Java:

import Security;
// Other imports omitted for brevity
enum ItemType { FOOD, CLOTHING, ELECTRONICS }
class InventoryItem {
    String name;
    double price;
    ItemType type;
    public InventoryItem(String name, double price, ItemType type) {
        this.name = name;
        this.price = price;
        this.type = type;
    }
    // RELEVANT: securityCheck
    public Security.SecurityType securityCheck() {
        switch (this.type) {
            case ItemType.CLOTHING:
                return Security.addTag();
            case ItemType.ELECTRONICS:
                return Security.putInSafeDisplay();
            default:
                return Security.noSecurity();
        }
    }
}
public class InventorySystem {
    private Map<int, InventoryItem> inventory = new HashMap<>();
    // RELEVANT: addItem
    public void addItem(int itemId, ItemType type, String name, double price) {
        InventoryItem item = new InventoryItem(name, price, type);
        inventory.put(itemId, item);
    }
    public void populateItemsFromDatabase() {
      // TODO: Connect to the database
      // TODO: Query the products table
      // TODO: Iteratively call addItem for each row.
    }
}

An OOP approach would look like the following:

interface InventoryItem {
  ItemType getType();
  public Security.SecurityType securityCheck();
}
class ElectronicsItem implements InventoryItem {
    String name;
    double price;
    public ElectronicsItem(String name, double price) {
        this.name = name;
        this.price = price;
    }
    public ItemType getType() {
      return ItemType.ELECTRONICS;
    }
    public Security.SecurityType securityCheck() {
      return Security.putInSafeDisplay();
    }
}
// FoodItem and ClothingItem omitted for brevity
public class InventorySystem {
    // &
    public void addItem(int itemId, ItemType type, String name, double price) {
        InventoryItem item;
        switch (type) {
            case ItemType.CLOTHING:
                item = new ClothingItem(name, price);
                break;
            case ItemType.ELECTRONICS:
                item = new ElectronicsItem(name, price);
                break;
            case ItemType.FOOD:
                item = new FoodItem(name, price);
                break;
        }
        inventory.put(itemId, item);
    }
}

This second version has a more structured approach to the problem, but it has a less lean implementation. It is immediately evident that the code is larger (by more than 4 times); also, the logic of the security check method has been moved in the addItem of the InventorySystem  and it’s used to create instances rather than select the exact security measure to be applied.

This change seems natural in an OOP world, but notice that the responsibility to decide what security is applied on the concrete item was given to the abstraction representing said item in the conceptual world of the program. By contrast, the same logic is now delegated to the InventorySystem, which is the owner of items in an OOP conceptual world, but doesn’t match a behaviour oriented representation of real-world items.

Focusing on InventorySystem.addItem and InventoryItem.securityCheck, we can verify how flexible the two implementations are.

  • If we were to introduce a new item type, the BO implementation would require only a few additional lines in securityCheck; by contrast, the OOP version would require writing a whole new class to represent it.
  • If we were to introduce new properties for the items, the BO implementation would require only a few additional lines in addItem and, possibly, in the constructor; conversely, the OOP version would require modifying all the classes in the hierarchy in order to accept new constructor parameters.

If new behaviours are introduced, the BO implementation may require some additional work. The obvious solution would be branching within the methods representing specific behaviours, but at a certain point, it may become opportune to switch perspective, and move out of a BO implementation into a full OOP, SOLID-compliant implementation.

If we were able to know in advance which changes are the most likely, we’d be able to chose the best model in advance (BO if increasing item types or properties, OOP/SOLID if new behaviours are introduced).

But future is unknown. What is the best, future-proof approach?

Flexibility as You Go

Consider the example in the previous paragraph.

Suppose that a team adopts the BO approach, as it’s more compact and easier to map into the business logic.

Suppose also that some items require a completely different behaviour when laid on the shelves; for example, every time an item is placed on the shelve, issue a warning that has to reach the marketing department.

Rather than spaghettify the code and add checks on critical paths, the team decides it’s time to switch to a SOLID compliant OOP implementation.

What is the cost of this change?

Additional Cost of Refactoring

To answer the question, we need to compute the differential changes required in the two scenarios.

  • InventoryItem must become an interface.
  • The same code that would have been written initially in the OOP implementation needs to be written now.
  • The logic for securityCheck must be moved out the item representation and into some owning class  InventorySystem is the perfect candidate.
  • A few lines specifically written for the BO logic must either be moved into some item subclass, rewritten or removed.

All considered, the amount of code that will be completely discarded when moving from a BO approach to a SOLID compliant OOP is minimal.

Minimal Average Cost of Change

By adopting the criterion of minimal effective code for task performed, we are reducing the amount of code that might be obsoleted, over-seeded or refactored due to unforeseen changes.

With a lean approach (minimal effective code for task performed), the changes that fit naturally in the adopted models require minimal modification to the existing code.

In the off-chance that the adopted models are not anymore viable to accommodate the incoming changes, a leaner implementation minimises the code to be refactored or removed, in order to adapt to the new requirements.

A Solution for all Possible Futures

Given this, how can we select an operating framework that gives us maximal flexibility under any circumstance?

Let’s revisit the definition of flexibility I proposed at the beginning of this article: minimising the code changes necessary to apply a given functional change. This requires to know what is the functional change: some code may be more flexible to some changes, and rigid towards others.

Let’s hypothesises to know every possible future; every change that may possibly impact a project. What would be the code that would minimise the average changes that may be required in any possible situation?

If we imagine any possible change, many of them may require a partial or complete rewrite of a whole project, no matter how forethought has been applied to try and write flexible code in advance. An so,

Asymptotically, the most flexible code is the shortest code possible necessary to fulfil the requirements.

Conclusions

The considerations in this article respect the 4th principle of the AGILE manifesto:

Responding to change over following a plan

The most flexible solution is the one allowing to respond to change in the most AGILE way; while some specific design patterns helps facing some specific changes, none of them can intercept all the possible range of changes that can intervene in the lifetime of a software project.

As such, flexibility in the absolute is not granted by any specific design pattern, but by the conscious decision of keeping the code base as small as possible, and using the most code-effective techniques, frameworks and design patterns to bring home the desired results.

In a word: by a lean design.