Composite pattern in Java

The composite pattern in Java is an essential design pattern that is often used to manage hierarchical structures of objects. It is a structural pattern that allows you to treat objects and groups of objects uniformly, providing a way to handle both individual and collections of objects seamlessly.

Imagine you have a tree-like structure of objects in your application. Each object in the tree may have child objects and can be either a leaf or a branch node. Without the composite pattern, you would have to write code to handle each type of node in the tree separately, leading to a lot of duplicated code.

But with the composite pattern, you can create a common interface that all objects in the tree will implement, and then write code that interacts with the objects using that interface. This way, you can treat both leaf and branch nodes in the same way, without worrying about their implementation details.

Not only does this simplify your code and reduce the chance of introducing bugs, but it also makes it easy to add new types of nodes to the tree in the future. You can simply create a new class that implements the common interface, and the rest of the code will work without modification.

Moreover, the composite pattern also allows you to perform operations on the entire tree structure as a whole. You can traverse the entire tree, execute some functionality on all nodes, or even filter out specific nodes based on some criteria. This is a powerful capability that can save you a lot of time and effort in your application development.

In summary, the composite pattern in Java is a powerful tool for managing hierarchical structures of objects. By providing a common interface and treating objects and groups of objects uniformly, it simplifies your code and reduces the chance of introducing bugs, while also allowing you to perform operations on the entire tree structure as a whole.

SOLID Principles part 2:

  1. Open/Closed Principle (OCP): The Open/Closed Principle states that software entities (classes, modules, etc.) should be open for extension but closed for modification. In other words, we should be able to add new functionality to our code without modifying existing code.

For example, if we want to add a new type of report to our application, we should be able to do so without changing the code that generates the existing reports. This can be achieved by using interfaces, abstract classes, or other design patterns that allow us to add new functionality through inheritance or composition.

By following the Open/Closed Principle, we create code that is more modular and easier to extend, reducing the risk of introducing new bugs when adding new features.

  1. Liskov Substitution Principle (LSP): The Liskov Substitution Principle states that objects of a superclass should be able to be replaced with objects of a subclass without affecting the correctness of the program. In other words, the subclass should behave in the same way as the superclass, and any assumptions made about the superclass should also hold for the subclass.

For example, if we have a class that handles database connections, we should be able to replace it with a subclass that provides a different implementation of the same functionality, such as a mock object for testing. The behavior of the mock object should be consistent with the behavior of the original class, so that our application functions correctly.

By following the Liskov Substitution Principle, we create code that is more robust and easier to test, as we can substitute objects in our code without breaking its functionality.

  1. Interface Segregation Principle (ISP): The Interface Segregation Principle states that clients should not be forced to depend on interfaces they do not use. In other words, we should design interfaces that are specific to the needs of their clients, rather than creating monolithic interfaces that contain all possible methods.

For example, if we have a class that generates reports, we should define an interface that includes only the methods needed by the classes that use the reports, rather than including all possible methods for generating reports. This makes the code more modular and easier to maintain, as changes to the interface do not affect unrelated classes.

By following the Interface Segregation Principle, we create code that is more flexible and easier to test, as we can define interfaces that are specific to the needs of their clients, rather than forcing clients to depend on interfaces they do not use.

  1. Dependency Inversion Principle (DIP): The Dependency Inversion Principle states that high-level modules should not depend on low-level modules, but both should depend on abstractions. In other words, we should design our code to depend on abstractions, rather than concrete implementations.

For example, if we have a class that needs to access a database, we should define an abstract interface that represents the database connection, rather than directly depending on a specific implementation of the database connection. This makes the code more modular and easier to test, as changes to the implementation of the database connection do not affect the rest of the code.

By following the Dependency Inversion Principle, we create code that is more flexible and easier to test, as we can change the implementation of a dependency without affecting the rest of the code.

Thank you for reading this.

Single Responsibility Principle

S in the SOLID principles represent single responsibility principle. SOLID is one the most praised set of principles developers should follow when they are working in OOP paradigm. As the name suggests every class should have one and only one responsibility to achieve, but why?

You can investigate following examples easier at my github page :

github link

public class SomeFinanceThing {
   String dbServerUrl = ...;
   String dbServerPass = ...;
   String dbName = "SomeFinanceThingDb";
   SomeDatabaseThing db;

   // Object specific fields
   ...
   ...

   public SomeFinanceThing(String dbServerUrl, ...){
      this.dbServerUrl = dbServerUrl;
      ...
      ...

      SomeConnectionObject conn = new SomeConnectionObject(dbServerUrl, dbServerPass);
      try{
        db = conn.connect(dbName);
      }
      catch(SomeDbConnException dbConnEx){
      // do some logging
      // return some feedback
      // etc
      application.quit();
      }
    }
    public void SaveSomeFinanceData(String data1, String data 2,...){
        SomeDbFriendlyObject dbFriendly = new Some...;
        db.save(dbFriendly);
    }
    public void DeleteSomeFinanceData();
    public SomeObject FindSomeFinanceData();
    // etc
}
public class SomeOtherFinanceThing {
   String dbServerUrl = ...;
   String dbServerPass = ...;
   String dbName = "SomeOtherFinanceThingDb";
   SomeDatabaseThing db;

   // Object specific fields
   ...
   ...

   public SomeOtherFinanceThing(String dbServerUrl, ...){
      this.dbServerUrl = dbServerUrl;
      ...
      ...

      SomeConnectionObject conn = new SomeConnectionObject(dbServerUrl, dbServerPass);
      try{
        db = conn.connect(dbName);
      }
      catch(SomeDbConnException dbConnEx){
      // do some logging
      // return some feedback
      // etc
      application.quit();
      }
    }
    public void SaveSomeFinanceData(String data1, String data 2,...){
        SomeDbFriendlyObject dbFriendly = new Some...;
        db.save(dbFriendly);
    }
    public void DeleteSomeFinanceData();
    public SomeObject FindSomeFinanceData();
    public SomeObject SomeOtherMethod();
    // etc
}

We handled database connection, we created a business object, converted it into a database object, write CRUD methods all the same time. Lets says we had 20 different objects we want to work with, we had to repeat same logic over and over again while we only cared about a few different fields. But thats not the only issue.

What if we want to change the server we are connecting to ? Then we would have to change 20 different classes, what if we decided to write backup copy to files on the disk, we would have to implement it in 20 different classes. What if we change the server password for some reason ? We would have to change it in all the class.

Every single change we want to achieve had to be replicated, every chance we might introduce a bug is also multiplied. If we want to change how we connect to a database we probably would have to rewrite our entire application.

public class DbConnectionHelper {
   String dbServerUrl = ...;
   String dbServerPass = ...;
   SomeConnection conn;

   public DbConnectionHelper(String dbServerUrl, ...){
      this.dbServerUrl = dbServerUrl;
      ...
      ...
      conn = new SomeConnectionObject(dbServerUrl, dbServerPass);
   }
   public Optional<SomeConnectionThing> getConnection(string dbName){
       return conn.connect(dbName);
   }
}
public class ObjectConverter<T, R> {
   SomeOtherHelperObject obj = ...;

   public ObjectConverter(...){
   ...
   }
   
   public R convert(T type){
   // some logic
   // some logic
   // some logic
   }
}
public interface CRUD<T> {
   public void save(T o);
   public void delete(T o);
   ...
}
public class FinanceObject1 {
    // fields

    // constructor
}
public class FinanceObject1Repository implements CRUD<FinanceObject1>{
   Optional<SomeConnectionThing> db;
   public FinanceObject1Repository(DbConnectionHelper helper, string dbName){
       db = helper.getConnection(dbName);
   }
   public void save(Data data1, Data data2, ...)
   {
       FinanceObject1 financeObject = new FinanceObject1(data1, data2, ...);
       ObjectConverter<FinanceObject1, DbFriendlyObject> converter = new Object Converter();
       db.save(converter.convert(financeObject));
   }
}

Much better already. Changes and bugs! are already confined within much smaller classes easier to handle, and code duplication reduced greatly. You don’t have repeat lots of code for each entity. But don’t think what i wrote above is anything optimal, you can make it better (and you should), you can create a generic repository and avoid rewriting CRUD methods for each repository.

But you should avoid creating classes for each CRUD method (create, read, update, delete), handling crud can be considered as a single responsibility because if you try to seperate those you will complicate your code more instead of simplifying. Rule of tumb is every class only needs one reason to be changed as stated in wikipedia by creator of SOLID principles Robert C. Martin.

Conclusion : by restricting each of our classes to have a single responsibility, we avoid duplicating code, exponentially increasing complexity, we reduce chance to introduce new bugs, make our code more readable and easier to extend and maintain all in once just by following a very simple principle.

Thank you for reading this.