Welcome to the next lesson in our journey through foundational design principles. In our previous discussion, we explored cohesion—the measure of how well the contents of a module belong together. We concluded that striving for high cohesion is a primary strategy for building maintainable software. Today, we will formalize this idea by studying its most direct application: the Single Responsibility Principle (SRP).
This lesson will equip you to apply SRP to decompose a system into focused, maintainable components. We will move beyond the simple mantra of "do one thing" to a more robust definition centered on "reasons to change." Through practical examples in both Python and Rust, including one from your domain of computer vision, you will see how adhering to this principle leads to code that is easier to understand, test, and evolve.
Defining the Single Responsibility Principle
The Single Responsibility Principle is the "S" in the SOLID acronym and is arguably the most fundamental of the five principles. It provides a clear guideline for how to achieve the high cohesion we discussed previously.
A common starting point is the idea that a function or class should "do one thing." While this is a useful heuristic, a more precise and powerful definition comes from Robert C. Martin ("Uncle Bob"):
"A module should have one, and only one, reason to change."
To understand what a "reason to change" is, we can rephrase the principle again:
"A module should be responsible to one, and only one, actor."
An "actor" is a group of users or stakeholders who require changes in the software. This could be the accounting department, the operations team, or even a different system that depends on your module. A module violates SRP if it serves multiple actors, because their needs may diverge, pulling the module in different directions.
Let's start by solidifying these definitions.
Single Responsibility Principle for Machine Learning Engineers: Theory and Practice
This video from "The Sound of AI" provides a clear and concise explanation of SRP, introducing the key concepts of functionality, reason to change, and actor.
Please watch the segment from 00:35 to 02:04. Pay close attention to how the definition evolves from "single functionality" to being centered on a single "actor."
When a single module, like a class, serves multiple actors, their independent requests for change create conflict. A change made for the accounting department might break functionality required by the human resources department. This tight coupling of different concerns within a single module leads to a fragile design.
Spotting SRP Violations
Identifying SRP violations is a critical skill. It requires you to look beyond what a class does and think about why it might need to change.
Single Responsibility Principle for Machine Learning Engineers: Theory and Practice
Continuing with the same video, let's learn how to detect SRP violations and understand their negative consequences.
Watch the segment from 03:01 to 04:26. The questions presented here are excellent diagnostic tools for reviewing your own code.
The video highlights the core problems of SRP violations: entangled code and "God Objects"—classes that know too much and do too much. Your experience with large-scale projects has likely shown you how such objects can become bottlenecks for development and sources of bugs.
Let's look at a concrete Rust example that perfectly illustrates this problem. We have an Employee struct that combines logic for four different actors.
SOLID Principles in Rust: A Practical Guide - 00 | 40tude
This article provides an excellent, practical walkthrough of SRP in Rust.
First, read the section that formally defines the principle, The Principle. Then, carefully study the code and the explanation in the section "The Problem: Accidental Coupling". Notice how the single Employee struct contains methods serving Accounting, Operations, DBAs, and HR.
This Employee struct is a classic SRP violation. The method for calculating pay serves the accounting department. The method for reporting hours serves operations. The persistence logic serves the database administrators. A change to the database schema (an infrastructure concern) requires modifying the same file that contains business logic for payroll calculation. This is precisely the kind of entanglement SRP helps us avoid.
Applying SRP: The Refactoring Process
Once a violation is identified, the solution is to decompose the component by separating the responsibilities. Each new component should be highly cohesive and serve only one actor.
Python: From Functions to Classes
SRP violations can happen at any scale. Let's start with a subtle but common violation within a single Python function.
Enforcing Single Responsibility Principle in Python - DEV Community
This article from dev.to provides a great analysis of a seemingly simple function.
Read the section beginning with the example of a calculate_price function. The author makes a compelling case for why mixing calculation with logging is a double responsibility. Pay attention to the "and, or, then" rule for function naming—it's a useful smell test.
As the article points out, the solution is composition. Instead of one function doing two things, we compose two functions that each do one thing. This could be done with decorators, callbacks, or other patterns, which we will explore more in later lessons on dependency injection.
Now, let's look at a class-level refactoring in Python.
Uncle Bob’s SOLID Principles Made Easy 🍀 - In Python!
This video from ArjanCodes (whom we saw in the last lesson) provides a clear, step-by-step refactoring of an Order class.
Watch the segment on the Single Responsibility Principle. Notice how the payment logic, which serves a different concern than managing order items, is extracted into a dedicated PaymentProcessor class.
By moving the payment logic out of the Order class, each class now has a single responsibility. The Order class is responsible for managing the state of an order (its items and total). The PaymentProcessor is responsible for the interaction with external payment systems. If you need to add a new payment method like Apple Pay, you only need to change the PaymentProcessor; the Order class remains untouched.
Rust: Separating Data and Behavior
Now, let's see the solution to the Employee problem in Rust. The refactoring strategy here is powerful: separate the data from the behavior. The behaviors are then grouped into new modules, each serving a single actor.
SOLID Principles in Rust: A Practical Guide - 00 | 40tude
Let's return to the Rust example and see how it's fixed.
Study the proposed refactoring in the "Solution" section. Observe how the Employee struct becomes a pure data container, and the logic is moved into separate modules like accounting::payroll and infrastructure::repository. Also, read the Rust-Specific Notes, which highlight how the language's features naturally support this kind of design.
This refactoring is a textbook example of applying SRP at an architectural level.
- The
domain::Employeestruct is just data. It has no behavior and is shared by all actors. PayrollCalculatorcontains the business logic for accounting.EmployeeRepositorycontains the infrastructure logic for persistence.EmployeeReportercontains the presentation logic for HR.
Now, a request from the accounting department to change overtime rules only affects the accounting module. A decision by the DBAs to switch to a NoSQL database only affects the infrastructure module. The responsibilities are cleanly separated, and the risk of one change inadvertently breaking another is dramatically reduced.
SRP in a Computer Vision Pipeline
Let's apply this principle directly to your field of interest. It's common in computer vision and ML projects for a single class to accumulate multiple responsibilities related to data handling, model execution, and results processing.
Single Responsibility Principle for Machine Learning Engineers: Theory and Practice
This video provides a perfect, practical example of applying SRP to a machine learning context.
First, watch the description of the problematic code from 04:33 to 05:47, where a single DLModel class handles preprocessing, training, and evaluation. Then, watch the analysis of the SRP violation from 05:47 to 07:35, which identifies the different actors involved. Finally, observe the refactoring process from 07:35 to 10:09, where the responsibilities are separated into distinct Preprocessor, DLModel, and DLEvaluator classes.
This example directly mirrors challenges in building industrial computer vision systems. Imagine a VideoFrameProcessor class. Initially, it might just run a single object detection model. Over time, it might grow to:
- Decode the video frame from a specific format.
- Preprocess the image (resize, normalize).
- Run an object detection model.
- Run a separate feature extraction model for object tracking.
- Perform object tracking logic.
- Annotate the frame with bounding boxes and tracking IDs.
- Encode the annotated frame for streaming.
This class now serves multiple actors and has many reasons to change. Applying SRP, you would decompose this into a pipeline of smaller, single-purpose components: FrameDecoder, ImagePreprocessor, ObjectDetector, ObjectTracker, FrameAnnotator, and FrameEncoder.

This decomposition allows you to swap out one object detector for another without touching the tracking or encoding logic. It makes each part of the pipeline independently testable and easier to reason about, which is critical for building robust and maintainable systems.
Conclusion
In this lesson, we established the Single Responsibility Principle as the primary tool for achieving high cohesion. By ensuring that a module is responsible to only one actor and has only one reason to change, we create software that is more resilient, adaptable, and maintainable.
Key Takeaways:
- SRP Definition: A module should be responsible to one, and only one, actor. This means it should have only one reason to change.
- Identifying Violations: Look for modules that serve multiple stakeholders or business functions. A class that handles business logic, persistence, and presentation in one place is a common violation. The question to ask is: "Who would request a change to this code, and why?" If there are multiple answers, you likely have an SRP violation.
- Applying SRP: Decompose large, multi-responsibility modules into smaller, highly cohesive ones. A common and effective pattern is to separate data structures from the behaviors that operate on them.
- Benefits: Adhering to SRP leads to a loosely coupled system where changes are localized, testing is simplified, and components are more reusable.
We have now seen how to create focused, single-purpose components. But what happens when we need to add new functionality? Should we modify these existing, stable components? Our next lesson will introduce the Open/Closed Principle (OCP), which provides a powerful strategy for extending the behavior of our system without changing existing code, building directly on the foundation we've laid with SRP.