SOLID
The SOLID principles are five design principles intended to make software designs more understandable, flexible, and maintainable. Coined by Robert C. Martin (also known as Uncle Bob), these principles are a cornerstone of object-oriented programming.
Single Responsibility Principle
Each entity in a software, be it a module or a class or a function, should just have a single job or responsibility. A function or class ideally should have just one responsibility. If it does something unrelated to the main responsibility, it is an indication that it should move to an independent unit. This helps in making each unit testable and thereby increasing the code coverage to the maximum possible value.
An example of not following SRP. The functions does follow SRP though!
class Logger{
fun log(text: String){
Log.i("Tag", text)
}
// This violates SRP for this class as it does a job unrelated to Logger
fun saveToFile(text: String) {
// code to save text to a file
}
}This can be fixed to follow SRP. Each class does its responsibility.
class Logger{
fun log(text: String){
Log.i("tag", text)
}
}
class SaveToFile () {
fun save(text: String){
// code to save text to a file
}
}Open/Closed Principle
Any entity should be open for extension and closed for modifications.
A class that follows OCP should be open for extension. We should be able to make sub classes by extending parent class. We can add new functionalities.
But at the same time, we shouldn’t be modifying all related classes which will result in refactor through out the code base.
Here is an implementation that violates OCP
open class Shape
class Circle(val radius: FLoat): Shape()
class Square(val side: FLoat): Shape()
class AreaCalculator(){
fun calculateArea(shape: Shape): Double{
return when(shape){
is Circle -> Math.PI * shape.radius * shape.radius
is Square -> shape.side * shape.side
// Every time you add a new shape, end up modifying here as well
}
}
}But if we modify the shape classes to implement Area calculation, we can add a new shape without affecting or change in any other code. So this follows OCP
interface Shape {
fun area(): Double
}
class Circle(radius: FLoat): Shape{
override fun area(): Double = Math.PI * shape.radius * shape.radius
}
class AreaCalculator(){
fun calculateArea(shape: Shape): Double{
return shape.area()
}
}
// When adding a new shape, just implement its area calculation.
// Other classes remain untouched!
class Recatngle(width: Float, height: Float): Shape {
override fun area(): Double = width * height
}Liskov Substitution Principle
LSP essentially says, if we have a parent class and its child classes. We should be able to substitute the object of parent class with objects of child classes without any modifications.
Below is an example where LSP is violated.
interface Bird {
fun fly()
}
class Eagle: Bird(){
override fun fly(){
println("Eagle is flying")
}
}
class Duck: Bird(){
override fun fly(){
println("Duck is flying")
}
}
class Penguin: Bird(){
override fun fly(){
throws Exception("Penquin cannot fly")
}
}
class FlyingEvent{
fun startFlying(bird: Bird){
bird.fly()
}
}
class Main(){
val event = FlyingEvent()
event.fly(Eagle())
event.fly(Duck())
event.fly(Penguin()) // This will throw an exception
}This will compile successfully but fail at run time. These violations can be caught during code review or when running unit tests.
Get Reluctant Starter’s stories in your inbox
Join Medium for free to get updates from this writer.
Below is the code that does not violate LSP
open class Bird
open class FlyingBird: Bird() {
open fun fly()
}
class Eagle: FlyingBird(){
override fun fly(){
println("Eagle is flying")
}
}
class Duck: FlyingBird(){
override fun fly(){
println("Duck is flying")
}
}
class Penguin: Bird(){
// No fly method to override
}
class FlyingEvent{
fun startFlying(bird: FlyingBird){
bird.fly()
}
}
class Main(){
val event = FlyingEvent()
event.fly(Eagle())
event.fly(Duck())
event.fly(Penguin()) //Build error. This is not allowed at compile time
}Interface Segregation Principle
ISP says, when an interface is defined, it should not force its implementations to define methods that are unrelated or not required for its existence.
If such a case arises, it means, we have to separate out or segregate them into a separate interface.
Example where ISP is violated
interaface Worker{
fun work()
fun eat()
}
class Human: Worker{
override fun work() {
// Human is working
}
override fun eat() {
// Human is eating
}
}
class Bot: Worker{
override fun work() {
// Bot is working
}
override fun eat() {
// Not required
// this is forcing to override a functionality
}
}This is fixed by segregation
interaface Workable{
fun work()
}
interface Eatable
fun eat()
}
class Human: Workable, Eatable{
override fun work() {
// Human is working
}
override fun eat() {
// Human is eating
}
}
class Bot: Workable{
override fun work() {
// Bot is working
}
}Dependency Inversion Principle
DIP states that higher level modules and lower level modules should depend on abstraction. They should not depend on concrete implementations
This helps classes to be loosely coupled. Easily achievable through Dependency Injection frameworks like Dagger, Hilt
Advantages are testable, maintainable code base. Can swap in dependencies without much changes.
Example where DIP is not followed:
We cannot test the function. Also, if we want to change the analytics type, we have to change the private dependency
class MainAnalyticsLayer{
fun track(event: String){
// Track api call
}
}
class MainViewModel: ViewModel {
val analyticsLayer = MainAnalyticsLayer() //Dependency
fun trackEvent(event: String){
analyticsLayer.track(event)
}
}
class MainViewModelTest{
@Test
fun testTrackEvent(){
val vmToTest = MainViewModel()
vmToTest.trackEvent("Test") // Cannot test as the dependency cannot be mocked
}
}How to fix this with DIP
interface AnalyticsLayer{
fun track(event: String)
}
class MainAnalyticsLayer: AnalyticsLayer{
override fun track(event: String){
// Track api call
}
}
//Using manual dependency injection
class MainViewModel (val analyticsLayer:AnalyticsLayer) : ViewModel {
fun trackEvent(event: String){
analyticsLayer.track(event)
}
}
//OR Using HILT in Android
@Module
@InstallIn(SingletonComponent::class)
class AnalyticsProvider{
@Provides
fun provideAnalytics(): AnalyticsLayer{
return MainAnalyticsLayer()
}
}
class MainViewModel @Inject constructor(val analyticsLayer :AnalyticsLayer) : ViewModel {
fun trackEvent(event: String){
analyticsLayer.track(event)
}
}
class MainViewModelTest{
@Test
fun testTrackEvent(){
val mockAnalyticsLayer = Mockk.mock(AnalyticsLayer::class.java)
val vmToTest = MainViewModel(mockAnalyticsLayer)
vmToTest.trackEvent("Test")
verify(mockAnalyticsLayer).track("Test")
}
}References: Github Copilot and Google Gemini







