OODP: Single Responsibility

Single Responsibility – A class should have one, and only one, reason to change.

The S in SOLID stands for Single Responsibility. This principle, in short, states that a class should have one, and only one, reason to change.

Why does having more than one responsibility matter? The simple answer is that more than one responsibility promotes low cohesion and allows for two or more different sets of logic to be accidentally broken and easily mixed by accident. This increases fragility which means the code will be hard to maintain and reuse.

Example 1: Simple example

Violation of SRP

Let’s take a look at this class below, PartList. Does it have any other responsibility other than managing a list of parts? If so, it breaks the Single Responsibility Principle.

PartList.java
public class PartList {
    private Map<String,Part> parts = new HashMap<>();

    public PartList(){
        loadParts();
    }

    public void addPart(Part part){
        parts.put(part.getPartNumber(),part);
    }

    public void removePart(String partNumber){
        parts.remove(partNumber);
    }

    public Map<String,Part> getParts(){
        return parts;
    }

    private  void loadParts(){
        String fileName = "c://parts.txt";

        //read file into stream, try-with-resources
        try (Stream<String> lines = Files.lines(Paths.get(fileName))) {

            lines.forEach(line -> {
                Part part = new Part(line);
                parts.put(part.getPartNumber(),part);
            });

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

The answer is yes. PartList.java does have two responsibilities. One is managing the business logic for maintaining a part list and the other is the data logic for reading parts from an external resource. If you look at the loadParts() method you can see that it has to know how to deal with reading data from a file system. This reading data from the file system is integration logic much like saving to a database and reading from a database.

What is wrong with this class having more than one responsibility? The basic idea is that it is less cohesive. It does not have high cohesion and has tight coupling between basic business logic managing a part list and data logic for saving and reading from a particular resource. It is possible to break one responsibility while changing the other. As the class grows, it will become even more likely. Imagine having to add logic to read the part’s list from different resources.

Take a look at the Part class that PartsList uses. It is very subtle and some might disagree, but I would argue that it also has broken SRP. Can you tell me what it is?

Part.java
public class Part {
    private String partNumber;
    private String description;

    public Part(String partLine){
       String[] partArray = partLine.split(",");
        partNumber = partArray[0];
        description = partArray[1];
    }

    public String getPartNumber() {
        return partNumber;
    }

    public String getDescription() {
        return description;
    }
}

As I mentioned, it is very subtle and questionable. Check out the constructor. The constructor takes in a string that is comma delimited and then splits it to create the part. This is an actual line from the file. While it is not tied to the file, it is tied to how a line is saved in the file. This act of interpreting a specific resource’s (the file resource) string format in itself breaks SRP. Think of it as serialization logic.

This one is debatable, but I think at the very least we can all agree that a string like this is not clean since it is not simple for the client of this class to know what the content of the string should be. I believe it fits into the category of violating SRP.

Adherence to SRP

Let’s look at how this can be written to not break SRP.

PartList.java
public class PartList {
    private Map<String, Part> parts;

    public PartList(PartsLoader partsLoader){
        this.parts = partsLoader.loadParts();
    }

    public void addPart(Part part){
        parts.put(part.getPartNumber(),part);
    }

    public void removePart(String partNumber){
        parts.remove(partNumber);
    }

    public Map<String,Part> getParts(){
        return parts;
    }
}
PartsLoader.java
public interface PartsLoader {
    Map<String,Part> loadParts();
}
CommaDelimitedFilePartsLoader.java
public class CommaDelimitedFilePartsLoader implements PartsLoader{
    @Override
    public Map<String, Part> loadParts() {
        String fileName = "c://parts.txt";
        Map<String, Part> parts = new HashMap<>();
        //read file into stream, try-with-resources
        try (Stream<String> lines = Files.lines(Paths.get(fileName))) {

            lines.forEach(line -> {
                Part part = createPart(line);
                parts.put(part.getPartNumber(),part);
            });

        } catch (IOException e) {
           throw new RuntimeException("Error reading parts file",e);
        }
        return parts;
    }

    private Part createPart(String partLine){
        String[] partArray = partLine.split(",");
        return new Part(partArray[0],partArray[1]);
    }
}
Part.java
public class Part {
    private String partNumber;
    private String description;

    public Part(String partNumber, String description){
        this.partNumber = partNumber;
        this.description = description;
    }

    public String getPartNumber() {
        return partNumber;
    }

    public String getDescription() {
        return description;
    }
}

The code above is much cleaner. The Part class does not have any logic around translating a comma delimited line. The PartsList class is now dependent on an interface PartsLoader, and all of the logic around loading parts is encapsulated into its own cohesive CommaDelimitedFilePartsLoader concrete class.

Example 2: Not so simple

SRP has often been quoted as being one of the most simple of principles but also the most violated. Why?

Bob mentioned he believed that we failed at this because engineers are only focused on making sure the code works, and they spend little time on making sure it is organized correctly. I agree, but I believe it is also because we are not practiced in finding the appropriate level of abstraction for where the Single Responsibility Rule should apply. Take a look at the next example.

Violation of SRP

Order.java

public class Order {
    private String orderNumber;
    private Date orderDate;
    private String orderStatus;
    private String customerNumber;
    private String firstName;
    private String lastName;
    private String phoneNumber;
    private String billingAddressLine1;
    private String billingAddressLine2;
    private String billingAddressLine3;
    private String addressLine1;
    private String addressLine2;
    private String addressLine3;
    private String deliverySpecialInstructions;
    private int creditCardNumber;
    private Date creditCardExpireDate;
    private int credicardCID;

    private Map<String, LineItem> lineItems = new HashMap<>();

    public void addLineItem(LineItem newLineItem){
        if(lineItems.containsKey(newLineItem.getSKU())){
           LineItem item = lineItems.get(newLineItem.getSKU());
            item.setQuantity(item.getQuantity() + newLineItem.getQuantity());
        }else{
            lineItems.put(newLineItem.getSKU(),newLineItem);
        }
    }

    public double retrieveTotalCost(){
        double total =0;
        for(LineItem lineItem : lineItems.values()){
            total = total + (lineItem.getPrice() * lineItem.getQuantity());
        }
        return total;
    }
}
LineItem.java
public class LineItem{
    private int quantity;
    private String SKU;
    private double price;
}

I have removed the Setters and Getters in this example to save space. To be quite honest, I do not write them anymore. I use Lombok for it. If you use Java and you have never heard of Lombok, you are missing out. Google it!

This order class looks clean at first glance. It seems cohesive. Every attribute that makes up an order is in the class. It is all encapsulated in one class. From the perspective of providing an interface to calling classes, the level of abstraction may appear at first glance to be somewhat correct. The classes single responsibility is managing order data and business rules. If we dig deeper into the classes interface or internal logic, we will see that the SRP applied at this higher level of abstraction is not correct. We are trying to hide everything about an order behind a single interface. We need to drop the abstraction down a level and expose some of the data and logic that makes up an order into smaller cohesive abstractions.

I might decide to add business logic around the order payment information or the customer’s shipping information. Should these two sets of attributes be in the same class, they are different responsibilities and changing one should not accidentally affect the other. If one changes, does the other need to change also, would you change both at the same time. I believe the answer is no, not necessarily.

Let’s create a different set of objects at a lower level that will be used as abstractions around these attributes and their upcoming business logic.

Order.java
public class Order {
    private String orderNumber;
    private Date orderDate;
    private String orderStatus;
    private Payment paymentInformation;
    private Customer customer;
    private ShippingInformation shippingInformation;
    private Map<String, LineItem> lineItems = new HashMap<>();

    public void addLineItem(LineItem newLineItem){
        if(lineItems.containsKey(newLineItem.getSKU())){
           LineItem item = lineItems.get(newLineItem.getSKU());
            item.setQuantity(item.getQuantity() + newLineItem.getQuantity());
        }else{
            lineItems.put(newLineItem.getSKU(),newLineItem);
        }
    }

    public double retrieveTotalCost(){
        double total =0;
        for(LineItem lineItem : lineItems.values()){
            total = total + (lineItem.getPrice() * lineItem.getQuantity());
        }
        return total;
    }
}
Payment.java
public class Payment{
    private int creditCardNumber;
    private Date creditCardExpireDate;
    private int credicardCID;
    private Address billingAddress;
}
Customer.java
public class Customer{
    private String customerNumber;
    private String firstName;
    private String lastName;
    private String phoneNumber;
}
ShippingInformation.java
public class ShippingInformation{
    private Address shippingAddress;
    private String deliverySpecialInstructions;
}
Address.java
public class Address{
    private String addressLine1;
    private String addressLine2;
    private String addressLine3;
}

This is much better, should the caller of this API decide to access or change the billing information, they will not accidentally change the customer’s information. Should any new logic need to be added to these different responsibilities they should not affect each other. These different responsibilities are loosely coupled now.

Take a look at the order class now. Specifically, look at line items and total cost logic. What is wrong with this? The other responsibilities are now encapsulated at a different level, but the line items and the total cost is still at the higher level. Let’s fix this.

Order.java
public class Order {
    private String orderNumber;
    private Date orderDate;
    private String orderStatus;
    private Payment paymentInformation;
    private Customer customer;
    private ShippingInformation shippingInformation;
    private LineItems lineItems;
    
    public void addLineItem(LineItem lineItem){
      lineItems.add(lineItem);
    }

    public double retrieveTotalCost(){
      //TODO: Use lineItems total cost, shipping information, tax logic, calculate total cost.
      return lineItems.retrieveLineItemsCost();
    }
}
LineItems.java
public class LineItems{
    private Map<String, LineItem> entries = new HashMap<>();

    public void add(LineItem newLineItem){
        if(entries.containsKey(newLineItem.getSKU())){
           LineItem item = entries.get(newLineItem.getSKU());
            item.setQty(item.getQty() + newLineItem.getQty());
        }else{
            entries.put(newLineItem.getSKU(),newLineItem);
        }
    }

    public double retrieveLineItemsCost(){
        double total =0;
        for(LineItem lineItem : entries.values()){
            total = total + lineItem.calculateTotalPrice();
        }
        return total;
    }
}
LineItem.java
public class LineItem{
    private int quantity;
    private String SKU;
    private double price;
   
    public double calculateTotalPrice(){
       return quantity * price;
    }
}

The addLineItem logic has now moved to a class that is called LineItems.The calculation for a line item’s quantity times price has now moved into the line item class where it belongs. The logic to loop through the line items list adding the cost of each line item has now moved into LineItems class where it belongs. The Order class calls LineItems to get the line items total cost and, in the near future, will call other strategies to calculate tax on that total line items cost to produce a total order cost. Notice now how there are more objects and each object not only has properties but also has behavior. A lot of engineers would tend to put all of this logic in a coordinating service class producing Transaction Script, or they would put it all into the Order class. Both of those styles breaks SRP and tightly couples logic that should not be.

The previous set of classes now follow the SRP principle and the responsibilities are loosely coupled. I did this by following Bob’s excellent instructions in his book. Take a given class, look at the attributes of that class and see if they all fall within a single responsibility, if not refactor those attributes into other classes. Read his book for a better explanation. It is a good read.

In Closing…

As we write more cohesive code that handles only one responsibility, we will increase the number of classes. We will start writing smaller classes. However, the code should be easier to maintain, extend and understand. Maintainability and extensibility will be increased. Your code will become less brittle.

“The first rule of classes is that they should be small. The second rule of classes is that they should be smaller than that.” – Quote from Bob Martin [Clean Code Handbook]

I know a lot of developers may disagree that more classes are better. As with everything I do believe there is a balance. I believe Bob gives an excellent description of why having more classes is not bad.

“… many developers fear that a large number of small, single purpose classes makes it more difficult to understand the bigger picture. They are concerned that they must navigate from class to class to figure out how a large piece of work gets accomplished.

However, a system with many small classes has NO MORE moving parts than a system with a few large classes. There is just as much to learn in a system with a few large classes. So the QUESTION IS: Do you want your tools organized into toolboxes with many small drawers each containing well-defined and well-labeled components? Or do you want a few drawers that you just toss everything into?” – Quote from Bob Martin [Clean Code Handbook]

I would rather have the well-labeled and well-defined components.

If you made it this far then hopefully, you found the post to be helpful. In either case, drop me a line, tell me what you think. Would love to hear back from you. Thanks for checking out the post.

Leave a Reply