Composite Design Pattern

Design File System

Introduction

Objects within Objects

Any problem that can be designed as a tree structure will fall under the composite design pattern.

Example 1: Consider the example of a company structure. The CEO is at the top, followed by the Director, Manager, Marketing team, and IT Engineer. The IT Engineer is the leaf node with no one under this position.

Example 2: Take the example of a delivery box. The box can contain a product, or the product can be wrapped inside another box. This sequence can continue based on the product.

Standard UML Diagram - Composite Design pattern

Problem Statement - Design File System

In a file system, there are two types of objects: files and directories. A directory can have multiple files and directories, but a file is a single object that cannot contain any other objects.

The directory has a File object

File class

package ProblemStatement;

public class File {
    private String fileName;

    public File(String fileName) {
        this.fileName = fileName;
    }

    public void ls() {
        System.out.println("File Name "+fileName);
    }
}

Directory Class

package ProblemStatement;

import java.util.ArrayList;
import java.util.List;

public class Directory {

    private String directoryName;

    private List<Object> objectList;

    public Directory(String directoryName) {
        this.directoryName = directoryName;
        this.objectList = new ArrayList<>();
    }

    public void add(Object object){
        objectList.add(object);
    }

    public void ls(){
        System.out.println("Directory Name: "+directoryName);
        for (Object obj: objectList){
            if (obj instanceof File){
                ((File) obj).ls();
            } else if (obj instanceof Directory) {
                ((Directory) obj).ls();
            }
        }
    }
}

We can see from the code in the directory that, in order to print the list of files (i.e., ls()), we need to always check the instances of the object. To avoid this code smell, we use the composite design pattern.

Solution

We can see that a directory contains files and other directories. Hence, we can use the composite design pattern.

So, we create an interface of a FileSystem and later create two child classes that implement this interface.

<<FileSystem>>

package Solution;

public interface FileSystem {
    public void ls();

}

File Class

package Solution;

public class File implements FileSystem{

    private String fileName;

    public File(String fileName) {
        this.fileName = fileName;
    }

    @Override
    public void ls() {
        System.out.println("File Name "+ fileName);
    }
}

Directory Class

package Solution;

import java.util.ArrayList;
import java.util.List;

public class Directory implements FileSystem{

    private String directoryName;

    private List<FileSystem> fileSystemList;

    public Directory(String directoryName) {
        this.directoryName = directoryName;
        this.fileSystemList = new ArrayList<>();
    }

    public void add(FileSystem fileSystemObj){
        fileSystemList.add(fileSystemObj);
    }

    @Override
    public void ls() {
        System.out.println("Directory Name "+ directoryName);
        for (FileSystem fileSystemObj: fileSystemList){
            fileSystemObj.ls();
        }
    }
}

In the class above, we can see that there is no checking of instances of the object.

Driver class

package Solution;

public class Main {

    public static void main(String[] args) {
        Directory movieDirectory = new Directory("Movie");

        FileSystem border = new File("Border");
        movieDirectory.add(border);

        Directory comedyMovieDirectory = new Directory("comedyMovie");
        File hulchul = new File("hulchul");
        comedyMovieDirectory.add(hulchul);

        movieDirectory.add(comedyMovieDirectory);

        movieDirectory.ls();
    }
}

Problem Statment - Calculator

Arithmetic Expression Evaluator

Expression - 2 * ( 1 + 7)

Solution

The UML diagram represents the Arithmetic Expression. We will create an interface named ArithmeticExpression, which includes the evaluate method. We will have two child classes: one is 'Number,' and the other is 'Expression.' For the 'Number' class, the evaluate method returns the number itself. The 'Expression' class contains all the details about the expression, such as the arithmetic expressions on the left and right, and the type of operation. In the 'Expression' class, the evaluation method calculates the value based on the operation using a switch case.

UML

ArithmeticExpression class

package ExpressionEvaluation;

public interface ArithmeticExpression {
    public int evaluate();
}

Number Class

package ExpressionEvaluation;

public class Number implements ArithmeticExpression{

    private int value;

    public Number(int value) {
        this.value = value;
    }

    @Override
    public int evaluate() {
        System.out.println("Number Value is " + value);
        return value;
    }
}

Operation enum class

package ExpressionEvaluation;

public enum Operation {
    ADD,
    SUBTRACT,
    MULTIPLY,
    DIVIDE;
}

Expression class

package ExpressionEvaluation;

public class Expression implements ArithmeticExpression{

    private Operation operation;
    private ArithmeticExpression leftExpression;
    private ArithmeticExpression rightExpression;

    public Expression(Operation operation, ArithmeticExpression leftExpression, ArithmeticExpression rightExpression) {
        this.operation = operation;
        this.leftExpression = leftExpression;
        this.rightExpression = rightExpression;
    }

    @Override
    public int evaluate() {
        int value = 0;

        switch (operation){
            case ADD -> {
                value += rightExpression.evaluate() + leftExpression.evaluate();
            }
            case SUBTRACT -> {
                value += rightExpression.evaluate() - leftExpression.evaluate();
            }
            case DIVIDE -> {
                value += rightExpression.evaluate() / leftExpression.evaluate();
            }
            case MULTIPLY -> {
                value += rightExpression.evaluate() * leftExpression.evaluate();
            }
        }
        System.out.println("Expression value is "+value);
        return value;
    }
}

Main Driver class

package ExpressionEvaluation;

public class Main {
    public static void main(String[] args) {

        ArithmeticExpression one = new Number(1);
        ArithmeticExpression two = new Number(2);
        ArithmeticExpression seven = new Number(7);

        ArithmeticExpression addExpression = new Expression(Operation.ADD, one, seven);
        ArithmeticExpression parentExpression = new Expression(Operation.MULTIPLY, two, addExpression);
        System.out.println(parentExpression.evaluate());
    }
}