【Перевод】По принципу SOLID

Solidity
【Перевод】По принципу SOLID

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

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

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

Чтобы понять принцип SOLID, вы должны иметь четкое представление об использовании интерфейсов.Если вы не понимаете концепции интерфейсов, я предлагаю вам сначала прочитать это.статья.

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

принцип единой ответственности

Класс может быть изменен только по одной причине.

A class should have one, and only one, reason to change.

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

Давайте посмотрим на этот код:

public class OrdersReportService {

    public List<OrderVO> getOrdersInfo(Date startDate, Date endDate) {
        List<OrderDO> orders = queryDBForOrders(startDate, endDate);

        return transform(orders);
    }

    private List<OrderDO> queryDBForOrders(Date startDate, Date endDate) {
        // select * from order where date >= startDate and date < endDate;
    }

    private List<OrderVO> transform(List<OrderDO> orderDOList) {
        //transform DO to VO
    }
}

Этот кодекс нарушает принцип единой ответственности. Почему оператор sql выполняется в этом классе? Такие операции следует размещать на уровне сохранения, который отвечает за обработку операций, связанных с сохранением данных, включая сохранение или запрос данных из базы данных. Так что эта ответственность не должна принадлежать этому классу.

Метод преобразования также не должен принадлежать к этому классу, потому что нам может понадобиться много типов преобразований.

Поэтому нам необходимо провести рефакторинг кода.Рефакторинг кода выглядит следующим образом (для экономии места):

public class OrdersReportService {

    @Autowired
    private OrdersReportDao ordersReportDao;
    @Autowired
    private Formatter formatter;
    public List<OrderVO> getOrdersInfo(Date startDate, Date endDate) {
        List<OrderDO> orders = ordersReportDao.queryDBForOrders(startDate, endDate);

        return formatter.transform(orders);
    }
}

public class OrdersReportDao {
    
    public List<OrderDO> queryDBForOrders(Date startDate, Date endDate) {}
}

public class Formatter {
    
    private List<OrderVO> transform(List<OrderDO> orderDOList) {}
}

принцип открыто-закрыто

Открыт для расширения, закрыт для модификации.

Entities should be open for extension, but closed for modification.

Программные объекты (включая классы, модули, функции и т. д.) должны быть расширяемыми без изменения содержимого объекта из-за расширения. Если мы строго следуем этому принципу, мы можем изменить поведение кода, не меняя исходный код.

В качестве примера возьмем кусок кода:

class Rectangle extends Shape {
    private int width;
    private int height;

    public Rectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }
}
class Circle extends Shape {
    private int radius;

    public Circle(int radius) {
        this.radius = radius;
    }
}
class CostManager {
    public double calculate(Shape shape) {
        double costPerUnit = 1.5;
        double area;
        if (shape instanceof Rectangle) {
            area = shape.getWidth() * shape.getHeight();
        } else {
            area = shape.getRadius() * shape.getRadius() * pi();
        }

        return costPerUnit * area;
    }
}

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

Поэтому мы можем поместить метод расчета площади в класс Shape, и тогда каждый подкласс, который его наследует, может реализовать свой метод расчета. Таким образом, нет необходимости изменять исходный код.

Принцип замены Лисков

Принцип замещения ЛисковаBarbara LiskovПредставлен на конференции «Абстракция данных» в 1987 году. Барбара Лисков иJeannette WingВ 1994 году была опубликована статья, иллюстрирующая этот принцип:

Если φ(x) — свойство типа T, а S — подтип T, то φ(y) — свойство S.

Пусть φ(x) — доказуемое свойство объектов x типа T. Тогда φ(y) должно быть истинным для объектов y типа S, где S — подтип T.

Барбара Лисков дает простую для понимания версию, но она больше полагается на систему типов:

1. Preconditions cannot be strengthened in a subtype. 2. Postconditions cannot be weakened in a subtype. 3. Invariants of the supertype must be preserved in a subtype.

Более краткое и последовательное определение было предложено Робертом Мартином в 1996 году:

Функции, использующие указатели на базовые классы, также могут использовать подклассы.

Functions that use pointers of references to base classes must be able to use objects of derived classes without knowing it.

Проще говоря, подклассы могут заменить родительские классы.

В соответствии с принципом подстановки Лискова мы можем заменить абстрактные классы (интерфейсы) их подклассами (классами реализации) везде, где они допустимы. В принципе, мы должны обращать внимание не только на входные параметры интерфейса при программировании, но и на то, чтобы возвращаемые значения классов, реализующих интерфейс, были одного типа.

Следующий код нарушает принцип подстановки Лискова:

<?php
interface LessonRepositoryInterface
{
    /**
     * Fetch all records.
     *
     * @return array
     */
    public function getAll();
}
class FileLessonRepository implements LessonRepositoryInterface
{
    public function getAll()
    {
        // return through file system
        return [];
    }
}
class DbLessonRepository implements LessonRepositoryInterface
{
    public function getAll()
    {
        /*
            Violates LSP because:
              - the return type is different
              - the consumer of this subclass and FileLessonRepository won't work identically
         */
        // return Lesson::all();
        // to fix this
        return Lesson::all()->toArray();
    }
}

Примечание переводчика: я не ожидал, как здесь должна быть реализована Java, поэтому использовал код автора напрямую, и все его понимают.

Принцип разделения интерфейса

Клиента нельзя заставить реализовать интерфейс, который он не использует.

Клиента не следует заставлять реализовывать интерфейс, который он не использует.

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

Подобно принципу единой ответственности, принцип разделения интерфейса сводит к минимуму побочные эффекты и дублирование кода за счет разделения программного обеспечения на независимые части.

Давайте посмотрим на пример:

public interface WorkerInterface {

    void work();
    void sleep();
}

public class HumanWorker implements WorkerInterface {

    public void work() {
        System.out.println("work");
    }
    public void sleep() {
        System.out.println("sleep");
    }
}

public class RobotWorker implements WorkerInterface {

    public void work() {
        System.out.println("work");
    }
    public void sleep() {
        // No need
    }
}

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

public interface WorkAbleInterface {

    void work();
}

public interface SleepAbleInterface {

    void sleep();
}

public class HumanWorker implements WorkAbleInterface, SleepAbleInterface {

    public void work() {
        System.out.println("work");
    }
    public void sleep() {
        System.out.println("sleep");
    }
}

public class RobotWorker implements WorkerInterface {

    public void work() {
        System.out.println("work");
    }
}

Принцип инверсии зависимости

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

Абстракции не должны зависеть от деталей, а детали должны зависеть от абстракций.

High-level modules should not depend on low-level modules. Both should depend on abstractions.

Abstractions should not depend on details. Details should depend on abstractions.

Проще говоря: абстракция не зависит от деталей, а детали зависят от абстракции.

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

Давайте посмотрим на этот код:

public class MySQLConnection {

    public void connect() {
        System.out.println("MYSQL Connection");
    }
}

public class PasswordReminder {

    private MySQLConnection mySQLConnection;

    public PasswordReminder(MySQLConnection mySQLConnection) {
        this.mySQLConnection = mySQLConnection;
    }
}

Существует распространенное заблуждение, что инверсия зависимостей — это просто еще один способ выражения внедрения зависимостей, хотя на самом деле это не одно и то же.

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

Если вы хотите изменить MySQLConnection на MongoConnection, измените инъекцию жестко запрограммированного конструктора в PasswordReminder.

Чтобы соответствовать принципу инверсии зависимостей, PasswordReminder полагается на абстрактные классы (интерфейсы), а не на детали. Итак, как мне изменить этот код? Давайте посмотрим вместе:

public interface ConnectionInterface {

    void connect();
}

public class MySQLConnection implements ConnectionInterface {

    public void connect() {
        System.out.println("MYSQL Connection");
    }
}

public class PasswordReminder {

    private ConnectionInterface connection;

    public PasswordReminder(ConnectionInterface connection) {
        this.connection = connection;
    }
}

В измененном коде, если мы хотим изменить MySQLConnection на MongoConnection, нам не нужно изменять внедрение конструктора класса PasswordReminder, потому что класс PasswordReminder полагается на абстракцию, а не на детали.

Спасибо за чтение!

исходный адрес

medium.com/better-pro Страна…

Комментарии переводчика

Введение автора в принцип SOLID относительно ясное, но я думаю, что принцип Лискова не очень ясен, и приведенные примеры кажутся не очень понятными. Я понимаю принцип замены Лискова: подклассы могут расширять функции суперкласса, но не могут изменять методы суперкласса. Таким образом, можно сказать, что принцип подстановки Лисков является реализацией принципа открытого-закрытого. Конечно, эта статья лишь кратко знакомит с каждым принципом SOLID, и вы можете получить более подробное представление, ознакомившись с информацией. Я считаю, что после понимания этих принципов проектирования у вас будет более глубокое понимание программирования. Позже я продолжу публиковать некоторые статьи о принципах дизайна, добро пожаловать на внимание.