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

ТВЕРДЫЕ принципы

  1. Принцип единой ответственности (SPR)
  2. Открытый/закрытый принцип (OCP)
  3. Принцип замещения Лисков (LSP)
  4. Принцип разделения интерфейсов (ISP)
  5. Принцип инверсии зависимостей (DIP)

1. Принцип единой ответственности (SPR):

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

Плохой код:

 class User {
  late String name;

  void login() {
    // Perform login functionality
  }

  void sendEmail() {
    // Send email functionality
  }

  void generateReport() {
    // Generate report functionality
  }
}

Хороший код:

class User {
  late String name;
}

class Authentication {
  void login() {
    // Perform login functionality
  }
  void logout() {
    // Perform logout functionality
  }
}

class EmailSender {
  void sendEmail() {
    // Send email functionality
  }
}

class ReportGenerator {
  void generateReport() {
    // Generate report functionality
  }
}

В хорошем примере кода у каждого класса есть одна обязанность. Класс User обрабатывает функции, связанные с пользователем, класс EmailSender обрабатывает отправку электронных писем, а класс ReportGenerator обрабатывает генерация отчетов. Такое разделение упрощает обслуживание и повторное использование.

2. Принцип открытия/закрытия (OCP):

В OCP указано, что классы должны быть открыты для расширения, но закрыты для модификации. Он продвигает код, который можно легко расширять без изменения существующего кода. Рассмотрим пример:

Плохой код:

import 'circle.dart';
import 'rectangle.dart';

class AreaCalculator {
  double calculateArea(dynamic shape) {
    if (shape is Rectangle) {
      return shape.length * shape.width;
    } else if (shape is Circle) {
      return shape.radius * shape.radius * 3.14;
    }
    return 0;
  }
}

class Rectangle {
  double length;
  double width;
  Rectangle(this.length, this.width);
}

class Circle {
  double radius;
  Circle(this.radius);
}

Хороший код:

class AreaCalculator {

  double calculateArea(Shape shape) {
    return shape.calculateArea();
  }
}

abstract class Shape {
  double calculateArea();
}

class Rectangle implements Shape {
  double width;
  double height;

  Rectangle(this.width, this.height);
  @override
  double calculateArea() {
    return width * height;
  }
}

class Circle implements Shape {
  double radius;
  Circle(this.radius);

  @override
  double calculateArea() {
    return radius * radius * 3.14;
  }
}

void main(){
  final circle = Circle(5);
  final areaCalculator = AreaCalculator();
  print(areaCalculator.calculateArea(circle));
}

В хорошем примере кода мы используем OCP, вводя класс abstract Shape, который определяет метод calculateArea. Классы Rectangle и Circle классы реализуют этот метод на основе специфической логики. Класс AreaCalculator принимает объект Shape объект и вычисляет площадь без изменения существующего кода.

3. Принцип замещения Лисков (LSP)

В LSP указано, что объекты суперкласса должны быть заменяемы объектами его подклассов без нарушения работы системы. Этот принцип гарантирует, что наследование используется правильно и последовательно. Рассмотрим пример:

Плохой код:

abstract class Bird {
  void fly();
}

class Ostrich extends Bird {
  @override
  void fly() {
    throw Exception('I can not fly');
  }
}

Хороший код:

abstract class Bird {
  void eat();
}

class Ostrich extends Bird {
  @override
  void eat() {
    // TODO: implement eat
  }
}

abstract class FlyingBird extends Bird {
  void fly();
}

class Duck implements FlyingBird {
  @override
  void eat() {
    // TODO: implement eat
  }

  @override
  void fly() {
    // TODO: implement fly
  }
}

В хорошем примере кода у нас есть абстрактный класс Bird, без метода fly. Класс Ostrict правильно реализует метод eat. Острог не умеет летать. Класс Duck реализует метод fly для выполнения соответствующего поведения. Большинство уток могут летать со скоростью 80 километров в час!

4. Принцип разделения интерфейсов (ISP)

Интернет-провайдер заявляет, что клиенты не должны зависеть от интерфейсов, которые они не используют. Он продвигает меньшие, связные интерфейсы вместо больших интерфейсов общего назначения. Рассмотрим пример:

Плохой код:

class Printer {
  void print(Document document) {
    // Print document functionality
  }

  void scan(Document document) {
    // Scan document functionality
  }

  void fax(Document document) {
    // Fax document functionality
  }
}

class Document {} 

Хороший код:

class Document {}

abstract class Printer {
  void print(Document document);
}

abstract class Scanner {
  void scan(Document document);
}

abstract class FaxMachine {
  void fax(Document document);
}

class AllInOnePrinter implements Printer, Scanner, FaxMachine {
  @override
  void fax(Document document) {
    // TODO: implement fax
  }

  @override
  void print(Document document) {
    // TODO: implement print
  }

  @override
  void scan(Document document) {
    // TODO: implement scan
  }
}

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

5. Принцип инверсии зависимостей (DIP)

DIP утверждает, что;

  1. Модули высокого уровня не должны зависеть от модулей низкого уровня; оба должны зависеть от абстракций.
  2. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

DIP обеспечивает слабую связь и упрощает техническое обслуживание. Рассмотрим пример:

Плохой код:

class BackendDeveloper {
  void writeJava() {
    print('Java');
  }
}

class FrontendDeveloper {
  void writeJavascript() {
    print('Javascript');
  }
}

class Project {
  late BackendDeveloper backendDeveloper;
  late FrontendDeveloper frontendDeveloper;

  Project() {
    backendDeveloper = BackendDeveloper();
    frontendDeveloper = FrontendDeveloper();
  }

  void implement() {
    backendDeveloper.writeJava();
    frontendDeveloper.writeJavascript();
  }
}

Модуль высокого уровня класса Project зависит от модулей низкого уровня BackendDeveloper и FrontendDeveloper. . Это нарушает первое правило DIP. Кроме того, класс «Project» напрямую зависит от реализации деталей, таких как методы 'writeJava()' и 'writeJavascript()', это нарушает второе правило DIP.

Хороший код:

abstract class Developer {
  void develop() {}
}

class Project {
  final List<Developer> developers;

  Project(this.developers);

  void implement() {
    for (final developer in developers) {
      developer.develop();
    }
  }
}

class BackendDeveloper implements Developer {
  @override
  void develop() {
    _writeJava();
  }

  void _writeJava() {}
}

class FrontendDeveloper implements Developer {
  @override
  void develop() {
    _writeJavaScript();
  }

  void _writeJavaScript() {}
}

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

Вы можете найти исходный код в моем GitHub Repository.

Использованная литература:

  1. ЧатГПТ
  2. Temiz Kod Yazmanın Prensipleri SOLID
  3. https://medium.com/@radheshyamsingh_83359/dependency-inversion-principle-solid-design-78214fe2b64b
  4. https://medium.com/@radheshyamsingh_83359/liskov-substitution-principle-solid-design-f9d48500c260