SOLID Principles in Android

Sonika Srivastava
CodeX
Published in
7 min readSep 3, 2022

SOLID Principles are one of the most important principles of Object Oriented Designs by Robert C. Martin. They tell us how to arrange our functions and data structures into classes and how these classes should be interconnected.

Just by keeping these principles in mind can help us write clean code not only in Android/Java but any software in general.

The goal of these principles is to create a software structures that

  • Can tolerate change
  • Are easy to understand
  • Are building blocks of other components.

SOLID stands for

S: Single Responsibility Principle

O: Open Closed Principle

L: Liskov Substitution Principle

I: Interface Segregation Principle

D:Dependency Inversion Principle

Let’s try to understand each principle with an example.

Single Responsibility Principle:

This is the most misunderstood principle, because the name might mislead us into thinking that each module/class/function should do only one thing. However what it really mean is

A module should have one and only one reason to change.

This also means that a module is responsible to just one actor.

This is easy to understand with a common example. E.g. There is an Employee Class.

Though the Employee class and its methods look logical, they violate Single Responsibility Principle. As we can see, a change in calculateHours() may impact calculateSalary() and thus different actors or use cases are affected because of change in one use case.

Thus is it better to create 3 different classes in this case.

In Android, there is a very common violation of the law.

The “Context” class has more than 150 methods, which are not interrelated. They definitely have more than one reason to change.

E.g. Consider few methods of Context class

getSystemService(String)

checkSelfPermission(String)

getString(int)

These are clearly unrelated.

Similarly, it’s very common to see a Fragment or Activity handling much more logic than just UI.

The “GOD” classes usually become bottlenecks later on as they are very difficult to change and many times, a small change leads to new bugs.

MVP, MVVM architecture helps us to follow this principle to some extent.

Open Closed Principle:

A software artifact should be open to extension but closed for modification.

In other words, behaviour of a software artifact should be extendible, without having to modify that artifact.

This is definitely one of the most important principles as simple changes should never cause massive changes.

This also can be broadly generalised to saying that any new functionality should be implemented by adding new classes, attributes and methods, instead of changing existing ones.

The simplest way to implement OCP is to implement new functionality in a new derived class and allow clients to access the functionality with abstract interfaces.

If there are many if and else conditions in our code, we are mostly violating this principle. E.g. Util classes.

If OCP is not followed, it leads to following problems:

  • Are easy to understand
  • Are building blocks of other components.
  • We need to test the entire functionality leading to more testing efforts and regression testing.
  • Maintenance overhead increases
  • Single Responsibility Principle also get violated.

Let’s say there is a NotificationSender module in our app, which processes all incoming notifications.

public class NotificationHandler {    private Sender sender;   void send(String message, Notification notification, Module   module){     if(notification.type==BILLING){        sender.send(message,module,notification.isSuccess() );    }else if(notification.type==USER){       sender.send(message, module);    }   }}

This is not only violating the open closed principle, but also makes this class know about all other modules. If a module has to receive notification, then we need another if condition in this code.

Instead we can create an interface

interface Notifiable {    void notify(String message, Notification notification);}

All the modules which need to be notified, can implement this interface.

public class NotificationHandler {    void send(String message, Notification notification,List<Notifiable> notifiableList){        for(Notifiable notifiable:notifiableList){            if(notifiable.shouldNotify(notification))                notifiable.notify(message,notification);        }   }}

Now any new module can be added to this list, without changing anything in this code.

This is the power of this principle.

Liskov Substitution Principle.

This principle states that Subtypes must be completely substitutable for their base types.

Basically we need to think carefully about when a subtype is replaceable for its super type i.e client should be easily able to substitute base class with the corresponding derived class.

Behavioural subtyping means that not only does a subtype provide all of the methods in the supertype, but it must adhere to the behavioural specification of the supertype. This ensures that any assumptions made by the clients about the supertype behaviour are met by the subtype.

This principle can be a guideline whenever we use inheritance.

Let us see a common example in an e-commerce app. There can be multiple items, but suppose there is a discount only for Apparels. So we can create a new method “getDiscountedPrice() only for Apparels.

Now this clearly violates the Liskov Substitution Principle. Now Item cannot just be replaced with Apparel everywhere, because it might not consider the discounted price.

How do we handle this?

We have to handle discount in getPrice() method itself, so that in future any other items like Accessories or Footwear can also handle discount.

In this case implementation of getPrice() and getDiscount can be as follows:

```long getPrice(){    return price-getDisocunt();}long getDiscount(){    return discount;// 0 if no discount}

Interface Segregation Principle

This principle states, “Clients should not be forced to depend upon interfaces that they do not use“.

The goal of this principle is to reduce the side effect of using larger interfaces by breaking application interfaces into smaller ones. This can also be thought of as applying the Single Responsibility Principle to Interfaces.

In android, suppose we have a BaseFragment and multiple fragments extend from this BaseFragment. It is very common to use an interface to communicate from BaseFragment to the other Fragments.

All three fragments implement the Listener. Now, FragmentA might only be interested in changeA and changeB, while FragmentB might only be interested in changeD(), but all the fragments are forced to implement all these methods unnecessarily, bloating the classes. And even if we add an additional method in the interface, all 3 fragments will have to change.

This is violating other principles as well.

Instead of this, we can segregate the interface into smaller interfaces where the methods logically belong together. This leads to a cleaner design and maintainable code.

Dependency Inversion Principle:

Robert C Martin’s definition of Dependency Inversion Principle consists of 2 parts:

  1. High level modules should not depend on low level modules. Both should depend on abstraction.
  2. Abstraction should not depend on details. Details should depend on abstraction.

This is not so simple to understand just by reading the principle.

Let us try to understand with an example.

Let’s assume there are 3 classes, with Class A depending on Class B, which in turn depends on Class C.

At first glance, there seems nothing wrong with this kind of dependency. However this poses several problems like

  • Writing unit tests becomes difficult as we need to mock all the classes, on which our class depends on.
  • Changing one class can lead to changing many other classes.
  • Classes become tightly coupled

This can be solved by making the classes depend on abstractions i.e. Interfaces instead of another class.

Now, both the classes depend on interfaces and not concrete classes, which makes writing unit tests easier, and also de-couples the classes which help manage change better.

As another example, consider that we have a cache in our Android application, which is a shared preference.

Our shared preference class contains the following methods.

public class SharedPrefHelper {    private SharedPreferences.Editor mEditor;    private SharedPreferences mSharedPreference;    public SharedPrefHelper(@NonNull Context context) {         mSharedPreference =   context.getApplicationContext().getSharedPreferences(PublicDefine.SHARED_PREF_NAME, Context.MODE_PRIVATE);        mEditor = mSharedPreference.edit();   }     public void putLong(String key, long value) {        mEditor.putLong(key, value);        mEditor.apply();    }    public long getLong(String key, long defaultValue) {        return mSharedPreference.getLong(key, defaultValue);    }    public long getLong(String key) {        return mSharedPreference.getLong(key, 0);    }    public void remove(String key) {        mEditor.remove(key).apply();    }    public void clear() {        mEditor.clear().apply();    }}

If our fragment depends directly on this class, any change in the implementation of these methods will cause all our UI classes to change.

We can create an interface like this:

public interface AppCacheStore {    public void putLong(String key, long value);    public long getLong(String key, long defaultValue);    public long getLong(String key);    public void remove(String key);    public void clear();}

If our UI layer, depends on this Interface, instead of concrete class, we can easily change the implementation and use Data Store instead of Shared Preference and our UI layer will not be effected.

Conclusion

SOLID principles are easy to remember and keeping these in mind while writing your code will create clean, testable, reusable and module code.

--

--