SOLID is a mnemonic acronym that helps define the five basic object-oriented design principles:
- Single Responsibility Principle
- Open-Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
SOLID #1: The Single Responsibility Principle (SRP)
A class should have one, and only one, reason to change.
for more details, let’s assume we need an adapter of Recyclerview, as you probably already know an adapter takes the data from the data set and adapts it to a view. An implementation I’ve seen could look like this:
// violation of single responsibility principle public class MovieRecyclerAdapter extends RecyclerView.Adapter<MovieRecyclerAdapter.ViewHolder> { private List<Movie> movies; private int itemLayout; public MovieRecyclerAdapter(List<Movies> movies, int itemLayout) { this.movies = movies; this.itemLayout = itemLayout; } @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View v = LayoutInflater.from(parent.getContext()) .inflate(itemLayout, parent, false); return new ViewHolder(v); } @Override public void onBindViewHolder(ViewHolder holder, int position) { final Movie movie = movies.get(position); holder.itemView.setTag(movie); holder.title.setText(movie.getTitle()); holder.rating.setText(movie.getRating()); String genreStr = ""; for (String str: movie.getGenre()) { genreStr += str + ", "; } genreStr = genreStr.length() > 0 ? genreStr.substring(0, genreStr.length() - 2) : genreStr; holder.genre.setText(genreStr); holder.releaseYear.setText(movie.getYear()); Glide.with(holder.thumbNail.getContext()) .load(movies.get(position) .getThumbnailUrl()) .into(holder.thumbNail); } @Override public int getItemCount() { return movies.size(); } public static class ViewHolder extends RecyclerView.ViewHolder { @Bind(R.id.title) TextView title; @Bind(R.id.rating) TextView rating; @Bind(R.id.genre) TextView genre; @Bind(R.id.releaseYear) TextView releaseYear; @Bind(R.id.thumbnail) ImageView thumbNail; public ViewHolder(View itemView) { super(itemView); ButterKnife.bind(this, itemView); } } }
The code above violates the Single Responsibility Principle. Coz the adapter’s onBindViewHolder method is not only mapping from an Movie object to the view, but is also performing data formatting as well. This violates the Single Responsibility Principle. The adapter should only be responsible for adapting an order object to its view representation. The onBindViewHolder is performing extra duties that it should not be. An updated onBindViewHolder method could look like this:
// single responsibility principle - Fix it example public class MovieRecyclerAdapter extends RecyclerView.Adapter<MovieRecyclerAdapter.ViewHolder> { private List<Movie> movies; private int itemLayout; public MovieRecyclerAdapter(List<Movie> movies, int itemLayout) { this.movies = movies; this.itemLayout = itemLayout; } @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View v = LayoutInflater.from(parent.getContext()) .inflate(itemLayout, parent, false); return new ViewHolder(v); } @Override public void onBindViewHolder(ViewHolder holder, int position) { final Movie movie = movies.get(position); holder.itemView.setTag(movie); holder.title.setText(movie.getTitle()); holder.rating.setText(movie.getRating()); holder.genre.setText( ArraysUtil.convertArrayListToString(movie.getGenre())); holder.releaseYear.setText(movie.getYear()); Glide.with(holder.thumbNail.getContext()) .load(movies.get(position) .getThumbnailUrl()) .into(holder.thumbNail); } @Override public int getItemCount() { return movies.size(); } public static class ViewHolder extends RecyclerView.ViewHolder { @Bind(R.id.title) TextView title; @Bind(R.id.rating) TextView rating; @Bind(R.id.genre) TextView genre; @Bind(R.id.releaseYear) TextView releaseYear; @Bind(R.id.thumbnail) ImageView thumbNail; public ViewHolder(View itemView) { super(itemView); ButterKnife.bind(this, itemView); } } }
As Uncle Bob said:
In the context of the Single Responsibility Principle (SRP) we define a responsibility as “a reason for change”. If you can think of more than one motive for changing a class, then that class has more than one responsibility.
SOLID #2: The Open-Closed Principle (OCP)
Software entities (classes, modules, functions, etc…) should be open for extension, but closed for modification
What we are basically talking about here is to design our modules, classes and functions in a way that when a new functionality is needed, we should not modify our existing code but rather write new code that will be used by existing code. let’s dive to it, and show some code:
// violation of Open closed principle
// Rectangle.java
public class Rectangle {
private double length;
private double height;
// getters/setters ...
}// Circle.java
public class Circle {
private double radius;
// getters/setters ...
}// AreaFactory.java
public class AreaFactory {
public double calculateArea(ArrayList<Object>... shapes) {
double area = 0;
for (Object shape : shapes) {
if (shape instanceof Rectangle) {
Rectangle rect = (Rectangle)shape;
area += (rect.getLength() * rect.getHeight());
} else if (shape instanceof Circle) {
Circle circle = (Circle)shape;
area +=
(circle.getRadius() * cirlce.getRadius() * Math.PI);
} else {
throw new RuntimeException("Shape not supported");
}
}
return area;
}
}
As we see above, this code smell like if we have a shape like Triangle or any other polygone, we’re going to be modifying the AreaFactory class over and over. And that’s violate the open closed principle. It is not closed for modification and it is not open to extension. And that’s really bad, so let’s fix that:
// Open closed principle: good example
// Shape.java
public interface Shape {
double getArea();
}
// Rectangle.java
public class Rectangle implements Shape{
private double length;
private double height;
// getters/setters ...
@Override
public double getArea() {
return (length * height);
}
}
// Circle.java
public class Circle implements Shape{
private double radius;
// getters/setters ...
@Override
public double getArea() {
return (radius * radius * Math.PI);
}
}
// AreaFactory.java
public class AreaFactory {
public double calculateArea(ArrayList<Shape>... shapes) {
double area = 0;
for (Shape shape : shapes) {
area += shape.getArea();
}
return area;
}
}
Now, if we need to add a new shape, the AreaFactory will not need to be changed because it is open for extension through the Shape interface.
SOLID #3: The Liskov Substitution Principle (LSP)
Child classes should never break the parent class’ type definitions.
As simple as that, a subclass should override the parent class’ methods in a way that does not break functionality from a client’s point of view. Here is a simple example to demonstrate the concept.
// violation of Liskov's Substitution principle// Car.java
public interface Car {
public void startEngine();
}// Ferrari.javapublic Ferrari implements Car {
...
@Override
public double startEngine() {
//logic ...
}
}
// Tesla.javapublic Tesla implements Car{
...
@Override
public double startEngine() {
if (!IsCharged)
return;
//logic ...
}
}// Make the call
public void letStartEngine(Car car) {
car.startEngine();
}
As you can see in the code above, there are two classes of cars. One fuel car and one electric car. The electric car can only start if it’s charged .The LetStartEngine method will not work if a car is electric and not charged. This breaks the LSP principle since it must be Charged to be able to start as the IsCharged (which also is part of the contract) won’t be set as in the base class.
To solve this you can do something like this:
// Make the call
public void LetStartEngine(Car car) { if (car instanceof Tesla)
((Tesla)car).TurnOnCar();
car.startEngine();
}
But this violates the Open/Closed Principale, so the proper way is to automatically turn on the car in the StartEngine method like below:
// Fix of Liskov's Substitution principlepublic interface Car {
public void startEngine();
}// Ferrari.javapublic Ferrari implements Car {
...
@Override
public double startEngine() {
//logic ...
}
}
// Tesla.javapublic Tesla implements Car{
...
@Override
public double startEngine() {
if (!IsCharged)
TurnOnCar();
//logic ...
}
}// Make the call
public void letStartEngine(Car car) {
car.startEngine();
}
SOLID #4: The Interface Segregation Principle (ISP)
The interface-segregation principle (ISP) states that no client should be forced to depend on methods it does not use.
This principale states that once an interface becomes too fat, it needs to be split into smaller interfaces so that client of the interface will only know about the methods that pertain to them. As you know, the Android View class is the root superclass for all Android views. You name it, if it’s a Button, the root superclass is View. Let’s dive to it, and show some code:
public interface OnClickListener {
void onClick(View v);
void onLongClick(View v);
void onTouch(View v, MotionEvent event);
}
As you can see, this interface contains three diffrent methods, assuming that we wanna get click from a button:
// Violation of Interface segregation principle
Button valid = (Button)findViewById(R.id.valid);
valid.setOnClickListener(new View.OnClickListener {
public void onClick(View v) {
// TODO: do some stuff...
}
public void onLongClick(View v) {
// we don't need to it
}
public void onTouch(View v, MotionEvent event) {
// we don't need to it }
});
The interface is too fat because it’s forcing to implement all the methods, even if it doesn’t not need them. Let’s trying to fix them using ISP:
// Fix of Interface Segregation principle
public interface OnClickListener {
void onClick(View v);
}public interface OnLongClickListener {
void onLongClick(View v);
}public interface OnTouchListener {
void onTouch(View v, MotionEvent event);
}
know we can use the interface without implement some messy methods.
SOLID #5: The Dependency Inversion Principle (DIP)
1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
2. Abstractions should not depend upon details. Details should depend upon abstractions.
Let’s start from the code. Many of us probably seen (or written) code like this:
// violation of Dependency's inversion principle// Program.java
class Program {
public void work() {
// ....code
}
}
// Engineer.java
class Engineer{
Program program;
public void setProgram(Program p) {
program = p;
}
public void manage() {
program.work();
}
}
The problem with the above code is that it breaks the Dependency Inversion Principle ; namely item (1.) from above: High-level modules should not depend on low-level modules. Both should depend on abstractions. We have the Engineer class which is a high level class, and the low level class called Program.
Let’s assume the Engineer class is quite complex, containing very complex logic. And now we have to change it in order to introduce the new SuperProgram. Let’s see the disadvantages:
- we have to change the Engineer class (remember it is a complex one and this will involve time and effort to make the changes).
- some of the current functionality from the engineer class might be affected.
- the unit testing should be redone.
// Dependency Inversion Principle - Good example
interface IProgram {
public void work();
}
class Program implements IProgram{
public void work() {
// ....code
}
}
class SuperProgram implements IProgram{
public void work() {
//....code
}
}
class Engineer{
IProgram program;
public void setProgram(IProgram p) {
program = p;
}
public void manage() {
program.work();
}
}
In this new design a new abstraction layer is added through the IProgram Interface. Now the problems from the above code are solved(considering there is no change in the high level logic):
- Engineer class doesn’t require changes when adding SuperProgram.
- Minimized risk to affect old functionality present in Engineer class since we don’t change it.
- No need to redo the unit testing for Engineer class.
https://academy.realm.io/posts/donn-felker-solid-part-5/
Another best example of D
Welcome to the final segment of the SOLID Principles for Android Developer series. We’ve made it to the end, and today I’m going to cover the last letter the SOLID pneumonic acronym, D: The Dependency Inversion Principle (DIP).
If you missed the first four articles, you can easily catch up here:
- S: Single Responsibility Principle
- O: Open/Closed Principle
- L: Liskov Substitution Principle
- I: Interface Segregation Principle
- D: Dependency Inversion Principle (this article)
So without further ado, our fifth and final principle –
The Dependency Inversion Principle states that we as developers should follow two pieces of advice:
a. High-level modules should not depend on low-level modules. Both should depend on abstractions.
and
b. Abstractions should not depend on details. Details should depend on abstractions.
Put succinctly, the Dependency Inversion Principle basically says this:
Depend on Abstractions. Do not depend on concretions.
Migrating to support the Dependency Inversion Principle
In order to fully grok what this principle is dictating I feel that it’s important to talk about how much of software is built – using a traditional layered pattern. We’ll look at this traditional layered architecture and then talk about how we can make changes to it so that we can support the DIP.
In a traditional layered pattern software architecture design, higher level modules depend on lower level modules to do their job. For example, here’s a very common layered architecture that you may have seen (or may even have in your application now):
Android UI → Business Rules → Data Layer
In the diagram above there are three layers. The UI Layer (in this case, the Android UI) – this is where all of our UI widgets, lists, text views, animations and anything Android UI-related lives. Next, there is the business layer. In this layer, common business rules are implemented to support the core application functionality. This is sometimes also known as a “Domain Layer” or “Service Layer.” Finally, there is the Data Layer where all the data for the application resides. The data can be in a database, an API, flat files, etc – it’s just a layer whose sole responsibility is to store and retrieve data.
Get more development news like this Subscribe Comments
Let’s assume that we have an expense tracking application that allows users to track their expenses. Given the traditional model above, when a user creates a new expense we would have three different operations happening.
- UI Layer: Allows user to enter data.
- Business Layer: Verifies that entered data matches a set of business rules.
- Data Layer: Allows for persistent storage of the expense data.
In regards to code, this might look like this:
// In the Android UI layer
findViewById(R.id.save_expense).setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
ExpenseModel expense = //... create the model from the view values
BusinessLayer bl = new BusinessLayer();
if (bl.isValid(expense)) {
// Woo hoo! Save it and Continue to next screen/etc
} else {
Toast.makeText(context, "Shucks, couldnt save expense. Erorr: " + bl.getValidationErrorFor(expense), Toast.LENGTH_SHORT).show();
}
}
});
In the business layer we might have some code that resembles this pseudo code:
// in the business layer, return an ID of the expense
public int saveExpense(Expense expense) {
// ... some code to check for validity ... then save
// ... do some other logic, like check for duplicates/etc
DataLayer dl = new DataLayer();
return dl.insert(expense);
}
The problem with the above code is that it breaks the Dependency Inversion Principle – namely item (a) from above: High-level modules should not depend on low-level modules. Both should depend on abstractions. The UI is depending upon a concrete instance of the business layer with this line:
BusinessLayer bl = new BusinessLayer();
This forever ties the Android UI layer to the business layer, and the UI layer won’t be able to do its job without the business layer.
The business layer also violates DIP, because it is depending upon a concrete implementation of the data layer with this line:
DataLayer dl = new DataLayer();
How would one go about breaking this dependency chain? If the higher-level modules should not depend on lower-level modules then how can an app do its job?
We definitely don’t want a simple monolith class that does everything. Remember, we still want to adhere to the first SOLID principle too – the Single Responsibility Principle.
Thankfully we can rely on abstractions to help implement these small seams in the application. These seams are the abstractions that allow us to implement the Dependency Inversion Principle. Changing your application from a traditional layered implementation to a dependency inverted architecture is done through a process known as Ownership Inversion.
Implementing Ownership Inversion
Ownership inversion does not mean to flip this on its head. We definitely don’t want lower-level modules depending on higher-level modules either. We need to invert this relationship completely, from both ends.
How can this be done? With abstractions.
With the Java language, there are a couple ways we can create abstractions, such as abstract classes or interfaces. I prefer to use interfaces because it creates a clean seam between application layers. An interface is simply a contract that informs the consumer of the interface of all the possible operations an implementor may have.
This allows each layer to rely on an interface, which is an abstraction, rather than a concrete implementation (aka: a concretion).
Implementing this is fairly easy in Android Studio. Let’s assume that you have That DataLayer class and it looks like this:
Since we want to depend an abstraction, we need to extract an interface off of the class. You can do that like this:
Now you have an interface you can use to depend on! However, it still needs to be utilized because the business layer still depends on the concrete data layer. Going back to the business layer, you can change that code to have the dependency injected through the constructor like this:
public class BusinessLayer {
private IDataLayer dataLayer;
public BusinessLayer(IDataLayer dataLayer) {
this.dataLayer = dataLayer;
}
// in the business layer, return an ID of the expense
public int saveExpense(Expense expense) {
// ... some code to check for validity ... then save
// ... do some other logic, like check for duplicates/etc
return dataLayer.insert(expense);
}
}
The business layer now depends upon an abstraction – the IDataLayer
interface. The data layer is now injected via the constructor via what is known as “Constructor Injection”.
In plain English this says “In order to create a BusinessLayer object, it will create an object that implements IDataLayer. It does not care who implements it, it just needs an object that implements that interface.”
So where does this data layer come from? Well, it comes from whoever creates the Business Layer object. In this case, it would be the Android UI. However, we know that our previous example illustrates that the Android UI is tightly coupled to the business layer because it is creating a new instance. We need the business layer to also be an abstraction.
At this point I would perform the same Refactor–>Extract–>Extract Interface steps that I did in the prior example. This would create a IBusinessLayer
interface that my Android UI could rely on, like this:
// This could be a fragment too ...
public class MainActivity extends AppCompatActivity {
IBusinessLayer businessLayer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
Finally, our higher-level modules are relying on abstractions (interfaces). Furthermore, our abstractions are not depending on details, they’re also depending upon abstractions.
Remember, the UI layer is depending upon the business layer interface, and the business layer interface is depending on the data layer interface. Abstractions everywhere!
Wiring it together in Android
Herein lies the rub. There’s always an entry point to an application or screen. In Android, that’s typically the Activity or Fragment class (the Application object is not a valid use case here because we may only want our objects to be active during a particular screen session). You’re probably wondering – How do I rely on an abstraction in the Android UI layer if this is the top layer?
Well, there are a couple ways you can solve it in Android using a creational pattern such as the factory or factory method pattern, or a dependency injection framework.
I personally recommend using a dependency injection framework to help you create these objects so you don’t have to manually create them. This will allow you to write code that looks like this:
public class MainActivity extends AppCompatActivity {
@Inject IBusinessLayer businessLayer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// businessLayer is null at this point, can't use it.
getInjector().inject(this); // this does the injection
// businessLayer field is now injected valid and NOT NULL, we can use it
// do something with the business layer ...
businessLayer.foo()
}
}
I personally recommend using Dagger as your dependency injection framework. There are various tutorials and video lessons on how to set up dagger so you can implement dependency injection in your application.
If you don’t use a creational pattern or dependency injection framework you’ll be left writing code that looks like this:
public class MainActivity extends AppCompatActivity {
IBusinessLayer businessLayer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
businessLayer = new BusinessLayer(new DataLayer());
businessLayer.foo()
}
}
While this may not look too bad at this time, you’ll eventually find that your object graph will grow to be quite large, and instantiating objects this way is very error-prone and breaks many of the SOLID principles. Plus, it makes your application more brittle as changes to the code can wreak havoc on your app. Ultimately, without a creational pattern or dependency injection framework, your Android UI still will not adhere to the DIP.
Patterns for Separating Interfaces
There are two patterns for separating interfaces. Which one you prefer is up to you.
- Keeping the interfaces close to the classes that implement them.
- Moving the interfaces to their own package.
The benefit of keeping them close to the classes that implement them is the pure simplicity of it. It’s not complicated, and it’s easy to grok. This has a downside though if you need to do some advanced tooling around your interfaces and implementations or if you need to share these interfaces.
The second method is to pull all of your interface abstractions into their own package and have your implementors reference this package to gain access to the interfaces. The pro of this is that it gives you more flexibility, but along with it comes the con of having another package to maintain and possibly another Java module (if you’ve taken it that far). This also increases the complexity. However, sometimes this is needed due to the circumstances of how the app (and its other related dependencies) is built.
Conclusion
The Dependency Inversion Principle is the very first principle I tend to heavily rely on in every single application I write. In every app I develop I end up using a dependency injection framework, such as Dagger, to help create and manage the object lifecycles. Depending on abstractions allows me to create application code that is decoupled, easier to test, more maintainable and enjoyable to work with (this last one is key to your sanity).
I highly recommend (and I mean it with every ounce of my being) that you take the time to learn a tool like Dagger so that you can apply it in your application. Once you fully grok what a tool like Dagger can do for you (or even a creational pattern), then you can truly grasp the power of the Dependency Inversion Principle.
Once you cross the chasm of dependency injection and dependency inversion you’ll wonder how you were ever able to get by without them. Next Up: More Android Learning Paths
https://blog.shreyaspatil.dev/dont-let-viewmodel-knew-about-framework-level-dependencies