DEV Community

Cover image for How Mental Models Influence Software Design
RyTheTurtle
RyTheTurtle

Posted on

How Mental Models Influence Software Design

The shared language, relationships, and framework of thought, collectively referred to as a "mental model", is the foundation for how we think about the world around us. In software development, the "world" is the application domain that we're working in. These domains can range from high level business applications such as ecommerce stores and insurance adjustment software, to "low level" application domains like device firmware and networking infrastructure.

Deciding which design patterns, paradigms, and software architecture are "right" for a particular application heavily depends on the mental model of the domain. Regardless of the domain, highly effective software development teams design and implement software in a way that closely matches the mental model of the domain. A mismatch between the mental models of the domain and the software implementation leads to systems that are difficult to understand, maintain, and evolve over time. This concept is commonly understood and explained when discussing the object-relational impedance mismatch problem between Object Oriented Design and relational databases. The same concept applies to the mismatch between the mental model of the application domain and the implementation model of the software.

To illustrate this concept, we can observe how two different mental models of the same domain can lead to two different implementation decisions to solve the same problem. The only deciding factor of which approach is "correct" is whether the implementation matches the mental model of the domain shared throughout the team and organization.

Case Study: Implementing Coffee Shop Menu Pricing

The book Head First Design Patterns uses the concept of a coffee shop ordering application as an example to illustrate the implementation of the decorator pattern. The author makes the decision to implement the decorator pattern based on their mental model of the coffee shop, but we can examine how a different mental model would dramatically change how we approach the task of ordering coffee.

The Task

In the coffee shop example, the application needs to be able to take a customer's order for a beverage and accurately calculate the price of the beverage. This needs to be implemented in a scalable way to handle updates to the coffee shop's menu over time. The menu of the coffee shop looks something like this

Coffees:
  House Blend: 0.89
  Dark Roast:  0.99
  Decaf:       1.05
  Espresso:    1.99

Condiments:
  Steamed Milk: 0.10
  Mocha:        0.20
  Soy:          0.15
  Whip:         0.10
Enter fullscreen mode Exit fullscreen mode

Mental Model 1: Condiments Are The Beverage

In Head First Design Patterns, the author's mental model treats every combination of beverage and condiments as a unique beverage. For example, a "double soy mocha latte" is considered a different beverage than "mocha latte with whip cream".

Using this mental model, the author goes on to illustrate how the decorator pattern can be used to implement the pricing logic. The detailed explanation of the decorator pattern is beyond the scope of this article, but the end result is that an order such as "house blend with soy mocha and whip cream" can be expressed with something like this

abstract class Beverage { 
    abstract double getCost();
}

public class HouseBlend extends Beverage { 
    public double cost(){ 
        return 0.89;
    }
}

abstract class CondimentDecorator extends Beverage { 
    Beverage beverage; 
}

public class Mocha extends CondimentDecorator { 
    public Mocha(Beverage b){ 
        this.beverage = b;
    }

    public double cost() {
        return beverage.cost() + .20; 
    }
}

public class WhipCream extends CondimentDecorator { 
    public Mocha(Beverage b){ 
        this.beverage = b;
    }

    public double cost() {
        return beverage.cost() + .10; 
    }
}


public class Soy extends CondimentDecorator { 
    public Mocha(Beverage b){ 
        this.beverage = b;
    }

    public double cost() {
        return beverage.cost() + .15; 
    }
}

// a soy mocha coffee with whip cream
Beverage order = new HouseBlend();
Beverage addedSoy = new Soy(order);
Beverage addedMocha = new Mocha(addedSoy);
Beverage addedWhip = new Whip(addedMocha);
Enter fullscreen mode Exit fullscreen mode

Each condiment decorator creates a new object (literally and conceptually) that has it's own unique definition of it's cost. The condiments are an input in to what beverage the customer is ordering, but condiments are not first-class entities. If the mental model of the team and business is that every combination of condiment and coffee is a unique beverage, this would be a perfectly acceptable approach to implementing the fictional coffee shop pricing logic.

Mental Model 2: A Beverage Consists of Condiments

Looking at the menu of items in the fictional coffee shop, both Condiments and Coffees are their own sections of the menu. Taking a cue from the menu, it's easy to see how someone would develop the mental model of a beverage being one of the coffees, with condiments being an entirely separate concept. In this mental model, condiments can be added to a beverage, but are not themselves tied to the definiton of a beverage. This mental model might be more applicable if our fictional customers are allowed to order condiments directly, such as ordering a cup of whip cream to give to their dog as a treat.

With this mental model, if we kept the approach of using the decorator pattern to represent every combination of coffee and condiments as a distinct beverage, a developer reading the code would likely struggle to understand the structure and make changes to it correctly as new requirements evolved. However, a structure that treats condiments separately , modeled below, would more closely repesent this mental model.

abstract class Beverage { 
    protected List<Condiment> condiments = new ArrayList<>();
    protected double price; 

    public void addCondiment(Condiment c){ 
        this.condiments.add(c);
    }

    public double getCost() { 
        double cost = this.price;
        for(Condiment c: condiments){ 
            cost += c.price();
        }
        return cost;
    }
}

public class HouseBlend extends Beverage { 
    public HouseBlend(){ 
        this.price = 0.89;
    }
}

public enum Condiment { 
  SOY(0.15),
  MOCHA(0.20),
  WHIP(0.10)

  public final double price;

  Condiment(double price) { 
    this.price = price;
  }
}


Beverage order = new HouseBlend();
order.addCondiment(SOY);
order.addCondiment(MOCHA);
order.addCondiment(WHIP);

Enter fullscreen mode Exit fullscreen mode

Which design is "correct"?

The answer is "it depends". In either scenario, if the code reflects the mental model of beverages and condiments that is used by the domain experts, it's a good design choice. When code reflects the mental model used by the developers, it's (usually) the correct choice because there is little translation that has to be done between the mental model used when communicating to stakeholders and the logic that is expressed in the code. Choosing designs, architectures, and paradigms that match the shared mental model of the domain is a key factor in writing code that can be easily understood and maintained over time.

Top comments (0)