top of page

Using Design Patterns to Streamline Model Development: Strategy

10 minutes ago
9 min read

After a brief hiatus exploring other industries, I’ve now been back in the simulation modeling world for about a year. Coming back with a few years of experience behind me, I’ve found myself looking at modeling in a slightly different way. Rather than simply building models that work, I’m increasingly interested in how I can build them better.


Jack Sparrow standing on the mast of a sinking ship, representing a simulation model that works but may be difficult to maintain.

When the model works, but maintenance is someone else’s problem... 😅


I've now found that getting a model to work is only part of the challenge. As models grow, change and get handed over to others, how we structure them becomes just as important.


That makes this a particularly exciting time for me to learn, experiment, and develop my skills further. One area I’ve been focusing on is software design and how some ideas from software development can be applied to simulation models.


One concept my mentor, Jaco-Ben Vosloo, recommended caught my attention: design patterns.


Design patterns provide established approaches to recurring software-design problems. They can help us structure our code so that it is easier to understand, maintain, and adapt as a model grows.


In this series, I’ll explore selected design patterns from Head First Design Patterns and show how to apply them to simulation models built in AnyLogic.


Our first example is the Strategy pattern.


What are design patterns?


Design patterns are commonly grouped into three categories:

  • Creational patterns – concerned with creating objects.

  • Structural patterns – concerned with how classes and objects are organized.

  • Behavioral patterns – concerned with how objects interact and how their behavior is structured.


The Strategy pattern is a behavioral pattern.


At its core, Strategy separates behavior that is likely to vary from the object that uses it.


A useful definition is:

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable.

In simpler terms, imagine an agent that needs to perform a task, but there are several ways of doing it.

Instead of putting every possible approach inside the agent, we can put each approach into its own class. The agent can then work with whichever approach is currently selected.

The important part is that the agent doesn't need to know how the behavior works.

It only needs to know what the behavior can do.


Let's see what that looks like in an inventory model.


The situation


Suppose we are building an inventory management model.

Our inventory manager receives customer demand, manages stock, tracks outstanding orders and decides when to place replenishment orders.


One particular responsibility is:

When and how much should be ordered?

We could answer that question in several ways.


For our example, we will use three:


  • Fixed Quantity – order a predefined quantity when the inventory position reaches the reorder point.

  • Order Up To – order enough to bring the inventory position back up to a target level.

  • Economic Order Quantity (EOQ) – calculate the order quantity using annual demand, ordering cost, and annual holding cost.


That makes them suitable candidates for interchangeable ordering policies.

This distinction is important. We deliberately give every policy the same responsibility: calculating an order quantity.


Building the model: the common inventory manager


Before looking at Strategy, let's build the model's common part.

We create an AbstractInventoryManager agent. Both our managers will inherit from it:


Class diagram showing AbstractInventoryManager extended by BadInventoryManager and GoodInventoryManager.


AnyLogic agent type settings showing BadInventoryManager extending AbstractInventoryManager.

The abstract manager contains the parts of the inventory model that do not change between our two implementations.


For example, it handles:

  • daily demand;

  • inventory on hand;

  • backlog;

  • inventory position;

  • outstanding orders;

  • deliveries;

  • lead time;

  • ordering costs; and

  • holding costs.


Our dailyCycle function is therefore shared by both managers:

currentDay++;
receiveDueDeliveries();
processDemand(demand);
reviewInventory();

cumulativeHoldingCost += onHand * annualHoldingCostPerUnit / 365.0;

The reviewInventory() function is where things become interesting.


This is where the manager has to answer:


Should I place an order, and if so, how much?


That is the behavior we are going to vary.


AnyLogic AbstractInventoryManager agent showing inventory variables, functions, deliveries, and a graph of on-hand inventory, backlog, and inventory position over time.


The first implementation: the Bad Manager


Let's start without the Strategy pattern.

We create a BadInventoryManager that inherits from AbstractInventoryManager.


At first, this seems perfectly reasonable.


We give the manager an enumerator variable called orderingPolicy and use it in a reviewInventory Function to decide which calculation to perform.


For example:

if (orderingPolicy == null) {
    error("No order policy has been configured");
    return;
}

double position = inventoryPosition();
double quantity = 0;

if (orderingPolicy.equals(OrderPolicy.FIXED)) {

        if (position <= reorderPoint) {
            quantity = fixedOrderQuantity;
        }

} else if (orderingPolicy.equals(OrderPolicy.ORDER_UP_TO)) {

    if (position <= reorderPoint) {
        quantity = Math.max(
            0,
            targetInventoryPosition - position
        );
    }

} else if (orderingPolicy.equals(OrderPolicy.EOQ)) {

    if (position <= reorderPoint
            && annualDemand > 0
            && orderingCost > 0
            && annualHoldingCostPerUnit > 0) {

        double eoq = Math.sqrt(
            2 * annualDemand
                * orderingCost
                / annualHoldingCostPerUnit
        );

        quantity = Math.ceil(eoq);
    }
}

if (quantity > 0) {
    placeOrder(quantity);
} else {
    lastOrderQuantity = 0;
}

Once we have only one policy, this isn't particularly alarming.

But imagine our requirements keep growing.

Tomorrow we might add another ordering policy.


Then another.


Then another.



Every new policy means opening BadInventoryManager and adding more code.


The manager now needs to know:

  • which policies exist;

  • how each policy works;

  • which variables each calculation requires; and

  • how to execute each calculation.



This is the problem we want to solve. The inventory manager is supposed to manage inventory.

Instead, it is also becoming a container for every possible ordering algorithm!


What is actually changing?


Before we start refactoring, let's stop and ask a useful design question:

What part of the manager is changing?

The answer isn't the entire inventory process.


Our managers still:

  • receive deliveries;

  • process demand;

  • calculate inventory position;

  • place orders; and

  • record inventory statistics.


The thing that changes is much smaller:

the calculation used to determine the order quantity.


This is exactly the sort of behavior that Strategy is designed to separate.


So instead of asking:

Which block of code should the manager execute?

we can ask:

Which ordering policy should the manager use?

Introducing the Strategy


We create an OrderingPolicy interface.

public interface OrderingPolicy {

    double calculateOrderQuantity(
        double inventoryPosition
    );
}
Java code showing the OrderingPolicy interface with a calculateOrderQuantity method that accepts inventory position and returns an order quantity.

This interface defines the capability that every ordering policy must provide.

It doesn't care whether the calculation uses a fixed quantity, a target inventory level, or the EOQ formula.


It simply says:

Give me the inventory position and I will give you an order quantity.

We can now create three separate implementations.


Diagram showing the OrderingPolicy interface with three interchangeable implementations: FixedQuantityPolicy, OrderUpToPolicy, and EOQPolicy.

Building the policies


FixedQuantityPolicy

Our first implementation is FixedQuantityPolicy.


It stores the reorder point and the quantity to order.



public class FixedQuantityPolicy
        implements OrderingPolicy {

    private final double reorderPoint;
    private final double orderQuantity;

    public FixedQuantityPolicy(
            double reorderPoint,
            double orderQuantity) {

        this.reorderPoint = reorderPoint;
        this.orderQuantity = orderQuantity;
    }

    @Override
    public double calculateOrderQuantity(
            double inventoryPosition) {

        if (inventoryPosition <= reorderPoint) {
            return orderQuantity;
        }

        return 0;
    }
}

Notice something important here.


The policy now owns the fixed-quantity calculation.

The manager doesn't need to know how it works.


Next, we create OrderUpToPolicy.


public class OrderUpToPolicy
        implements OrderingPolicy {

    private final double reorderPoint;
    private final double targetInventoryPosition;

    public OrderUpToPolicy(
            double reorderPoint,
            double targetInventoryPosition) {

        this.reorderPoint = reorderPoint;
        this.targetInventoryPosition =
            targetInventoryPosition;
    }

    @Override
    public double calculateOrderQuantity(
            double inventoryPosition) {

        if (inventoryPosition > reorderPoint) {
            return 0;
        }

        return Math.max(
            0,
            targetInventoryPosition - inventoryPosition
        );
    }
}

Again, the manager doesn't need to know this formula.

It simply knows that an OrderingPolicy can calculate an order quantity.

Finally, we create EOQPolicy.


public class EOQPolicy
        implements OrderingPolicy {

    private final double reorderPoint;
    private final double annualDemand;
    private final double orderingCost;
    private final double annualHoldingCostPerUnit;

    @Override
    public double calculateOrderQuantity(
            double inventoryPosition) {

        if (inventoryPosition > reorderPoint) {
            return 0;
        }

        if (annualDemand <= 0
                || orderingCost <= 0
                || annualHoldingCostPerUnit <= 0) {
            return 0;
        }

        double eoq = Math.sqrt(
            2 * annualDemand
                * orderingCost
                / annualHoldingCostPerUnit
        );

        return Math.ceil(eoq);
    }
}

The EOQ calculation is now completely contained within EOQPolicy.


This means someone working on the EOQ calculation doesn't need to modify the inventory manager.


Likewise, someone working on the inventory manager doesn't need to understand the EOQ formula.


Java code showing the three OrderingPolicy implementations: FixedQuantityPolicy, OrderUpToPolicy, and EOQPolicy, each with its own order quantity calculation.

Each policy implements the same interface but encapsulates a different order-quantity calculation.


The Good Manager


Now we can return to our manager.

Instead of storing a String such as "fixedPolicy", the Good Manager stores an OrderingPolicy.


OrderingPolicy orderingPolicy;

Its reviewInventory() function becomes remarkably small:

if (orderingPolicy == null) {
    error("No order policy has been configured");
    return;
}

double quantity =
    orderingPolicy.calculateOrderQuantity(
        inventoryPosition()
    );

if (quantity > 0) {
    placeOrder(quantity);
} else {
    lastOrderQuantity = 0;
}

That's it.


The manager doesn't contain the fixed quantity formula.

It doesn't contain the Order Up To formula.

It doesn't contain the EOQ formula.


It simply asks its current policy:

How much should I order?

The policy answers.


The manager then decides what to do with that answer.

This is the Strategy pattern in action.


But who chooses the policy?


There is an important detail here.


The Strategy pattern doesn't mean that the decision about which strategy to use disappears.

We still need somewhere to choose the active policy.


In our model, Main handles that responsibility.


We create the policies:

OrderingPolicy fixedPolicy =
    new FixedQuantityPolicy(90, 120);

OrderingPolicy orderUpToPolicy =
    new OrderUpToPolicy(90, 220);

OrderingPolicy eoqPolicy =
    new EOQPolicy(90, 9125, 60, 8);

Then our buttons select the active policy.


For example:

GoodManager.orderingPolicy = fixedPolicy;

or:

GoodManager.orderingPolicy = orderUpToPolicy;

or:

GoodManager.orderingPolicy = eoqPolicy;

The Bad Manager receives the same selection, but as an enumerator:

BadManager.orderingPolicy = OrderPolicy.Fixed;

This gives us a useful comparison.



The selection mechanism is therefore separate from the calculation itself.


The result


Now comes the interesting part.

We run both managers using the same demand stream.


When we select the Fixed Quantity Policy, both managers use the same calculation.

When we switch to Order Up To, both use the same calculation.

When we switch to EOQ, both use the same calculation.


And because they use the same underlying inventory mechanics, their results are the same.


The difference is not the outcome.

The difference is where the behavior lives.


Animated AnyLogic inventory model showing the Fixed Quantity, Order Up To, and EOQ policies being selected while the Bad and Good Managers produce matching inventory results.

The graphs on our model show:

  • On Hand – the inventory currently available;

  • Backlog – demand that could not be fulfilled; and

  • Inventory Position – on-hand inventory plus outstanding orders minus backlog.


Because both managers receive the same policy and demand, their graphs should follow the same behavior.


The Strategy pattern hasn't made the inventory calculation magically different.

It has changed how the code is organized.




Why does this matter?


Imagine that we now want to add a fourth ordering policy.


With the Bad Manager, we would need to open BadInventoryManager, add another branch, and put the new calculation inside it.


With the Good Manager, we can create another implementation:


public class NewOrderingPolicy
        implements OrderingPolicy {

    @Override
    public double calculateOrderQuantity(
            double inventoryPosition) {

        // New calculation
    }
}

The existing ordering-policy classes don't need to change.


The Good Manager doesn't need to know how the new calculation works.


This separation also makes collaboration easier. One person can work on the inventory manager while another works on a particular ordering policy, because the responsibilities are separated through the interface.


That doesn't mean Strategy should be added to every model. If you have only one simple, stable ordering calculation, introducing an interface and several classes may add unnecessary complexity.


The benefit appears when we genuinely have multiple interchangeable behaviors, particularly when those behaviors are likely to evolve independently.


Conclusion


When I first started building models, I often thought that adding another requirement simply meant adding another condition.


Sometimes that's exactly what we need.


But as models grow, I've found that getting the model to work is only part of the challenge. How we structure the model becomes just as important.


As the number of alternatives grows, those conditions can signal that different responsibilities are being mixed together.


In our inventory model, the Bad Manager contained the logic for every ordering policy.

The Strategy pattern allowed us to separate that changing behavior into its own classes.


Our final structure looks like this:


Comparison diagram showing a BadInventoryManager containing all ordering policies versus a GoodInventoryManager using an OrderingPolicy with separate FixedQuantity, OrderUpTo, and EOQ policy implementations.

The manager is responsible for managing the inventory process.


The ordering policies are responsible for calculating order quantities.


And Main is responsible for choosing which policy is active.


That separation gives us something valuable as our models grow: we can change one piece of behavior without having to untangle the rest of the model. More importantly, the model still produces the same results. We've changed the way the code is organized, not what the model is trying to do.


That is the real lesson of the Strategy pattern:

When behavior varies, consider separating that behavior from the object that uses it.

Now, maintenance doesn't look quite so scary. 😅



The example model can be downloaded below:



Devon Cowling is a simulation engineer and data scientist working with The AnyLogic Modeler. To contact him, use LinkedIn.


With contributions from Selaelo Kgoale



What next?

If you liked this post, you could read more posts by following the links above. Why not subscribe to our blog or follow us on any of the social media accounts for future updates? The links are in the Menu bar at the top, or the footer at the bottom. You can also join the mobile app here!


If you really want to make a difference in supporting us please consider joining our Patreon community here. 


If you want to contact us for some advice, maybe a potential partnership or project or just to say "Hi!", feel free to get in touch here, and we will get back to you soon!



Comments


  • LinkedIn
  • Facebook
  • YouTube
  • Twitter
  • Instagram

©2021 by The AnyLogic Modeler

bottom of page