Эта статья первоначально опубликована на https://www.learncsdesign.com

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

Согласно Википедии, идеи SOLID:

  • Принцип единой ответственности (SRP). Класс никогда не должен изменяться более чем по одной причине.
  • Принцип открытого-закрытого (OCP). Сущности должны быть открыты для расширения, но закрыты для модификации.
  • Принцип подстановки Лискова (LSP). Любой метод, использующий указатели или ссылки на базовые классы, должен иметь возможность использовать производные классы или объекты, не зная об этом.
  • Принцип разделения интерфейсов (ISP). Лучше иметь много клиентских интерфейсов, чем один интерфейс общего назначения.
  • Принцип инверсии зависимостей (DIP) — опирайтесь на абстракцию, а затем на конкретную реализацию.

Давайте рассмотрим эти принципы SOLID более подробно.

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

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

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

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

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

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

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

Его основная цель — предотвратить поломку существующего кода при реализации новых функций.

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

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

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

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

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

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

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

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

Правило 1: Типы параметров метода в подклассе должны совпадать или быть более абстрактными, чем у метода в суперклассе.

Допустим, есть класс с методом, который кормит собак.

feed(Dog dog);

В этот метод всегда будет передаваться объект Dog из клиентского кода.

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

feed(Animal animal);

Теперь, если мы передаем клиентскому коду объект этого подкласса вместо объекта суперкласса, все по-прежнему будет работать как положено. Этот метод способен кормить всех животных, поэтому он по-прежнему может кормить любую собаку, мимо которой прошел клиент.

Затем вы создали еще один подкласс и ограничили метод кормления только собаками хаски (подкласс Dog).

feed(HuskyDog dog);

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

Правило 2: В методе подкласса возвращаемый тип должен совпадать или быть подтипом возвращаемого типа в методе суперкласса.

Рассмотрим класс, в котором есть метод

Dog buyDog();

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

Теперь мы создали подкласс, который переопределяет метод следующим образом.

HuskyDog buyDog();

Клиент получает собаку Хаски, которая остается собакой, так что все хорошо.

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

Animal buyDog();

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

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

В клиентском коде блоки try-catch нацелены на определенные типы исключений, которые может генерировать базовый метод. Таким образом, неожиданное исключение может проскочить через защитные линии клиентского кода и привести к сбою всего приложения.

Правило 4: Подклассы не должны усиливать предварительные условия.

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

Клиентский код, который раньше отлично работал при передаче отрицательных чисел в метод, теперь дает сбой, когда он начинает работать с этим подклассом.

Правило 5: Подклассы не должны ослаблять постусловия.

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

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

Правило 6: Суперкласс должен сохранять свои инварианты.

Инвариант — это состояние, при котором объект имеет смысл. При расширении класса самый безопасный способ — ввести новые поля и методы, не связываясь ни с какими существующими членами суперкласса.

Правило 7: Подкласс не должен иметь возможность изменять частные поля суперкласса.

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

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

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

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

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

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

Классы высокого уровня не должны зависеть от классов нижнего уровня. Оба должны полагаться на абстракции. Абстракции не должны зависеть от деталей (конкретная реализация). Скорее, абстракции должны определять детали.

Обычно при разработке программного обеспечения существует два уровня классов:

  • Низкоуровневые классы. Используя эти классы, вы можете работать с хранилищем, перемещать данные по сети, подключаться к БД и т. д.
  • Класс высокого уровня. В этих классах содержится сложная бизнес-логика, которая указывает низкоуровневым классам выполнять определенные действия.

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

Согласно принципу инверсии зависимости, эта зависимость должна быть обращена.

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

Для завинчивания и отвинчивания винтов с плоской головкой в ​​рукоятке отвертки (класс высокого уровня) используется фиксированный инструмент с плоской головкой (класс низкого уровня). Поскольку тип винта тесно связан с классом рукоятки отвертки, изменение типа винта повлияет на класс рукоятки отвертки.

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

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

Рекомендации

https://en.wikipedia.org/wiki/SOLID
Dive Into Design Patterns от Александра Швеца