Programming principles — Android app architecture by example Part 2/5
Learning how to use a tool is one aspect and creating a quality product using it is another aspect.
This is the second article of a five-article series on Android app architecture:
Android Studio, Java/Kotlin, Gradle, Dagger, RxJava etc. are all tools which help us to create a solution. Overall quality of the solution though does not depend on the tools. To bring quality to our solutions we need to take guidance from the programming principles.
Programming principles guide us to do things the right way. It’s the right mix of tools and programming principles which help us create an optimal solution.
The mother of all programming principles is “Separation of concerns”. Separation of concerns is focusing on a certain aspect of the software. This can be applied at function/class or higher levels. For example, a class which reads and writes data to a disk should not be concerned about how the data is processed. Its job should be only to read/write the data and the processing of the data should be handled by another class.
Why do we want to do that? Why the separation of concerns?
The answer lies in the fact that change is inevitable. If we were to release just one version of an app, we could overlook the programming principles and practices. It would have been sufficient just to make the app functional. This is never the case though. Apps keep changing as per the requirements of clients or users.
All the programming principles and best practices we apply is to prepare against the change. It’s not like “change” is bad but a change can bring errors to the app, a bug can creep in or the app can become unstable.
The change is inevitable so what we can do is, minimize the impact of the change by reducing the scope. A change which happens to one class or a function is much better than a change which creates ripples across the app. We separate function/classes/modules to reduce the impact of a change. Most of the programming principles we follow are directly or indirectly based on this separation.
One set of programming principles for object-oriented programming is SOLID introduced by Robert C. Martin in 2000. These principles are widely followed by developers all over the world.
SOLID is an acronym for the following principles:
- Single Responsibility Principle (SRP)
- Open/Closed Principle (OCP)
- Liskov Substitution Principle (LSP)
- Interface Segregation Principle (ISP)
- Dependency Inversion Principle (DIP)
Single Responsibility Principle (SRP)
A class should have only one reason to change
This principle takes direct guidance from the separation of concerns. A class should be responsible for one thing or we can say that there should be only one reason for it to change. Only one reason because if there are two reasons to change then the probability of an error or instability becomes twice and so on.
The easiest way to implement this principle is to divide a class into small functions and then group these functions together based on their proximity(cohesiveness). This way we can create multiple classes from a single class. All of them having a single responsibility. We need to be careful not to overdo it.
Let’s take an example, if we have two functions
readDataFromDisk, should they be in separate classes
DataWriter or a single class
DataAccess? Both these functions are tied to the same data and that data would be the reason to change so it makes sense to keep them in the same class. Although a lot of other factors come into play with experience it becomes easy for us to decide.
Open/Closed Principle (OCP)
Classes should be open for extension but closed for modification
A class should be open for extension but closed for modification. How can we extend without modifying the code? Here extend doesn’t mean the keyword extend but more like adapt to future changes. The answer lies in abstraction. Instead of a class depending on another concrete class, it should depend on an abstraction
For example, let’s say we have a
ReportWriter class which calls
getReportData() function of
PdfReport to get the data and print it.
It is reasonable to assume that in near future we may need to print the report in text, xml or other formats. When that happens we will have to modify the
ReportWriter class to adjust for any new format. That would be a violation of OCP, which says that a class should be closed for modification.
A better way to make our code future ready is to depend on abstraction.
We can declare an interface
Report and make the concrete class
PdfReport implement it. We can pass
Report object to the
ReportWriter class. This way
ReportWriter will depend on abstraction and wouldn’t be concerned with the concrete class which is implementing that interface.
Liskov Substitution Principle (LSP)
Subtypes must be substitutable for their base types
A child object should behave like its parent when passed as a parent. Let’s say Class B extends Class A. There is a function which requires Class A object but instead, if we pass Class B object (perfectly legal in object-oriented world) then Class B object should behave like Class A object.
For example, if Class A has a function
parseData() which returns a
String in JSON format. Class B extends class A and overrides
parseData() where it returns a
String without JSON formatting. Class B
parseData() function will work fine where Class B object is required. The problem would happen if we pass Class B object where Class A object is required. The calling code would expect a JSON formatted string but instead would get a non formatted string.
This kind of violation brings instability to our apps.
Interface Segregation Principle (ISP)
Many client specific interfaces are better than one general purpose interface
There is a
Service class which implements
ServiceInterface contains the functions required by its clients.
A better way to handle this is to divide the large single interface into multiple interface and let clients use them. This way clients would know about only those methods which it requires. Remember, separation of concerns?
Service class is implementing 3 interfaces instead of a single large interface. Other than that there is no change in
Service class but the exposure to clients are now reduced to only those functions they require.
Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules
A high level class should not depend on low level class and both should depend on abstraction. Before going further let’s see what’s a high/low level class.
An Android app executes inside a boundary. Outside the boundary, there is the Android framework itself, various service manager etc. A class which is closest to the boundary is the lower level class. For example Activity/Fragment get events directly from the Android framework which is outside the boundary so it’s low level class. A low level class is based on implementation/details.
A high level class is more abstract, dealing with business logic (the code specific to your app). High level classes are less dependent on the platform/third party libraries.
Repository class needs to get data from
DB class. Here
Repository is a higher level class than
DB class because
DB class is more dependent on detail (SQLite). Now,
Repository class can’t directly depend on
DB class as it would be a violation of DIP.
What we do is create an Interface
DataInterface which is implemented by
DB class and then
Repository class uses an object of
DataInterface . The data interface is an abstract class and at the same level as
Repository so it’s fine for the
Repository to depend on it.
Also if we see both
DB class now depend on abstraction. This helps to loosely couple the classes. Another advantage is now that we have
DataInterface , a network class can implement it and
Repository class (without any modification) would be ready to get data from the network. Reminds you something of Open Closed Principle?
Once we have an understanding of SOLID principles the next step should be to study various design patterns. Design patterns are the ready-made solutions to implement these principles in our app. For example, Open Closed Principle can be implemented using a strategy pattern or template method pattern.
In the next part we will learn what‘s layered architecture.