SOLID: пять основных принципов объектно-ориентированного проектирования

задняя часть

Текст|Alibaba Amoy Technology Тянь Ханг (шпоры)

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

  • Принцип единой ответственности
  • Открытый закрытый принцип (принцип открытия и закрытия)
  • Принцип замены Лисков
  • Принцип разделения интерфейса
  • Принцип инверсии зависимости

Давайте подробнее рассмотрим эти пять основных принципов.

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

Что такое обязанности?

Ответственность определяется в Принципе единой ответственности (SRP) как «причина для изменения».

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

interface Modem
{
    public void dial(String pno);
    public void hangup();
    public void send(char c);
    public char recv();

}

В интерфейсе кота выше есть две обязанности: первая — управление соединением (дозвон и отбой); вторая — передача данных (отправка и прием)

Эти две обязанности должны быть разделены, потому что:

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

Ниже приведена оптимизированная конструкция модема:

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

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

Подводя итог, можно сказать, что принцип единой ответственности:

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

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

Английский Open Closed Principle — Open Closed Principle, сокращенно OCP.

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

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

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

Здесь мы берем в качестве примера продажу компьютеров, сначала определяем интерфейс верхнего уровня Компьютер, а затем определяем два класса реализации, компьютер ASUS и Apple Mac, иерархия классов показана на следующем рисунке:

Вышеизложенное является нашими требованиями на начальном этапе, но с выпуском и эксплуатацией программного обеспечения наши требования не могут быть статичными и должны соответствовать рынку. Если предположить, что сейчас это Double Eleven, ноутбукам ASUS нужно участвовать в акциях. Тогда наш код должен добавить новую функциональность. Возможно, кто-то из новичков внесет изменения в исходный код, что явно не соответствует принципу открыт-закрыт.Хотя этот подход самый прямой и простой, в большинстве проектов реализация функции гораздо сложнее, чем ожидалось. Их намного больше, мы модифицируем исходный код, риск намного больше, чем расширение и реализация метода. Правильный способ таков:

Мы реализуем подкласс о скидке, который содержит метод о скидке, который эквивалентен методу расширения. Видно, что этот подкласс — AsusComputer, так почему бы не спроектировать его как общий класс скидок, такой как DiscountComputer, все классы реализации наследуют этот класс скидок. Это связано с тем, что схема скидок может быть разной для каждого класса реализации. Поэтому нам лучше реализовать его отдельно как подкласс каждого реализующего класса. Вы также можете поделиться им, если сможете убедиться, что новые функции в вашем бизнесе совместимы со всеми связанными требованиями.

Таким образом, принцип открытого-закрытого:

Основная идея: попытаться разрешить изменения требований, расширяя программные объекты, а не изменяя существующий код для внесения изменений.

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

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

Принцип подстановки Лисков был предложен Барбарой Лисков. Этот принцип очевиден. Полиморфизм Java или сама виртуальная функция C++ позволяет вызывать указатель или ссылку на базовый класс при вызове его метода или функции. Метод или функция фактического типа называется функцией. Давайте рассмотрим простой пример: Circle и Square наследуют базовый класс Shape, а затем в методе приложения он оценивается в соответствии с типом входного объекта Shape, и для рисования фигуры выбираются различные функции рисования в соответствии с типом объекта.

void drawShape(Shape shape) {
    if (shape.type == Shape.Circle ) {
        drawCircle((Circle) shape);
    } else if (shape.type == Shape.Square) {
        drawSquare((Square) shape);
    } else {
        ……
    }

}

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

Когда вы впервые видите такой код if/else, вы можете судить о том, что принцип открытия-закрытия (о котором мы только что говорили в предыдущем разделе) нарушается: при добавлении нового типа Shape вы должны модифицировать этот метод и добавить else if код. Во-вторых, по той же причине нарушается принцип подстановки Лискова: при добавлении нового типа Shape, если не модифицировать метод и не добавлять код else if, то новый тип не может заменить базовый класс Shape.

Решить эту проблему на самом деле очень просто, нужно только определить метод рисования в базовом классе Shape, все подклассы Shape, Circle и Square могут реализовать этот метод:

public abstract Shape{
  public abstract void draw();
}

У тех выше drawShape() код будет проще:

void drawShape(Shape shape) {
  shape.draw();
}

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

Подводя итог, принцип замещения Лисков таков:

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

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

Преимущество: Повысьте надежность программы, даже если будут добавлены подклассы, исходные подклассы могут продолжать работать. Примечание: Если подкласс не может полностью реализовать методы суперкласса, или некоторые методы суперкласса были «искажены» в подклассе, рекомендуется разорвать отношения наследования родитель-потомок и использовать зависимости, агрегацию, композицию и другие отношения вместо наследства.

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

Принцип разделения интерфейсов на английском языке называется SInterface Segregation Principle, сокращенно ISP. Этот принцип гласит: клиент не должен быть вынужден полагаться на ненужный ему интерфейс.

Когда мы разрабатываем интерфейсы микросервисов или библиотек классов, если часть интерфейса используется только некоторыми вызывающими объектами, то нам необходимо изолировать эту часть интерфейса и использовать ее отдельно для соответствующего вызывающего объекта, вместо того, чтобы заставлять других вызывающих абонентов полагаться на него. Некоторые интерфейсы не будут использоваться. Возьмем простой пример:

public interface UserService {
  boolean register(String cellphone, String password);
  boolean login(String cellphone, String password);
  UserInfo getUserInfoById(long id);
  UserInfo getUserInfoByCellphone(String cellphone);
}

public interface RestrictedUserService {
  boolean deleteUserByCellphone(String cellphone);
  boolean deleteUserById(long id);
}

public class UserServiceImpl implements UserService, RestrictedUserService {
  // ...省略实现代码...

}

Удаление пользователя — очень предусмотрительная операция, мы надеемся выполнить ее только через систему управления фоном, поэтому этот интерфейс ограничен системой управления фоном. Если мы поместим его в UserService, все системы, использующие UserService, смогут вызывать этот интерфейс. Неограниченные звонки из других бизнес-систем могут привести к ошибочному удалению пользователей.

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

Подводя итог, можно сказать, что принцип изоляции интерфейса таков:

Основная идея: Зависимости между классами должны устанавливаться на наименьшем интерфейсе.

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

Примечание. Интерфейс должен быть как можно меньше, но с ограничениями. Уточнение интерфейса может повысить гибкость дизайна программы, но если он слишком мал, это вызовет слишком много интерфейсов и усложнит дизайн. Поэтому необходимо умеренно улучшать сплоченность и уменьшать внешнее взаимодействие. Сделайте так, чтобы интерфейс выполнял большую часть задач наименьшим количеством способов, чтобы настроить службы для классов, зависящих от интерфейса. Вызывающему классу доступны только те методы, которые ему нужны, а методы, которые ему не нужны, скрыты. Минимальные зависимости можно установить, только сосредоточившись на предоставлении пользовательских услуг для одного модуля.

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

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

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

В приведенном выше дизайне есть три модуля: модуль копирования вызывает модуль чтения с клавиатуры для чтения вывода, а затем копирование вызывает модуль записи на принтер для вывода символов. Read Keyboard и Write Printer — это два низкоуровневых модуля, которые можно легко использовать повторно.

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

Например, мы также хотим скопировать ввод с клавиатуры в файл на диске. Конечно, мы хотим повторно использовать модуль «Копирование», но на самом деле «Копирование» зависит от клавиатуры и принтера, без которых не обойтись, поэтому его нельзя использовать повторно. Мы также можем добавить условие if в Copy для поддержки вывода нового файла на диск, но это нарушает принцип открытого-закрытого, и в конечном итоге код станет неуправляемым по мере увеличения количества функций.

Проблема в этом примере заключается в том, что модули более высокого уровня (модуль копирования) зависят от модулей более высокого уровня (чтение с клавиатуры и запись на принтер); если мы сможем найти способ сделать копирование независимым от модулей более низкого уровня, которыми он управляет, , то мы можем повторно использовать этот модуль копирования. На следующем рисунке показано решение для инверсии зависимостей.

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

Подводя итог, принцип инверсии зависимостей таков:

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

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

Преимущество: преимущества инверсии зависимостей трудно реализовать в небольших проектах. Но в крупных и средних проектах рабочую нагрузку, вызванную изменением требований, можно уменьшить. Сделайте параллельную разработку более удобной.

Суммировать

Сегодняшнее содержание резюмировано в одном предложении: принцип единой ответственности говорит нам реализовать единую ответственность за класс; принцип подстановки Лисков говорит нам не разрушать систему наследования; принцип изоляции интерфейса говорит нам упрощать и унифицировать при проектировании интерфейса. ; принцип инверсии зависимостей говорит нам Программировать интерфейс. Принцип открытого-закрытого — это общая схема, он говорит нам открывать для расширения и закрывать для модификации.

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