SOLID == ООП?
Наверное я не ошибусь, если скажу, что чаще всего на собеседованиях спрашивают о SOLID принципах. Технологии, языки и фреймворки разные, но принципы написания кода в целом похожи: SOLID, KISS, DRY, YAGNI, GRASP и подобные стоит знать всем.
В современной индустрии уже много десятков лет доминирует парадигма ООП и у многих разработчиков складывается впечатление, что она лучшая или и того хуже — единственная. На эту тему есть прекрасное видео Why Isn’t Functional Programming the Norm? про развитие языков/парадигм и корни их популярности.
SOLID изначально были описаны Робертом Мартином для ООП и многими воспринимаются как относящиеся только к ООП, даже википедия говорит нам об этом, давайте же рассмотрим так ли эти принципы привязаны к ООП?
Single Responsibility
Давайте пользоваться пониманием SOLID от Uncle Bob:
This principle was described in the work of Tom DeMarco and Meilir Page-Jones. They called it cohesion. They defined cohesion as the functional relatedness of the elements of a module. In this chapter we«ll shift that meaning a bit, and relate cohesion to the forces that cause a module, or a class, to change.
Каждый модуль должен иметь одну причину для изменений (а вовсе не делать одну вещь, как многие отвечают) и как объяснял сам автор в одном из видео — это означает, что изменения должны исходить от одной группы/роли людей, например модуль должен меняться только по запросам бизнес-аналитика, дизайнера, DBA специалиста, бухгалтера или юриста.
Обратите внимание, этот принцип относится к модулю, видом которого в ООП является класс. Модули есть во многих языках и скажем прямо — в языках вроде Java не самая лучшая реализация модулей. Выходит этот принцип не ограничен ООП.
Open Closed
SOFTWARE ENTITIES (CLASSES, MODULES, FUNCTIONS, ETC.) SHOULD BE OPEN FOR EXTENSION, BUT CLOSED FOR MODIFICATION
Bertrand Meyer
Этот принцип обычно почему-то вызывает наибольшие проблемы у людей, но суть его довольно проста — проектируя модуль следует использовать абстракции, заменяя реализацию которых можно изменять поведение модуля (расширять его) без необходимости менять его код.
При этом функция это одна из лучших абстракций (исходя из принципа сегрегации интерфейсов, о котором позже). Использование функций для обеспечения этого принципа настолько удобно, что подход уже прочно перекочевал из функциональных языков во все основные ООП языки. Для примера можно взять функции map
, filter
, reduce
, которые позволяют менять свой функционал прямой передачей кода в виде функции. Более того, весь этот функционал можно получить используя только одну функцию foldLeft
без изменения ее кода!
def map(xs: Seq[Int], f: Int => Int) =
xs.foldLeft(Seq.empty) { (acc, x) => acc :+ f(x) }
def filter(xs: Seq[Int], f: Int => Boolean) =
xs.foldLeft(Seq.empty) { (acc, x) => if (f(x)) acc :+ x else acc }
def reduce(xs: Seq[Int], init: Int, f: (Int, Int) => Int) =
xs.foldLeft(init) { (acc, x) => f(acc, x) }
Выходит, что этот принцип тоже не ограничен ООП, более того — некоторые подходы приходиться заимствовать из других парадигм.
Liskov Substitution
Обратимся к самой Барбаре:
If for each objecto1
of typeS
there is an objecto2
of typeT
such that for all programsP
defined in terms ofT
, the behavior ofP
is unchanged wheno1
is substituted foro2
thenS
is a subtype ofT
.
Немного заумно, но в целом этот принцип требует, что бы объекты «подтипа» можно было подставить в любую программу вместо объектов родительского типа и поведение программы не должно поменяться. Обычно имеется в виду не полная идентичность поведения, а то, что ничего не сломается.
Как видите тут речь хоть и идет об «объектах», но ни слова о классах нет, «объект» тут это просто значение типа. Многие говорят о том, что этот принцип регламентирует наследование в ООП и они правы! Но принцип шире и может быть даже использован с другими видами полиморфизма, вот пример (немного утрированный конечно), который без всякого наследования нарушает этот принцип:
static T increment(T number) {
if (number instanceof Integer) return (T) (Object) (((Integer) number) + 1);
if (number instanceof Double) return (T) (Object) (((Double) number) + 1);
throw new IllegalArgumentException("Unexpected value "+ number);
}
Тут мы объявляем, что функция принимает тип T
, не ограничивая его, что делает все типы его «подтипом» (т.е. компилятор позволяет передать в функцию объект любого типа), при этом функция ведет себя не так, как объявлена — работает не для всех типов.
Вообще люди, привыкли считать, что «полиморфизм» это один из принципов ООП, а значит про наследование, но это не так. Полиморфизм это способность кода работать с разными типами данных, потенциально неизвестными на момент написания кода, в данном случае это параметрический полиморфизм (собственно ошибочное его использование), в ООП используется полиморфизм включения, а существует еще и специальный (ad hoc) полиморфизм. И во всех случаях этот принцип может быть полезен.
Interface Segregation
Этот принцип говорит о том, что интерфейсы следует делать настолько минимальными, насколько это возможно, разделяя большие интерфейсы на мелкие по способам использования.
С одной стороны этот принцип говорит об интерфейсах, как о наборе функций, «протоколе» который обязуются выполнять реализации и казалось бы уж этот принцип точно про ООП! Но существуют другие схожие механизмы обеспечения полиморфизма, например классы типов (type classes), которые описывают протокол взаимодействия с типом отдельно от него.
Например вместо интерфейса Comparable
в Java
есть type class Ord
в haskell
(пусть слово class
не вводит вас в заблуждение — haskell
чисто функциональный язык):
// упрощенно
class Ord a where
compare :: a -> a -> Ordering
Это «протокол», сообщающий, что существуют типы, для которые есть функция сравнения compare
(практически как интерфейс Comparable
). Для таких классов типов принцип сегрегации прекрасно применим.
Dependency Inversion
Depend on abstractions, not on concretions.
Этот принцип часто путают с Dependency Injection, но этот принцип о другом — он требует использования абстракций где это возможно, причем абстракций любого рода:
int first(ArrayList xs) // ArrayList это деталь реализации ->
int first(Collection xs) // Collection это абстракция ->
T first(Collection xs) // но и тип элемента коллекции это только деталь реализации
В этом функциональные языки пошли гораздо дальше чем ООП: они смогли абстрагироваться даже от эффектов (например асинхронности):
def sum[F[_]: Monad](xs: Seq[F[Int]]): F[Int] =
if (xs.isEmpty) 0.pure
else for (head <- xs.head; tail <- all(xs.tail)) yield head + tail
sum[Id](Seq(1, 2, 3)) -> 6
sum[Future](Seq(queryService1(), queryService2())) -> Future(6)
эта функция будет работать как с обычным списком чисел, так и со список асинхронных запросов, которые должны вернуть числа.
Вот и выходит, что принципы SOLID более общие, чем ООП в сегодняшнем нашем его понимании. Не забывайте оглядываться по сторонам, докапываться до смысла и узнавать новое!