Pull to refresh

SOLID == ООП?

Reading time5 min
Views21K

Наверное я не ошибусь, если скажу, что чаще всего на собеседованиях спрашивают о 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 специалиста, бухгалтера или юриста.


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


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 object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.

Немного заумно, но в целом этот принцип требует, что бы объекты "подтипа" можно было подставить в любую программу вместо объектов родительского типа и поведение программы не должно поменяться. Обычно имеется в виду не полная идентичность поведения, а то, что ничего не сломается.


Как видите тут речь хоть и идет об "объектах", но ни слова о классах нет, "объект" тут это просто значение типа. Многие говорят о том, что этот принцип регламентирует наследование в ООП и они правы! Но принцип шире и может быть даже использован с другими видами полиморфизма, вот пример (немного утрированный конечно), который без всякого наследования нарушает этот принцип:


static <T> 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<Integer> xs) // ArrayList это деталь реализации -> 
int first(Collection<Integer> xs) // Collection это абстракция -> 
<T> T first(Collection<T> 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 написано множество статей, так что моя цель тут не объяснять снова их смысл. Надеюсь мне удалось показать, что принципы SOLID более общие, чем ООП в сегодняшнем нашем его понимании. Не забывайте оглядываться по сторонам, докапываться до смысла и узнавать новое!

Tags:
Hubs:
Total votes 17: ↑14 and ↓3+11
Comments93

Articles