Separation of Concerns & Single Responsibility - Android Settings Example | Alexander Molochko

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.