Single Purpose Principle

Introduction

Single Responsibility Principle is a confusing term because Rebecca Wirfsbrock came up with the term “Responsibility” to mean a different thing when she was talking about Responsibility Driven Design. The term responsibility is overloaded because the SRP now is using the same term to mean a different thing.

You’re correct that terminology can sometimes become overloaded and potentially cause confusion, especially in a field as wide-ranging as software development. Here’s an attempt to clarify the two usages you mentioned:

  1. Responsibility-Driven Design (RDD): Rebecca Wirfs-Brock’s use of “responsibility” in RDD is related to object-oriented design. In this context, a responsibility refers to a contract or obligation of a class or an object. For example, an object of class Circle has the responsibility to know its radius, and it has the responsibility to calculate its area when asked. Responsibilities are realized through methods or operations of the class. So, in RDD, “responsibility” is about what a class or an object does.

  2. Single Responsibility Principle (SRP): The term “responsibility” in the context of the Single Responsibility Principle, one of the five principles of SOLID, is often interpreted as a “reason to change”. SRP states that a class or module should have only one reason to change, meaning it should encompass a single aspect of the system’s functionality.

So, while both terms use “responsibility”, they’re used in slightly different contexts. In RDD, a responsibility is a behavior or knowledge an object encapsulates, while in SRP, a responsibility is a specific aspect of functionality that a class or module encapsulates.

It’s important to look at these principles as guidelines, not hard rules. They are tools to help design more maintainable and understandable software, but they may not apply perfectly to every situation. The key is to understand the principles well enough to know when to apply them and when to make exceptions.

The single-responsibility principle (SRP) is a computer programming principle that states that every module, class, or function should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by the class.

Applying the SRP leads to smaller, simpler classes, each focused on a single task. While this can indeed increase the overall complexity of the code base due to the increased number of classes, it also has several benefits:

  • Each class is easier to understand and maintain because its functionality is limited and specific.
  • Each class is easier to test because it has fewer cases to cover.
  • The code is more flexible and easier to change because changes in one class don’t affect others.

However, if the principle is applied excessively, it can result in a system with an overwhelming number of classes, each performing a trivial task. As a result, it becomes difficult to understand how these classes interact to achieve larger functionalities. The principle should be applied judiciously, considering the context and requirements.

Let’s look at a simplified example. Imagine a UserManager class in a system:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class UserManager {
    constructor(users) {
        this.users = users;
    }

    add(user) {
        this.users.push(user);
    }

    remove(user) {
        let index = this.users.indexOf(user);
        this.users.splice(index, 1);
    }

    notify(user) {
        console.log(`Hello, ${user}!`);
    }
}

In this case, UserManager is responsible for managing users, which includes adding, removing, and notifying users. This is a violation of the SRP because these responsibilities belong to different domains: user management and user notification.

We could refactor this into two classes, each with a single responsibility:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class UserManager {
    constructor(users) {
        this.users = users;
    }

    add(user) {
        this.users.push(user);
    }

    remove(user) {
        let index = this.users.indexOf(user);
        this.users.splice(index, 1);
    }
}

class UserNotifier {
    notify(user) {
        console.log(`Hello, ${user}!`);
    }
}

Now UserManager is only responsible for managing users, and UserNotifier is only responsible for notifying users. Each class is simpler and adheres to the SRP.

In conclusion, while it’s important to strive for single responsibilities in your classes to promote cohesion and reduce coupling, balance is essential. The number of classes and the degree of interaction between them should be managed to keep the complexity of the system under control.

Benefits

One of the key benefits of applying the Single Responsibility Principle (SRP) and splitting classes into more focused and specific components is the enhanced reusability and replaceability.

  1. Reusability

By making each class have a single responsibility, we make it more likely that the class can be reused in different parts of our application. This is because a class with a single, well-defined role is more likely to fit in different contexts than a class that tries to do too many things.

Consider the example from before where we had a UserNotifier class. This class could be used anywhere in your application where user notification is needed, not just in conjunction with the UserManager.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class OrderManager {
    constructor(userNotifier) {
        this.userNotifier = userNotifier;
    }

    placeOrder(user, order) {
        // Logic for placing the order
        this.userNotifier.notify(user);
    }
}

In the above example, we use UserNotifier inside OrderManager to notify the user after placing an order. This shows the reusability of the UserNotifier class.

  1. Replaceability

Having classes with a single responsibility also makes it easier to swap out certain parts with alternative implementations. This is particularly important when we need to change the behavior of our system or when writing tests.

Continuing with our UserNotifier example, imagine we want to change our notification method. Instead of notifying users through console logs, we want to send emails. We could easily create a new EmailUserNotifier class and substitute it for our UserNotifier.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class EmailUserNotifier {
    notify(user) {
        // Code to send email
    }
}

// In the part of our code where we instantiate objects
let notifier = new EmailUserNotifier();
let userManager = new UserManager(users);
let orderManager = new OrderManager(notifier);

Here, we’ve replaced the console notifier with an email notifier without having to change the UserManager or OrderManager classes. This kind of modularity and replaceability is a huge benefit of adhering to the SRP.

Single Purpose Principle

Is it better to say Single Purpose Principle instead of Single Responsibility Principle?

The term “Single Responsibility Principle” (SRP) is quite ingrained in the software development industry, being one of the SOLID principles of object-oriented design, so changing it could cause confusion.

However, your suggestion does offer a certain clarity, especially for those new to the principle. “Single Purpose Principle” might intuitively suggest that a module or class should have only one job or purpose, which is essentially what SRP aims to convey.

Regardless of the term used, the underlying concept remains the same. A class or module should be responsible for only one part of the software’s functionality, and thus have only one reason to change. This helps create more maintainable, understandable code by reducing complexity and limiting the impact of future changes.

Coding Construct

If a coding construct has one purpose, it will map directly to one reason to change. Is this logically correct?

Yes, it’s generally accurate. If a coding construct serves a single purpose, changes in requirements should ideally affect only that one construct if they are specifically related to its purpose.

However, keep in mind that in practice, this doesn’t always guarantee isolation from other changes. For instance, a change in a shared interface, a common data format, or the way modules interact could still cause a construct to change even if its primary “purpose” remains the same.

Also, a module might be responsible for a single concept in the system (a single purpose), but that concept could itself be complex and multifaceted, leading to many potential reasons for change within that context.

So while the concept of “single purpose” can help reduce the scope of potential changes, it doesn’t entirely eliminate the reality that many factors can lead to needing changes in a piece of code.

Let’s consider a simple example. Suppose you have an e-commerce application.

  1. Let’s say you have a Product object, and its sole purpose is to hold information about a product — its name, price, description, and so forth. If there’s a change in the way your business handles product information — for example, if you decide to start tracking the weight of each product — you’d need to change the Product object. That’s because this change directly relates to its purpose. This aligns with the idea of a single purpose leading to a single reason to change.

  2. However, consider a situation where you have an Order object that calculates the total order price. Suppose it uses product prices and shipping costs. Now, this object has a single purpose - to calculate and provide the total order price. However, changes to the pricing structure of the products or changes to the way shipping costs are calculated (which could happen for a variety of business or logistical reasons) would require changes to the Order object. Here, the Order object has a single purpose, but multiple potential reasons to change due to its dependencies.

  3. Similarly, consider a ShippingCalculator object whose purpose is to calculate shipping costs based on distance and package weight. If there is a change in the pricing policy of your courier service, you would need to change this object. However, if you also decide to account for package dimensions (height, width, length), you would also need to change the ShippingCalculator, even though its purpose is the same.

In conclusion, while designing each module with a single purpose can indeed limit the scope of potential changes, it doesn’t completely isolate modules from changes due to dependencies or due to multifaceted nature of some concepts.

Likelihood of Change

It depends on how likely something is going to change. If it is more likely, then the classes must be packaged such that the changes are within that package and does not affect the rest of the system. So the boundary within which we apply the Single Purpose Principle expands.

Exactly! You’ve touched upon a critical aspect of system design: defining boundaries.

A design principle like the Single Purpose Principle is a heuristic, not an absolute law. It should be interpreted within the context of your system and applied where it makes the most sense. Sometimes, these boundaries are defined at a very granular level, such as individual functions or classes. But in other cases, it might make more sense to think in terms of larger units, like modules or packages, especially when you’re dealing with complex systems where related behaviors are tightly coupled.

The boundary you choose for applying the Single Purpose Principle can indeed expand based on the likelihood of change and the nature of the system you’re working with. The goal is to minimize the impact of changes, and the best way to achieve that can vary. For instance, if you predict that two pieces of functionality are likely to change together, it may make sense to group them together, even if they serve different purposes.

The art of software design often lies in making these kinds of judgments, and it’s something that gets easier with experience and a deep understanding of the problem domain. There’s rarely a one-size-fits-all answer, and the best solutions often involve a degree of compromise and balance between competing concerns.

Identify Boundaries

How do we identify these boundaries in an application?

Identifying boundaries in an application is more of an art than a science, and it relies heavily on your understanding of the domain, the software system, and your expectations about how the system might evolve over time. Here are some strategies to identify boundaries:

  1. Domain-Driven Design (DDD): DDD is an approach to software development that emphasizes the importance of understanding the problem domain and modeling your software based on that understanding. In DDD, you identify aggregates, which are clusters of domain objects that can be treated as a single unit. These aggregates often serve as good boundaries.

  2. Separation of Concerns: This principle suggests that you should organize your software so that each part deals with a specific aspect and doesn’t have to worry about the details of other aspects. This could be based on technical concerns (like separating the UI layer from the business logic), or it could be based on domain concepts.

  3. Cohesion and Coupling: High cohesion and low coupling is a general principle of software design that also applies to defining boundaries. Cohesion refers to how closely the responsibilities of a module or component are related to each other. Coupling refers to how much a component depends on other components. You would want to group together things that change together (high cohesion) and aim for components that are as independent as possible (low coupling).

  4. Common Closure Principle: This is one of the principles of the SOLID design principles. It states that you should group together things that change together. In other words, if you expect multiple parts of your system to be affected by the same kinds of changes, it makes sense to consider them as a single unit for the purpose of your design.

  5. Business Processes: Often, business processes, transactions or workflows naturally define boundaries. They encompass a sequence of steps or operations that achieve a specific business outcome.

  6. Data/State Ownership: Data can often serve as a natural boundary. If a particular component or set of components is responsible for owning and managing a certain piece of data, it makes sense to treat that as a boundary.

  7. Conway’s Law: Conway’s law suggests that the way your organization is structured will be mirrored in your software. This can sometimes (not always) be a helpful way to identify boundaries.

Ultimately, you’re trying to carve up your system in a way that isolates changes and minimizes the ripple effects of those changes. It’s often not easy to get this right, and it’s common to have to refactor your boundaries as you gain a deeper understanding of the problem domain and how your software needs to evolve.

What is Concern?

Concern can seem like a vague term, but in software engineering, it refers to a specific area of interest or focus within a system or application.

Generally, a concern is a piece of interest or focus in a program. Typically, concerns are synonymous with features or functionality (logging, data validation, error handling, etc.), which can be used to break up a program into modular components.

For instance, in web development, some common areas of concern include:

  1. User interface (UI): The part of the application the user interacts with. This includes everything related to the display of information to the user and the user interface controls the user can interact with.

  2. Business logic: The rules and processes that define how the system behaves, or how the data is processed. This includes all the code that represents the real-world rules that determine how data can be created, displayed, stored, and changed.

  3. Data access and storage: How the application interacts with the database or other forms of data storage.

  4. Application infrastructure: Concerns that are not specific to the business domain but are required for the application to run, such as logging, configuration management, security, caching, etc.

In the context of “separation of concerns”, each of these areas would be kept as separate as possible to make the codebase easier to manage and understand.

So, in essence, a “concern” refers to a distinct aspect or functional area within an application or system. The idea of identifying these concerns is key to the design principle of Separation of Concerns, where each part of a software system should have a distinct responsibility or “concern”.