In this tutorial, we will solve a real-world problem and solve it with the composite pattern.

The Real-world business problem that I had in my job

I will show the same problem I presented in my template pattern article (because we solved that problem using a combination of template, builder, and composite).

We had one microservice that just worked as a proxy to Salesforce. 

Our proxy service had multiple endpoints like getCarsByCriteria , getPersonsByCriteriagetCarsWithExpiredInsurance, etc. Inside the proxy service, we do a few things:

  • Transform the API request to the salesforce query(which is very similar to the SQL query)
  • Invoke Salesforce(using a query that we just created) through HTTP
  • When we receive the result, convert the result to the proxy service response

Which part of the problem here can be solved using composite?

One Salesforce query can have multiple conditions. For example, we can query for cars manufactured > 2019 AND insurance = expired AND color = red. As you see, one condition I wrote is COMPOSED of three separate conditions. In our example, those three conditions are connected using the AND operator but we should be able to connect (compose) our conditions in different ways.

The Difference between the standard composite pattern and the complex one that I am showing you

What is the definition of the composite pattern?

The intent of a composite is to “compose” objects into tree structures to represent part-whole hierarchies. Implementing the composite pattern lets clients treat individual objects and compositions uniformly” – Gof Book

I need to say that what I am going to show you is a bit complex instance of composite pattern. To understand our complex example, understanding standard composite is required. For that reason, I have to go quickly through the standard example.

Simple composite problem explained

The usual composite problem involves only one way of composing the parts. For example, if your task is to create an ordering system for Amazon you might want to have classes such as BoxProduct, and Component(this is an abstraction, Component can be either a Box or Product).

There are multiple scenarios of how order can look like:

  • Order can be only one product(no boxes)
  • Order can be one box that contains three products
  • Order can be a box that contains one smaller box(which contains two products) plus one product, etc.

The important thing to notice is that there is only one way to compose products into the bigger units and that is by adding products to the box or adding smaller boxes to the bigger boxes, etc. However, that’s one operation – adding. Let’s take a look quickly at the UML diagram of the simple composite:

Let’s for a second show simple implementation of this diagram:

public interface Component {
    int getPrice();
}

public class Box implements Component {
    private List<Component> components = new ArrayList<>();
    @Override
    public int getPrice() {
        if (components != null) {
            return components.stream().mapToInt(Component::getPrice).sum();
            //here you can have complex logic. For example, if there are more than 10 products then you approve 5%
            //discount or if the sum is greater than 10000 then another discount, etc.
        }
        return 0;
    }
    
    public void add(Component component) {
        components.add(component);
    }
}

public class Product implements Component {

    public Product(String name, int price) {
        this.name = name;
        this.price = price;
    }

    private String name;
    private int price;
    @Override
    public int getPrice() {
        return price;
    }
}

The usage of this is fairly straightforward:

public class Client {
    public static void main(String[] args) {
        Component boxOrder = createBoxOrder();
        Component productOrder = createProductOrder();
        System.out.println("Price of box order is: " + boxOrder.getPrice());
        System.out.println("Price of product order is: " + productOrder.getPrice());
    }

    private static Component createBoxOrder() {
        Box box = new Box();
        box.add(new Product("BMX bicycle", 600));
        Box smallerBox = new Box();
        smallerBox.add(new Product("Phone charger",20));
        smallerBox.add(new Product("Chess book",30));
        box.add(smallerBox);
        return box;
    }

    private static Component createProductOrder() {
        return new Product("PS5 Joystick", 400);
    }
}

As you can see in the main method, the client does not need to know if the order(component) is only one simple product or if the component is a big box full of other smaller boxes and products. That would be the end of the simple composite pattern, now let’s move on to this more complex and interesting example.

Our complex real-world problem

Note: I found this idea in the book “Domain-Driven Design: Tackling Complexity in the Heart of Software By Eric Evans” in the chapter named “A Declarative Style of Design”. There are a lot of great examples in that book.

In our case, we have multiple ways to compose objects. We are not adding conditions to a bigger condition but we can compose complex conditions using ANDOR, and NOT operations. Those are three ways to compose instead of one. Let’s first try one logical but naive solution. Following the logic of the previous example, instead of one class Box, we would have three classes: AndConditionOrCondition, and NotCondition so the client would call our code like this:

        //name= "John" AND age = 20
        AndCondition andCondition = new AndCondition();
        andCondition.add(new EqualsCondition("name", "John"));
        andCondition.add(new EqualsCondition("age", 20));
        andCondition.getQuery();

Ouch. This looks odd. Composing any complex query would be a nightmare and very error-prone. We want a better experience for the client that uses our class. We want to offer him an API that looks like this:

Condition condition = new EqualsCondition("name", "John").and(new EqualsCondition("age", 20)).getQuery();
//you can notice that if we want to create a query "(name= "John" AND age = 20) or married = true", we have a problem. We will solve that problem a bit later, stick with me.

This is way more logical. Our client connects smaller conditions with and method and he doesn’t need to create an instance of AndCondition class. Let’s see and explain the implementation of this new complex composite:

public interface Condition {
    Condition and(Condition condition);
    Condition or(Condition condition);
    Condition not();

    String getQuery();
}

If you think about how Condition should behave from the domain perspective, you will realize that you want to be able to chain both container objects(AndConditionOrCondition, and NotCondition) and also leaf conditions such as EqualsCondition. Notice that in our previous example in the Component interface, there was no add method. That’s because if you create a product there is no sense to call an add method on the product because you can only add to the box, not into the product. This is the first difference between those two examples. Another difference would be that in the previous example, the client was the one who was creating the instance of the Box(container object). In our example, we are simplifying things for the client offering him those nice andor and not methods and we will handle creating or container classes in our code!

Also, in the Condition interface, there is a getQuery method that will create our query as a String representation.

Now if we create a class EqualsCondition implements Condition we will realize that we only want to override the getQuery method but are forced to override other methods as well. Why would we implement and method in the EqualsCondition? That doesn’t make any sense at all. For that reason, let’s first take care of the container conditions:

public abstract class AbstractCondition implements Condition {
    @Override
    public Condition and(Condition condition) {
        return new AndCondition(this, condition);
    }

    @Override
    public Condition or(Condition condition) {
        return new OrCondition(this, condition);
    }

    @Override
    public Condition not() {
        return new NotCondition(this);
    }
}

public class AndCondition extends AbstractCondition {
    private final Condition condition1;
    private final Condition condition2;

    public AndCondition(Condition condition1, Condition condition2) {
        this.condition1 = condition1;
        this.condition2 = condition2;
    }

    @Override
    public String getQuery() {
        return condition1.getQuery() + " AND " + condition2.getQuery();
    }
}

public class OrCondition extends AbstractCondition {
    private final Condition condition1;
    private final Condition condition2;

    public OrCondition(Condition condition1, Condition condition2) {
        this.condition1 = condition1;
        this.condition2 = condition2;
    }

    @Override
    public String getQuery() {
        return condition1.getQuery() + " OR " + condition2.getQuery();
    }
}

public class NotCondition extends AbstractCondition {

    private final Condition condition;

    public NotCondition(Condition condition) {
        this.condition = condition;
    }

    @Override
    public String getQuery() {
        return "!" + condition.getQuery();
    }
}

You will realize that there is a bug inside the getQuery method of the NotCondition. If the condition is “name = 5”, negation is not “!name=5”. If we wanted to implement not method properly we would need to implement not in every single leaf class instead. But our use case was that in most leaves we don’t use not so we decided to give it a quick fix. We had a few checks inside that method checking what is the instance of the condition so we were throwing an exception if the condition is not one of those three container conditions.

In my opinion, this is a good lecture on how you don’t need to be “100% clean and pedantic” if your use cases don’t require those things from you. In this case, we made the right decision. “Clean and pedantic” solution would be an overengineering. Another important note is that AbstractCondition class handles the creation of the container objects. And now we can finally implement EqualsCondition:

public class EqualsCondition extends AbstractCondition {

    private final String fieldName;
    private final Object fieldValue;

    public EqualsCondition(String fieldName, Object fieldValue) {
        this.fieldName = fieldName;
        this.fieldValue = fieldValue;
    }

    @Override
    public String getQuery() {
        return fieldName + " = " + SalesforceQueryCreator.getSalesforceFormat(fieldValue);
    }
}

The purpose of this EqualsCondition class is to concatenate the name of the column with its value using the = operator. We don’t want to handle adding apostrophes to string fields, converting LocalDate or LocalTime to a proper salesforce formatter, etc. in this class. Because if we handle those things here we would need to repeat our code in many other leaf classes. Instead, we delegate that to the SalesforceQueryCreator class which adds apostrophes, format dates, etc.

I hope you noticed that I made another “controverisal” decision here. If you think about how to unit test our EqualsCondition you will realize there is no way to mock SalesforceQueryCreator class. That is one of the reasons why you shouldn’t call static methods like this. But if you think more about this problem, the solution is not that trivial. If you want to pass SalesforceQueryCreator as a dependency that looks ugly because your client class would look like this:

Condition condition = new EqualsCondition("age", 20, salesforceQueryCreator).and(new EqualsCondition("name", "John", salesforceQueryCreator))

Disgusting. Every single time when you create a condition, you need to pass that creator as an argument. You can try to create ConditionFactory:

public class ConditionFactory {

    public static Condition equals(String fieldName, Object fieldValue) {
        return new EqualsCondition(fieldName, fieldValue, new SalesforceQueryCreator());
    }
}

But even if you try that you will see that code is too complex and you have a small gains. Why do you complicate that much? What was the original problem? EqualsCondition is not properly decoupled from SalesforceQueryCreator class? Pfff, big deal. Did we have any big issues regarding that? Nope. So, just make a practical decision and skip solving problems that don’t exist. Make that SalesforceQueryCreator static for the time being and if a problem arises in the future we will deal with it in the future.

Last small problem

We have one last small problem regarding “(name= “John” AND age = 20) or married = true” query. There is no way to properly do this since we don’t have a way to add brackets now. We can apply a decorator pattern to solve this problem:

public class BracketsCondition extends AbstractCondition {

    private final Condition condition;

    public BracketsCondition(Condition condition) {
        this.condition = condition;
    }

    @Override
    public String getQuery() {
        return "(" + condition.getQuery() + ")";
    }
}

And now we can create problematic query in this fashion:

Condition condition = new BracketsCondition(new EqualsCondition("name", "John").and(new EqualsCondition("age", 20)))
                    .or(new EqualsCondition("married", true));

Uml diagram of our complex composite

The main differences between this one and the simple composite UML are:

  • Condition interface contains andor and not methods since those are relevant for both containers and leaves
  • Instead of one container class Box, we have multiple classes here AbstractConditionAndConditionOrConditionNotCondition
  • Leaf does not extends Condition (because we don’t want to implement andor and not) but extends container class

Still, even with those differences, our example satisfies the definition of a composite pattern.

Conclusion

And that’s it. You learned composite pattern in a real-world, very instructive example!