Pull to refresh

Принципы SOLID. Dart/Flutter

Level of difficultyEasy
Reading time6 min
Views3.5K

SOLID - это принципы разработки программного обеспечения, следуя которым вы получите хороший код, который в дальнейшем будет хорошо масштабироваться и поддерживаться.

  • S - Single Responsibility Principle - принцип единственной ответственности. Каждый класс должен иметь только одну зону ответственности.

  • O - Open closed Principle - принцип открытости-закрытости. Классы должны быть открыты для расширения, но закрыты для изменения.

  • L - Liskov substitution Principle - принцип подстановки Барбары Лисков. Должна быть возможность вместо родительского класс подставить любой его класс-наследник, при этом работа программы не должна измениться.

  • I -  Interface Segregation Principle - принцип разделения интерфейсов. Данный принцип обозначает, что не нужно заставлять класс реализовывать интерфейс, который не имеет к нему отношения.

  • D - Dependency Inversion Principle - принцип инверсии зависимостей. Модули верхнего уровня не должны зависеть от модулей нижнего уровня. И те, и другие должны зависеть от абстракции. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Single Responsibility Principle - Принцип единственной ответственности

Допустим у нас есть класс UserRepostory и в нем есть несколько методов:

1.Получить пользователя

2.Запросить уведомление для пользователя.

3.Отправить чек пользователю.

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") {}
  }
}

У данного класса есть несколько зон ответственности, что является нарушением первого принципа. Возьмем метод отправки чека - postNotification() . У нас есть только три типа, как мы отправим пользователю уведомление, но если нам нужно добавить еще пару типов, придется изменять данный метод. Тоже самое касается метода printOrder, вдруг мы захотим добавить условие, когда наш count равен к примеру 3.

Одним словом, данный класс нарушает принцип единой ответственности, так как отвечает за разные действия.

Необходимо разделить данный класс UserRepostory на несколько, и тем самым, предоставить каждому классу отвечать только за одну зону или действие, так в дальнейшем его будет проще дополнять и модифицировать.

PrintOrderRepostory будет нести ответственность только за отправку чеков.

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

PostNotificationRepostory будет нести ответственность только за отправку уведомлений.

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

UserRepostory остается нести ответственность лишь за получение пользователя.

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

Теперь каждый класс несет ответственность только за одну зону и есть только одна причина для его изменения.

Open closed Principle - Принцип открытости-закрытости

Рассмотрим данный принцип на предыдущем примере. Представим, что нам все таки нужно обработать случай, когда количество мест куда может быть отправлены уведомления станет больше. Изменяя данный метод мы не нарушаем первый принцип, ведь данный класс имеет одну зону ответственности.

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

Но в данном случае мы нарушим второй принцип, потому что класс должен быть закрыт для изменений, но открыт для расширения, а мы изменяем метод добавив проверку для type.

Для того чтобы придерживаться принципа открытости-закрытости нам необходимо создать абстракцию PostNotificationRepostory и в нем поместить метод postNotification().

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

Далее создадим EmailNotificationRepostoryImpl, который имплементируется от нашей абстракции PostNotificationRepostory и реализует метод отправки сообщений по электронной почте.

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

Аналогично с Phone и Facebook:

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

Проектируя код таким образом мы не будем нарушать принцип открытости-закрытости, так как мы расширяем нашу функциональность, а не изменяем наш класс.

Liskov substitution Principle - Принцип подстановки Барбары Лисков

Данный принцип связан с наследованием классов. Допустим у нас есть класс Player , который содержит в себе методы: покупать, посмотреть баланс, просмотр игроков:

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

Нам необходимо разделить игроков на обычных и администраторов. При этом администратор содержит все методы, а игрок не может содержать себе метод просмотра игроков - 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
	}
}

Если в коде программы, везде где мы использовали Player заменить его на AdminPlayer, то программа продолжит работать как и раньше.

Но если мы попробуем заменить на DefoultPlayer, то программа даст сбой т.к. метод checkBalance выбрасывает исключение.

Для того чтобы следовать принципу подстановки Барбары Лисков необходимо в родительский класс выносить только общую логику, характерную для классов наследников, которые будут ее реализовывать, и соответственно можно будет родительский класс без проблем заменить на его класс-наследник. Тогда наш код будет выглядеть следующим образом:

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

Наследуем от него класс обычного игрока:

class DefoultPlayer extends Player {

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

Создадим дополнительный класс CheckBalance , который унаследуем от Player.

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

И наш класс AdminPlayer, который теперь наследуем от CheckBalance :

class AdminPlayer extends CheckBalance {

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

Interface Segregation Principle - Принцип разделения интерфейсов

Представим у нас есть абстрактный класс Pay. В нем три метода: оплата картой, оплата наличными и PayPall

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

У нас стоит задача реализовать два сервиса по проведению оплаты (по терминалу и по интернету).

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
 }
 
}

Как мы уже поняли , в классе оплаты по интернету, нет места быть методу с наличным платежом. Мы заставили переопределить этот метод, который не будет использован. Таким образом произойдет нарушение принципа разделения интерфейсов.

Для того чтобы этого не происходило необходимо разделить наш исходный интерфейс Pay на несколько и создавая классы, имплементить в них только те интерфейсы с методами, которые им нужны.

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с - Принцип инверсии зависимостей.

Мой самый любимый из всех принципов SOLID. Данный принцип повседневно встречается в разработке мобильных приложений.

Для начала определимся, что такое зависимость. Когда класс А использует класс или интерфейс B, тогда А зависит от B. А не может выполнить свою работу без B, и А не может быть переиспользован без переиспользования B. В таком случае класс А называют «зависимым», а класс или интерфейс B называют «зависимостью».

В нашем приложении есть класс AuthRepository, который зависит от EmailSignInRepository.

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

Мы уже нарушили данный принцип, потому что AuthRepository тесно связан с входом с помощью почты и если нам придется добавить новый метод входа, тогда нам придется изменять метод signIn в нашем AuthReposito и еще больше количество кода, которое от него зависит. Проще говоря ошибка в том, что мы связали модуль верхнего уровня с модулем высшего уровня.

Чтобы решить данную проблему создадим абстрактный класс SignInRepository.

abstract class SignInRepository {
	void signIn();
}

И теперь прописываем реализации наследуясь от нашего класса.

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

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

Теперь у каждого класса будет своя логика входа. Эти классы будут переданы в качестве экземпляра класса в наш AuthRepository где мы ничего не меняем:

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

Теперь наш репозиторий с аунтификацией слабо связан с системой входа, он зависит от абстракции, т.е. уже не важно каким способом входа пользователь будет пользоваться.

Для лучшего понимания как это работает:

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

На этом все, желаю вам всем удачи!

Tags:
Hubs:
Total votes 9: ↑4 and ↓5+3
Comments5

Articles