The green reed that bends in the wind is stronger than the great oak that breaks in a storm.

 Confucius

The first component of the FLUID methodology is flexibility: characteristic of code being resilient to change with minimal impact.

The concept of code flexibility is often cited in literature as a good thing(TM), but not to the point of sacrificing performance or quality.

However, in my career, I observed that well designed code ends up being both reasonably flexible and efficient: flexibility, quality and performance are not necessarily a trade off.

A Measure of Flexibility

Although highly sought after by many developers, flexibility is a poorly defined concept; it is often just recognised based personal experience. I like to define it as follows:

Code flexibility is minimising the ratio of effective code changes required per functional change applied.

Effective code refers to the parts of the code that actively perform tasks or functions. 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.

A functional change is a change in either requirements or desired output of a program. For example, adding a column of data to a tabular output, performing additional intermediate computation, adding checks on the user input, and so on. It doesn’t include fixing bugs, unless that introduces new functionalities a side effect.

Flexibility is Relative

Given two programs performing the same task, the more flexible of them is the one requiring fewer modifications to the e_ffective code_ to implement the same functional change. While this seems a concept simple enough, there is a complication: not all the functional changes are born equal.

In the rest of the article, I’ll demonstrate that even this simple engineering principle produces different results given different parameters: a code structure that is most flexible when a certain change is applied becomes rigid in other scenarios.

Flexibility through Modularity

Let’s take a simple example: applying a discount to prices during compilation of invoices.

Initially, we are required only to either apply either a flat or a percentage discount:

public class DiscountCalculator {
    public double applyDiscount(double price, double discount, bool isFlat) {
        if (isFlat) {
            if (discount < price ) return price - discount;
            return 0;
        } else {
            return price - (price * discount/100.0);
        }
    }
}

An equivalent OOP would be:

// Interface
interface DiscountStrategy {
    double applyDiscount(double productPrice);
}
// Subclass for a Fixed Discount
class FixedDiscount implements DiscountStrategy {
    public double applyDiscount(double productPrice, double amount) {
        if(amount < productPrice) return productPrice - amount;
        return 0.0;
    }
}
// Subclass for a Percentage Discount
class PercentageDiscount implements DiscountStrategy {
    public double applyDiscount(double productPrice, double amount) {
        return productPrice - (productPrice * amount / 100.0);
    }
}
// Main class
public class DiscountCalculator {
    private DiscountStrategy strategy;

    DiscountCalculator(DiscountStrategy s) {
      strategy = s;
    }
    public double applyDiscount(double price, double discount) {
        return strategy.applyDiscount(price, discount);
    }
}

Let’s suppose that we need to introduce a new discount system, based on the purchase history of customers, to be retrieved from a database.

Doing that on the OOP side would require something like the following (boilerplate, error checks and imports are omitted for simplicity):

public class FidelityDiscountStrategy implements DiscountStrategy {
    private final int userId;
    private final int discountThreshold; // Number of purchases to qualify for a discount
    public FidelityDiscountStrategy(int userId, int discountThreshold) {
        this.userId = userId;
        this.discountThreshold = discountThreshold;
    }
    private Connection connectToDatabase() {
      return DriverManager.getConnection(
           "jdbc:your_database_url", "your_username", "your_password");
    }
    private int countUserPurchasesInLastMonth() {
        String query = "SELECT COUNT(*) FROM user_purchases WHERE user_id = ? AND purchase_date > ?";
        LocalDate oneMonthAgo = LocalDate.now().minus(1, ChronoUnit.MONTHS);
        Connection connection = connectToDatabase();
        PreparedStatement preparedStatement = connection.prepareStatement(query)) {

        preparedStatement.setInt(1, userId);
        preparedStatement.setDate(2, java.sql.Date.valueOf(oneMonthAgo));
        ResultSet resultSet = preparedStatement.executeQuery();
        if (resultSet.next()) return resultSet.getInt(1);
        return 0;
    }
    @Override
    public double applyDiscount(double productPrice, double amount) {
        if (countUserPurchasesInLastMonth() >= discountThreshold) {
            return productPrice - productPrice * amount / 100.0;
        }
        return productPrice;
    }
}

Even without writing any pseudocode for the other case, it is immediately evident how the branch-based code would be less fluid: not only we’d need to write more or less the same code as in the above example, but we’d need to pass additional information to the applyDiscount() function; information like the userId, the database connection, additional parameters to decide whether the user may be eligible for a discount or not, and possibly even more as further requirements are introduced.

This would require also changing all the user code of the DiscountCalcuator: all of its callers would now have to pass the needed information. Such change may easily propagate to the callers of the user code, up to propagating to a large portion of the original code.

In this scenario, the OOP version is vastly more flexible, as it isolates the concerns regarding the special case of user-fidelity based discounts to a specific, opaque entity.

A counterintuitive example

For how OOP (and all the methodologies based on that as i.e. SOLID) is considered, by average, the most flexible design in many situations, there are scenarios where it becomes detrimental.

Let’s take the simple task of a program printing a greeting at user login depending on the time of the day. This can be achieved with a simple branch based on the current time:

// somewhere in a class&
public static greetUser() {
    int hour = getCurrentHour(); // Method to get current hour
    if (hour < 12) {
        System.out.println("Good morning!");
    } else {
        System.out.println("Good evening!");
    }
}

A purely OOP approach would require to create an interface for greeting the user, and performing the greeting in specialised sub-classes

// Interface for greeting based on time of day
interface TimeBasedGreeting {
    void displayGreeting();
}
// Morning Greeting Implementation
class MorningGreeting implements TimeBasedGreeting {
    public void displayGreeting() {
        System.out.println("Good morning!");
    }
}
// Evening Greeting Implementation
class EveningGreeting implements TimeBasedGreeting {
    public void displayGreeting() {
        System.out.println("Good evening!");
    }
}
// Somewhere in a class&
private static int getCurrentGreeting() {
  int hour = getCurrentHour();
  if (hour < 12) {
      return new MorningGreeting();
  } else {
      return new EveningGreeting();
  }
}
public static void greetUser() {
    TimeBasedGreeting greeting = getCurrentGreeting();
    greeting.displayGreeting();
}

It is immediately evident that if we need to add another greeting to be issued at a certain time of the day, like “good afternoon”, the first program is more flexible: we simply need to add a branch to the code to achieve the same result that can be obtained only by creating a new full class (and a similar branch) in the second code.

In this case, both the AGILE methodology and the flexibility concept in the FLUID framework suggest that we should stick to the first approach until.

Configure for Flexibility

We have shown how flexibility is a relative concept, which depends on the specific change required. Some type of changes may require extensive rewrites of codebases written under certain frameworks, while being trivial when applied to others.

A project can achieve maximal flexibility when the types of changes likely to occur during its lifespan are known in advance.

Future is unknowable, but there are several ways through which a development team can constraint the type of changes coming their way; supposing that this is possible, here are the most common change scenario and how to achieve maximal flexibility in each of them.

  • Operational changes, e.g. in the input or output format, or in the amount or type of internal processing required: procedural imperative style. When the meta-parameters of the problem stay constant, and only the details of existing operations need adjusting, a procedural style allows to organise the code so that there is a tight fitting between application logic and code units.
  • Interface changes, e.g. in the return or parameter types that cannot be masked by a common interface: functional style. Functional orientation allows to focus on the processing logic rather than on the details of the data types.
  • Domain changes, e.g. for example adding a database storage besides an existing file-based one, or adding new categories of items: Object Oriented style. The class-instance abstraction provides the most flexible way to treat uniformly entities with very different behaviour, requirements, setup, implementation details etc. More specific OOP models and methodologies (interfaces, multiple inheritance, composition, SOLID, etc.) can provide higher flexibility depending on the finer details of the type of domain change, but a fine analysis goes beyond the scope of this article.

Conclusions

In the FLUID methodology, flexibility is not absolute. There isn’t an a recipe that will automatically make your code the most flexible possible under all circumstances.

It is possible to select a maximally flexible approach when the type of functional changes that will affect a project in its lifetime its known in advance, but in most cases, this is a factor outside the control of the developers.

However, the FLUID methodology suggests following a principle granting maximum average flexibility given all possible functional changes, while minimising the negative impact in the worst case scenario.

We’re going to analyse it next: Lean Implementation.