Separation of Concerns (SoC) and Single Responsibility Principle (SRP) are two software design principles that are applied in order to make software maintainable, extensible and testable. This definition can be used for most design patterns and principles, but let’s find out what these principles are really about and how they can help in a real project.
The goal of this tutorial is not to explain these principles in details, but to show a really simple usage of them in order to maintain a good software structure. This topic has been discussed and explained a lot of time, so you can find a lot of articles and conference talks to dive deeper.
Introduction
First and foremost, these principles are closely related to each other and sometimes it can be difficult to distinguish one from another, but nevertheless these principles are not the same. I will try to define these principles as I understand them.
Separation of Concerns (SoC) — is about a dividing a software system into smaller modules, each on these modules is responsible for a single concern. A concern, in this case, is a feature or a use case of a software system. A module has a well-defined API (interface) as a result making a whole system highly cohesive. There are two major types: horizontal and vertical.
Single Responsibility Principle (SRP) — is a design principle that states that each building block (it can be a class, a module, an object or even a function) of a system should have only a single responsibility. Robert C. Martin. Martin describes a responsibility as a reason to change. In general, it is much better to have a single class/object that has responsibility over a single part of the functionality instead of being able to perform a lot of, sometimes even unrelated, functions, making this class large and tightly coupled, so-called “God object”.
I have mentioned the Separation of Concerns (SoC) principle in the first place because it has broader meaning and often used on the higher level than the Single Responsibility Principle (SRP). In the best case scenario a system should be divided into modules that in turn are made from logical units (classes, objects, functions, etc.), that have only a single responsibility and therefore a one reason to change.
Examples
There are a lot of examples could be found, but all these great software design principles are borrowed from real life experience. Just remember where did the concept of “design pattern” firstly originate from ? Initially it was proposed by Christopher Alexander, a practicing architect, builder, and Emeritus Professor of Architecture at the University of California. He described and explained more than 200 patterns for building various environments from towns to kitchens in his book A Pattern Language.
Real-world examples
Real world examples of the Separation Of Concerns principle could be:
- Different departments of a software development company (HR , Finance, Quality Assurance, Engineering, etc.)
- Organizational Structure of a Hospital (Information services, Therapeutic services, Diagnostic services, etc.)
Sometimes it may be useful to combine several concerns into a single thing, as an example the digital camera of a smartphone. Nowadays smartphones have great cameras that allow to take spectacular shots. But on the other hand sales of digital cameras (not DSLRs) have fallen dramatically. In this case we are speaking not about the hardware parts, but rather about ability to make calls and taking pictures.
Single Responsibility Principle (SRP) is a little narrower term. And in real life it could be violated very often, for instance a person doing several unrelated jobs.
- A doctor is only responsible for a specific system of the human body or a specific medical area.
- Different types of cutlery (forks, knifes, spoons, etc.)
- Stationery
Just like the former principle, SRP also can be sometimes violated for the sake of usability, size, etc. (have a look at the featured image for this post).
Technical examples
There are a lot more technical examples, here are a few of them:
Separation of Concerns (SoC)
- Microservices
- The Internet Protocol Stack
- HTML, CSS and Javascript
- Separating a software into separate layers vertically or horizontally
Single Responsibility Principle (SRP)
- A class doing one specific thing however it could also be responsible for several related tasks, like reading and writing to a file.
- Avoiding God classes
- Inversion of control
Again, there always could be some exceptions. They are mostly caused by more important and strict requirements for a software application. A great example is BusyBox. BusyBox is a single executable file that provides optimized versions of common Unix tools, but keeping in mind the size of the executable file and performance.
Using Separation of Concerns and Single Responsibility principles in Android Development
Sorry for such long introduction, I hope it could be useful for understanding the principles. Now let’s solve a simple problem in an Android project. There are no clear boundaries between these two principles in this example, both of them are used.
The problem
The definition of the problem is quite simple. An application has different modes for working out and fitness. For example running, swimming, cycling and others. Each of these modes has different settings, that a user can adjust personally for himself/herself.
Under the hood all settings are stored in SharedPreferences, but this is not a strict rule of course and the persistence mechanism should be replaceable without any consequences or additional changes to the business layer.
Defining SettingsManager interface
Now let’s get started. At first we need to define an interface containing all required method for managing settings.
1 2 3 4 5 6 7 | public interface SettingsManager {
public SettingsBundle getSettingsBundle(String key, int mode);
public void saveSettingsBundle(String key, SettingsBundle bundle);
public SettingsBundle getSettingsBundle(String key);
}
|
As you can see above SettingsManager returns an instance of SettingsBundle. This will class will be defined in a while.
Implementing the SettingsManager interface
Next, let’s implement the SettingsManager interface specifically for Android.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | public class AndroidSettingsManager implements SettingsManager {
private Context mContext;
public AndroidSettingsManager(Context context) {
mContext = context;
}
@Override
public SettingsBundle getSettingsBundle(String key, int mode) {
return new AndroidSettingsBundle(mContext.getSharedPreferences(key, mode));
}
@Override
public SettingsBundle getSettingsBundle(String key) {
return this.getSettingsBundle(key, Context.MODE_PRIVATE);
}
@Override
public void saveSettingsBundle(String key, SettingsBundle bundle) {
// Persistent is handled by the bundle in this case
}
}
|
Defining SettingsBundle interface
This interface is used as an abstraction of related settings and contains methods required for accessing different types(primitives) of settings. In case of this example the implementation of the SettingsBundle interface stores settings for a single mode. In addition this also could be useful to separate different settings instead of writing everything to a single file.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | public interface SettingsBundle {
public void setIntSetting(String key, int value);
public void setBooleanSetting(String key, boolean value);
public void setLongSetting(String key, long value);
public void setFloatSetting(String key, float value);
public void setStringSetting(String key, String value);
public int getIntSettings(String key);
public long getLongSetting(String key);
public float getFloatSetting(String key);
public String getStringSetting(String key);
public boolean getBooleanSetting(String key);
}
|
Implementing the SettingsBundle interface
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | public class AndroidSettingsBundle implements SettingsBundle {
private static class DefaultSettingsValue {
public static final int INTEGER = 0;
public static final long LONG = 0L;
public static final String STRING = "";
public static final float FLOAT = 0.0f;
public static final boolean BOOLEAN = false;
}
private SharedPreferences mSharedPreferences;
public AndroidSettingsBundle(SharedPreferences sharedPreferences) {
mSharedPreferences = sharedPreferences;
}
@Override
public void setIntSetting(String key, int value) {
mSharedPreferences.edit().putInt(key, value).apply();
}
@Override
public void setBooleanSetting(String key, boolean value) {
mSharedPreferences.edit().putBoolean(key, value).apply();
}
@Override
public void setLongSetting(String key, long value) {
mSharedPreferences.edit().putLong(key, value).apply();
}
@Override
public void setFloatSetting(String key, float value) {
mSharedPreferences.edit().putFloat(key, value).apply();
}
@Override
public void setStringSetting(String key, String value) {
mSharedPreferences.edit().putString(key, value).apply();
}
@Override
public int getIntSettings(String key) {
return mSharedPreferences.getInt(key, DefaultSettingsValue.INTEGER);
}
@Override
public long getLongSetting(String key) {
return mSharedPreferences.getLong(key, DefaultSettingsValue.LONG);
}
@Override
public float getFloatSetting(String key) {
return mSharedPreferences.getFloat(key, DefaultSettingsValue.FLOAT);
}
@Override
public String getStringSetting(String key) {
return mSharedPreferences.getString(key, DefaultSettingsValue.STRING);
}
@Override
public boolean getBooleanSetting(String key) {
return mSharedPreferences.getBoolean(key, DefaultSettingsValue.BOOLEAN);
}
}
|
Important note
The implementation doesn’t follow all rules and principles. A better implementation way is to have a data structure internally that contains all settings. And then the SettingsManager instance would serialize all added settings and save them to a persistent storage. However, because of the API provided by Android Framework and for the sake of simplicity I will use this implementation.
That’s why we have defined interfaces to abstract required functionality. And we can easily reimplement classes without changing interfaces.
Creating a use case
Now we need to create a use case. It will be RunningModeSettingsUseCase. The term “use case” comes from the Clean Architecture proposed by Uncle Bob.
1 2 3 4 | public interface RunningModeSettingsUseCaseContract extends BaseUseCase {
public Observable<RunSettingsEntity> getSettings();
public void saveSettings(RunSettingsEntity settings);
}
|
Important note
Ideally a use case should be an atomic part of functionality and have an input/output interface and only a single method to execute or perform the use case. But in this case I will combine two use cases into single one as this requires more classes and interfaces. Separately they could be named as “GetRunningModeSettingsUseCase” and “SaveRunningModeSettingsUseCase”.
Next, we need to implement the use case interface. There are several possible ways to do this.
First attempt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public class RunningModeSettingsUseCase implements RunningModeSettingsUseCaseContract {
private final SettingsManager mSettingsManager;
public RunningModeSettingsUseCase(SettingsManager settingsManager) {
mSettingsManager = settingsManager;
}
//@Override
public Observable<RunSettingsEntity> getSettings() {
SettingsBundle settingsBundle = mSettingsManager.getSettingsBundle("RunningSettings");
// Somehow convert to the required class
return settingsObservable;
}
// @Override
public void saveSettings(RunSettingsEntity settings) {
SettingsBundle settingsBundle = // Somehow convert ;
mSettingsManager.saveSettingsBundle("RunningSettings",settings);
}
}
|
The implementation above has a lot of drawbacks. We need ask ourselves several questions:
- We are using directly the SettingsManager implementation. Do we really need all functionality provided by the SettingManager instance to solve the problem ?
- Do we need to know about low level details in the use case layer ?
- What will happen if we change the definition of the SettingsManager interface ? Won’t it cause any changes in the use case layer ?
- Is is possible to affect another system part through making changes via the SettingsManager interface ?
The problem here that we don’t need everything defined in the SettingsManager interface furthermore it is related to the lower layer. In addition we can erase other modes settings, for example.
The Repository pattern
Now we are going to get rid of this dependency. First of all define the repository interface that should contain only the methods required by the use case or related use cases.
1 2 3 4 | public interface RunningModeSettingsRepositoryContract {
Observable<RunSettingsEntity> getRunSettings();
void saveRunSettings(RunSettingsEntity settingsEntity);
}
|
Let’s implement the defined interface like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | public class RunningModeSettingsRepository implements RunningModeSettingsRepositoryContract {
// Constants
private static final String RUNNING_MODE_SETTINGS_KEY = "modes_settings.running";
// Variables
private SettingsManager mSettingsManager;
private BaseMapper<RunSettingsModel, RunSettingsEntity> mSettingsMapper;
private BaseMapper<RunSettingsEntity, RunSettingsModel> mSettingsMapperReverse;
public RunningModeSettingsRepository(SettingsManager settingsManager, BaseMapper<RunSettingsModel, RunSettingsEntity> settingsMapper,
BaseMapper<RunSettingsEntity, RunSettingsModel> reverseMapper) {
mSettingsManager = settingsManager;
mSettingsMapper = settingsMapper;
mSettingsMapperReverse = reverseMapper;
}
@Override
public Observable<RunSettingsEntity> getRunSettings() {
SettingsBundle experimentsSettingsBundle = mSettingsManager.getSettingsBundle(RUNNING_MODE_SETTINGS_KEY);
RunSettingsModel settingsModel = new RunSettingsModel(experimentsSettingsBundle);
return Observable.just(settingsModel).map(mSettingsMapper);
}
@Override
public void saveRunSettings(RunSettingsEntity settingsEntity) {
SettingsBundle experimentsSettingsBundle = mSettingsManager.getSettingsBundle(RUNNING_MODE_SETTINGS_KEY);
RunSettingsModel settingsModel = mSettingsMapperReverse.transform(settingsEntity);
settingsModel.saveStateToBundle(experimentsSettingsBundle);
mSettingsManager.saveSettingsBundle(RUNNING_MODE_SETTINGS_KEY, experimentsSettingsBundle);
}
}
|
As a result we have used composition and implemented all low level details in the repository class. It could be divided into more classes and this implementation is far from an ideal one. There are lot of thing that could be improved.
You may also wonder if the repository class is the right place for performing mapping between entities and data objects. This question is not trivial and I have my own thoughts about it, but in case of the Clean Architecture this approach doesn’t violates the dependency rule. The reason is that repositories are located on the outermost layer and therefore if this was implemented vice versa, the use case layer would have the dependency pointing outwards.
Refactoring the use case class
Finally refactor the RunningModeSettingsUseCase class as follows.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public class RunningModeSettingsUseCase implements RunningModeSettingsUseCaseContract {
private RunningModeSettingsRepositoryContract mSettingsRepository;
public PASettingsUseCase(RunningModeSettingsRepositoryContract settingsRepository) {
mSettingsRepository = settingsRepository;
}
@Override
public Observable<RunSettingsEntity> getRunSettings() {
return mSettingsRepository.getRunSettings();
}
@Override
public void saveRunettings(RunSettingsEntity settingsEntity) {
mSettingsRepository.saveRunSettings(settingsEntity);
}
}
|
Results
As a result let’s define benefits we have with such implementation:
- We can change the delivery mechanism at any time and this won’t affect the domain layer. For example our settings could be stored in memory or fetched over the network.
- It is not possible to make undesired changes to the data store. The reason for that is the expressly defined repository interface.
- The Dependency rule is not violated.
- Each of the defined classes has a Single Responsibility.
Conclusion
This tutorial is not intended to describe the only one possible right way, but rather to provide an example, share my thoughts and ideas. In addition I’ve tried to explain these important principles Separation of Concerns (SoC) and Single Responsibility Principle (SRP) using real world examples. Hopefully, you found something useful in the post.
I would be grateful for sharing your thoughts. You may not agree with described above or have another understanding hence advice, criticism, comments or suggestions are highly appreciated.
Recent posts
- Mar 1, 2020 Implementing Laravel custom Auth Guard and Provider
- Feb 16, 2019 Hacking Java Applications with Byte Buddy and Decompilers
- Jan 5, 2019 Page Specific Dynamic Angular Components using Child Routes
- Oct 13, 2018 Understanding Dagger 2 Scopes Under The Hood
- Jul 21, 2018 Understanding and using Xdebug with PHPStorm and Magento remotely
Popular posts
- 139702 Views How to Install The Latest Apache Server (httpd) on Centos 7
- 101053 Views Routing network traffic through a transparent SOCKS5 proxy using DD-WRT
- 72335 Views How to Unbrick TP-Link WiFi Router WR841ND using TFTP and Wireshark
- 71666 Views Android Reverse Engineering: Debugging Smali in Smalidea
- 59138 Views Clean Architecture : Part 2 – The Clean Architecture