SOLID Principles. Dart

SOLID – these are software development principles that, if followed, will result in good code that will scale well and be maintained in the future.

  • S – Single Responsibility Principle – the principle of sole responsibilityEach class should have only one area of ​​responsibility.

  • O – Open closed Principle – the principle of openness-closedness. Classes should be open for extension but closed for modification.

  • L – Liskov substitution principle – the substitution principle of Barbara Liskov. It should be possible to substitute any of its descendant classes for the parent class, without changing the program's operation.

  • I – Interface Segregation Principle – the principle of separating interfaces. This principle means that you should not force a class to implement an interface that is not related to it.

  • D – Dependency Inversion Principle – the principle of dependency inversion. Top-level modules should not depend on lower-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.

Single Responsibility Principle

Let's say we have a UserRepostory class and it has several methods:

1.Get a user

2.Request notification for the user.

3.Send a receipt to the user.

class UserRepostory {

  User getUser(String id) {
    return user;
  }

  void printOrder(int count) {
    if (count == 1) {}
    if (count == 2) {}
  }

  void postNotification(String type) {
    if (type == "email") {}
    if (type == "phone") {}
  }
}

This class has several areas of responsibility, which is a violation of the first principle. Let's take the method for sending a check – postNotification() . We have only three types of how we will send a notification to the user, but if we need to add a couple more types, we will have to change this method. The same applies to the printOrder method, suddenly we want to add a condition when our count is equal to, for example, 3.

In short, this class violates the single responsibility principle because it is responsible for different actions.

It is necessary to divide this UserRepostory class into several, and thus, provide each class with responsibility for only one zone or action, so that in the future it will be easier to supplement and modify it.

PrintOrderRepostory will only be responsible for sending checks.

class PrintOrderRepostory {
  void printOrder(int count) {
    if (count == 1) {}
    if (count == 2) {}
  }
}

PostNotificationRepostory will only be responsible for sending notifications.

class PostNotificationRepostory {
  void postNotification(String type) {
    if (type == "email") {}
    if (type == "phone") {}
  }
}

UserRepostory remains responsible only for retrieving the user.

class UserRepostory {
  User getUser(String id) {
    return user;
  }
}

Now each class is responsible for only one zone and there is only one reason to change it.

Open Closed Principle – Open-Closed Principle

Let's consider this principle using the previous example. Let's imagine that we still need to handle the case when the number of places where notifications can be sent becomes larger. By changing this method, we do not violate the first principle, because this class has one area of ​​responsibility.

class PostNotificationRepostory {
  void postNotification(String type) {
    if (type == "email") {}
    if (type == "phone") {}
    if (type == "facebook") {} //add facebook
  }
}

But in this case we will violate the second principle, because the class should be closed for changes, but open for extension, and we change the method by adding a check for type.

In order to adhere to the open-closed principle, we need to create an abstraction PostNotificationRepostory and place the postNotification() method in it.

abstract class PostNotificationRepostory {
	void postNotification(String type);
}

Next, we will create EmailNotificationRepostoryImpl, which is implemented from our PostNotificationRepostory abstraction and implements the method for sending messages via email.

class EmailNotificationRepostoryImpl implements PostNotificationRepostory {
	@override
	void postNotification(String type){
		//code
	}
}

Similar with Phone and Facebook:

class PhoneNotificationRepostoryImpl implements PostNotificationRepostory {
	@override
	void postNotification(String type){
		//code
	}
}
class FacebookNotificationRepostoryImpl implements PostNotificationRepostory {
	@override
	void postNotification(String type){
		//code
	}
}

By designing the code this way we will not violate the open-closed principle, since we are extending our functionality, not changing our class.

Liskov substitution principle – Barbara Liskov substitution principle

This principle is related to class inheritance. Let's say we have a Player class that contains methods: buy, view balance, view players:

class Player {
	void buy(){
		//code
	}
	void checkPlayers(){
		//code
	}
	void checkBalance(){
		//code
	}
}

We need to divide players into regular and administrators. The administrator contains all methods, and the player cannot contain the method for viewing players – checkPlayers().

class DefoultPlayer extends Player {

	@override
	void buy(){
		//code
	}
	
	@override
	void checkPlayers(){
		throw Exception('It is not AdminPlayer');
	}
	
	@override
	void checkBalance(){
		//code
	}
}
class AdminPlayer extends Player {

	@override
	void buy(){
		//code
	}
	
	@override
	void checkPlayers(){
		//code
	}
	
	@override
	void checkBalance(){
		//code
	}
}

If in the program code, wherever we used Player, we replace it with AdminPlayer, then the program will continue to work as before.

But if we try to replace it with DefoultPlayer, the program will crash because the checkBalance method throws an exception.

In order to follow the principle of Barbara Liskov substitution, it is necessary to put only the general logic in the parent class, which is typical for the classes of heirs that will implement it, and accordingly, it will be possible to replace the parent class with its heir class without problems. Then our code will look like this:

class Player {
	
	void buy(){
		//code
	}
	
	void checkBalance(){
		//code
	}
}

We inherit the regular player class from it:

class DefoultPlayer extends Player {

	@override
	void buy(){
		//code
	}
	
	@override
	void checkBalance(){
		//code
	}
}

Let's create an additional class CheckBalance , which we will inherit from Player.

class CheckBalance extends Player {
	void checkPlayers(){
		//code
	}
}

And our AdminPlayer class, which we now inherit from CheckBalance :

class AdminPlayer extends CheckBalance {

	@override
	void buy(){
		//code
	}
	
	@override
	void checkPlayers(){
		//code
	}
	
	@override
	void checkBalance(){
		//code
	}
}

Interface Segregation Principle – Interface Segregation Principle

Let's imagine we have an abstract class Pay. It has three methods: payment by card, payment by cash and PayPall

abstract class Pay {
	void buyCreditCard();
	void buyCash();
	void buyPayPal();
}

Our task is to implement two services for making payments (via terminal and via the Internet).

class InternetPay implements Pay {

 @override
 void buyCreditCard(){
  //code
 }
 
 @override
 void buyCash(){
  //wtf ?
 }
 
 @override
 void buyPayPal(){
 //code
 }
 
}
class TerminalPay implements Pay {

 @override
 void buyCreditCard(){
  //code
 }
 
 @override
 void buyCash(){
 //code
 }
 
 @override
 void buyPayPal(){
 //code
 }
 
}

As we have already understood, in the Internet payment class, there is no place for a method with cash payment. We forced to redefine this method, which will not be used. Thus, the principle of separation of interfaces will be violated.

To prevent this from happening, we need to split our original Pay interface into several and, when creating classes, implement in them only those interfaces with methods that they need.

abstract class BuyCreditCard {
	void buyCreditCard();
}

abstract class BuyCash {
	void buyCash();
}

abstract class BuyPayPal{
	void buyPayPal();
}
class InternetPay implements BuyCreditCard , BuyPayPal {

  @override
   void buyCreditCard(){
    //code
   }
 
  @override
   void buyPayPal(){
   //code
   }
 
}
class TermialPay implements BuyCreditCard , BuyPayPal , BuyCash  {

   @override
   void buyCreditCard(){
    //code
   }
   
   @override
   void buyCash(){
   //code
   }
   
   @override
   void buyPayPal(){
   //code
   }
 
}

Dependency Inversion Principleс – Dependency Inversion Principle.

My favorite of all the SOLID principles, this principle is encountered every day in mobile app development.

First, let's define what a dependency is. When class A uses class or interface B, then A depends on B. A can't do its job without B, and A can't be reused without reusing B. In such a case, class A is called “dependent,” and class or interface B is called a “dependency.”

In our application we have a class AuthRepository which depends on EmailSignInRepository.

class EmailSignInRepository {
	void signInEmail(){
		//code
	}
}
class AuthRepository {
	EmailSignInRepository emailSignInRepository;
	Auth({required this.emailSignInRepository});
	
	void signIn() {
		emailSignInRepository.signInEmail();
	}
	
}

We have already violated this principle because AuthRepository is tightly coupled with email login and if we were to add a new login method then we would have to change the signIn method in our AuthReposito and even more code that depends on it. In simple terms the mistake is that we coupled a top-level module to a higher-level module.

To solve this problem, let's create an abstract class SignInRepository.

abstract class SignInRepository {
	void signIn();
}

And now we write the implementations inheriting from our class.

class EmailSignInRepository implements SignInRepository {
	@override
	void signIn(){
		//code
	}
}

class PhoneSignInRepository implements SignInRepository {
	@override
	void signIn(){
		//code
	}
}

Now each class will have its own login logic. These classes will be passed as an instance of the class to our AuthRepository where we do not change anything:

class AuthRepository {
	SignInRepository signInRepository;
	Auth({required this.signInRepository});
	
	void signIn() {
		signInRepository.signIn();
	}
	
}

Now our repository with authentication is weakly connected with the login system, it depends on the abstraction, i.e. it no longer matters which login method the user will use.

To better understand how it works:

if (typeButton == "phone") signInRepo = PhoneSignInRepository();
if (typeButton == "email") signInRepo = EmailSignInRepository ();
authRepo = AuthRepository(signInRepo);

That's all, I wish you all good luck!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *