In this article we will see the remaining Principles,
if you haven't went through the first two principle
refer its first part.
L - Liskov Substitution Principle (LSP)
This principle says that we need to ensure that subclasses can replace their base classes without altering the correctness of the program.
Here its objective is , whenever we use subclasses, it should be substitutable in a manner it should not give any error and ofcourse should work without changing the base class behaviour.
Let's take a real example - Say a store have generic payment system, having payment processed and refund processed category, now say store wants to include a new payment system Gift card, so here refund is not allowed, so if this giftCard throws exception on invoking refundProcess then it violates the LSP principle.
You can correct it as :
interface Payment{
fun paymentProcessed()
}
interface Refund{
fun refundProcessed()
}
class CashPayment : Payment,Refund {
override fun paymentProcessed() {
println("Cash Payment Processed")
}
override fun refundProcessed() {
println("Cash Refund Processed")
}
}
class GiftCardPayment : Payment,Refund {
override fun paymentProcessed() {
println("Gift Card Payment Processed")
}
override fun refundProcessed() {
println("Refund for GiftCard is Not Supported")
}
}
Here it may look like, we can avoid directly but in big scenario, we need to ensure the if there is any restriction for one of the subclasses, it should execute without any exception.
I - Interface Segregation Principle (ISP)
Probably the easiest principle to understand,
This principle says that, the methods that are declared in the interface should not be forced to implement, if its subclass doesn't requires it.
Lets say a person can do many jobs like engineer, doctor, Civil service, but treating a patient is not something engineers can do, so to adhere with ISP, we can create separate interfaces from person then we can use them.
interface Person {
fun engineer()
fun doctor()
fun teacher()
}
class Engineer : Person {
override fun engineer() {
println("Engineering")
}
}
here this code will give error, as engineer class should not implement other methods, but here it is forced to.
To fix this
interface Person {
fun engineer(){}
fun doctor(){}
fun teacher(){}
}
class Engineer : Person {
override fun engineer() {
println("Engineering")
}
}
Here this code is just an example on how you can adhere to ISP.
D - Dependency Inversion Principle
This principle says that, for any implementations, we should depend on their abstractions and not on their concrete implementation.
For this i will give two examples,
first is, If you are following Android best practices, then you might have noticed that, if you have repository in your app then in your UI layer you were using repository abstraction and in data layer, you were using that repositoryImplementation, this is one example of DIP,
another example would be, say you are using authentication methods in your app, then instead of having a concrete extension, you can make it abstract and then use its function.
This helps in various ways, first your UI layer or whoever is using the abstraction will only know what is happening and how it is happening will be hidden. second when you use generic abstraction, it gives you a vast way of implementation on how that abstraction should work.
Let's see with the code,
class LoginMethod(
private val firebaseAuth: FirebaseAuth,
private val fieldValidator: FieldValidator,
private val errorHandler: ErrorHandler
) {
fun signIn(email: String, password: String) {
// Authentication
try {
firebaseAuth.signInWithEmailAndPassword(email, password)
println("Login successful")
} catch (e: Exception) {
// Error handling
errorHandler.handleError(e)
}
}
}
Here the Firebase Auth is concrete implementation, now it has two cons, first your subclass is also aware of how it is working second if you want to use other authentications, we are restricted to do so here.
Let's Fix it
class LoginMethod(
private val authentication: Authenticator,
private val fieldValidator: FieldValidator,
private val errorHandler: ErrorHandler
) {
fun signIn(email: String, password: String) {
// Authentication
try {
authentication.signInWithEmailAndPassword(email, password)
println("Login successful")
} catch (e: Exception) {
// Error handling
errorHandler.handleError(e)
}
}
}
interface Authenticator{
fun signInWithEmailAndPassword(email: String, password: String)
fun signInWithGoogle()
}
class AuthImpl(
private val firebaseAuth: FirebaseAuth
) : Authenticator {
override fun signInWithEmailAndPasswordWithFirebase(email: String, password: String) {
firebaseAuth.signInWithEmailAndPassword(email, password)
}
override fun signInWithGoogle() {
signInWithGoogle()
}
}
Hope till now, if you are with me, this article might have helped you in understanding these principles. But in order to make it muscle memory you need to start using these principles.
If you have any suggestions, open for any discussions.
Top comments (0)