Комментарии 27
Спасибо за примеры из реальной жизни.
Сам применяю принципы SOLID в повседневности.
Примеры хорошие, за это плюс. Однако, есть упущение.
В DIP раскрыли только первую часть принципа, а вторую - нет.
Абстракции не должны зависеть от деталей, но детали должны зависеть от абстракций.
Здесь стоит добавить, что абстракции не должны быть большими и т.о. не должны требовать от класса-реализатора реализовать весь огромный функционал. Иначе, абстракция становится зависимой от дефолтной реализации - т.е. абстракция становится зависимой от деталей.
Если абстракция становится зависимой от дефолтной реализации, то и компоненты, зависимые от этой абстракции, транзитивно становятся зависимы от дефолтной реализации этой абстракции - т.е. нарушается первая часть DIP, поскольку компоненты становятся зависимы не от абстракции а от других компонентов - дефолтных реализаций абстракции.
Фактически, вторая часть DIP'а требует от абстракций соблюдать SRP и ISP.
Я например согласен с таким взглядом. Он сводится к тому, что к этим принципам надо подходить разумно, без фанатизма. Иначе всё может стать только хуже: огромное количество микрофункций/микромодуле/микроклассов, разбросанных по тысячам дирректорий/файлов... Разобраться в такой каше может быть намного сложнее, чем в простыне на десяток-другой тысяч строк. В общем, главное во всём этом - вовремя остановиться. А многие ведь буквально воспринимают принципы типа "не более пяти строк".
Глобально, согласна, что меру нужно во всем знать:)
Иначе всё может стать только хуже: огромное количество микрофункций/микромодуле/микроклассов, разбросанных по тысячам дирректорий/файлов... Разобраться в такой каше может быть намного сложнее, чем в простыне на десяток-другой тысяч строк
...
принципы типа "не более пяти строк".
Вы палитесь. Вы мешаете в кучу SOLID и подход "Чистый код". Так делают или тролли или те, кто не видит разницу и плохо понимает вопрос. Вот Мартин-то не зря описал эти 2 вещи в 2 разных трудах.
Во-первых, я рассматриваю аспекты фанатизма/неофитства, а они общие для всех подходов.
Во-вторых, связь между этими вещами всё же есть и она прямая. Например, требование к компактности функций и малому числу аргументов (не более трех вроде?) выводятся из принципа единственной ответственности. Т.е. эти "пять строк" не из пустого места взялись.
Как общие подходы эти принципы хороши, но рассматривать их сами по себе, в отрыве от конкретной задачи, да ещё с фанатизмом, не очень хорошая затея. Зачастую проект простой и там не нужны горы многоуровневых абстракций. Я раньше думал, что может быть в будущем, когда наступит время расширять и усложнять проект, эти дополнительные абстракции помогут, но... по факту оказывалось, что это не нужно, а сопровождение проекта из-за лишних абстракций усложнялось. А в случае если всё же наступала пора радикально расширять проект, то он обычно с нуля полностью переписывался с учетом новых требований. А первоначальное простое решение было по сути прототипом.
Мой вывод: все эти принципы держать в голове, но конкретную реализацию выбирать под конкретную задачу и главное разумно выбирать.
выводятся из принципа единственной ответственности
Не выводятся. Потому что SRP это вообще не про то, что у функции/класса должна быть одна ответственность. О чем этот принцип лучше прочитать от его создателя напрямую.
Во-вторых, связь между этими вещами всё же есть и она прямая. Например, требование к компактности функций и малому числу аргументов (не более трех вроде?) выводятся из принципа единственной ответственности
Это вы сами придумали. Все-таки книги Мартина написаны на простом современном языке и в толковании не нуждаются. Открываем и видим, что и откуда у него выводится. Тезисы про арность и длину функций следуют просто из его опыта. Про SOLID ни слова. Он просто советует делать так, потому что ему так было проще. Хочешь - доверяешь его опыту, хочешь - не доверяешь.
Функции хорошие в плане SRP могут быть короткими, могут быть длинными и страшными. Например, весь класс HashMap в джаве страшный как ядерная война. Однако по SRP какие к нему вопросы? Такая ситуация скорее всего в стандартной либе любого языка. Чистым кодом и не пахнет, а вот найти нарушения SOLID надо прям постараться.
Это вы сами придумали. Все-таки книги Мартина написаны на простом современном языке и в толковании не нуждаются.
Извините, я не фанатик, чтобы такие тексты воспринимать буквально.
А если мысли авторов воспринимать по существу, то связь с некоторыми сабжевыми принципами прекрасно видна. Да и почти буквально в этих книгах говорится, что если функция длинная, если она принимает на вход больше трех аргументов или принимает на вход флаги, то это скорей всего означает, что ф-ция выполняет более одной задачи. И эту логику тоже не надо рассматривать буквально. Это корректней рассматривать как рекомендацию внимательней посмотреть на функцию в плоскости соответствия некоторым принципам, если обнаруживаются эти признаки. Да, это не прямые связи, а косвенные, но всё взаимосвязано.
В принципе статья про это же. Тут пример про шкаф для одежды, для S, но можно же и дальше дальше пойти, отдельный шкаф для носков, трусов, футболок.
Где то остановиться же надо и определить для себя, что для вашего приложения S.
Случай из жизни. Довелось мне как-то рефакторить один API. Где, как и что - неважно.
Но была там одна функция на 700 строк, написанная в стиле "что вижу, то пою". Т.е. логика совершенно разнородная в одной функции, куча if-ов и так далее. Ну, я выделил отдельные логические блоки в функции, постарался их понятно обозвать и сделать так, чтобы функция верхнего уровня читалась как объяснение логики работы.
А в это же самое время пришел к нам еще один товарищ, адепт ООП. Он рефакторил другой кусок, тоже большой. Он взял, создал класс, получился класс со множеством методов, но, так как логика была довольно сложная,он накидал еще условий в метод
_init_()
Так, что теперь логика созданного объекта зависела от того, с какими аргументами вызывался этот инит.
И снизошло на меня просветление. Я подумал, хорошо, что я рефакторил код, написанный в стиле "большая простыня кода". Если бы я что-то делал за адептом ООП, да если бы еще он применил бы все принципы, создал бы дерево классов, я бы еще и сейчас бы сидел там и разбирался, что куда наследует и с какими свойствами получается объект.
Мне тоже пришлось недавно изучать один проект. Сразу скажу, возникли двоякие ощущения... С одной стороны, код был идеально вылизан, всё было четко разбито на уровни/классы/функции, причем на очень компактные функции (менее 5 строк, как по феншую). И я, пытаясь понять логику кода,... задолбался перескакивать из одной микрофункции (в одном файле), в другую микрофункцию (в другом файле), потом далее, делее, далее, потом... обратно, обратно, обратно... А их тысячи. Например, вижу вызов функии, иду туда (в ее реализацию), там пара строк с вызовом другой функции, иду в нее, там одна строка с вызовом третьей функции из четырех строк... и так далее... всё глубже и глубже... я уже давно забыл, чего начал...
Для себя я выработал примерно такое эмпирическое правило. Может оно и не по фэншую, но для меня работает. И оно довольно простое и помещается в голове.
1) Всю логику верхнего уровня делаю в функции run() верхнего уровня. Ну или методе run().
2) Этоа функция по возможности не должна содержать "if" или только один уровень, так, чтобы вложенных ифов не было.
3) Функции второго уровня называю так, чтобы читая функцию верхнего уровня сверху вниз в голове создавалась блок схема того, что делает программа.
4) Вложенность второго уровня должна быть минимальной, насколько возможно.
В общем структура программы должна напоминать грабли с короткими зубьями, а не дерево со множеством ветвлений.
Наследование стараюсь применять по минимому, в основном там где нужно изменить поведение, просто переопределяыя методы. (пишу на питоне).
Но выбор архитектуры программирования ИМХО зависит от области применения. Где-то хорошо подходит ООП, где-то пайплайн. Но таких областей не так уж много.
В моем стиел один недостаток, на интервью начнешь описыать - не поймут о чем это.
побочная сложность растет не только если у компонента большой функционал, но еще и когда слишком много уровней скрыто за абстракцией. пока гуляешь по этим вызовам забывается что было в предыдущих вызовах
думаю, стоит выделять в системе небольшое количество концептуальных слоев и делить реализацию между ними. даже если методы получаются не меньше 5 строк
Для этого вначале рисуют дизайн на UML например, но это при водопад. И потом смотрят не в код, а на дизайн.
При еджайле, конечно такое не проканает.
Хранение одежды - это его основная и единственная задача.
Потом мы решили хранить там не только вещи, но и инструменты. Это усложнило назначение шкафа и поиск конкретной вещи стал медленнее (вещей-то больше)
Потом мы решили хранить в этом же шкафу еще и продукты. Теперь шкаф выполняет сразу несколько абсолютно разных задач
Это довольно плохой пример для иллюстрации SRP. Шкаф может состоять из модулей (про это вы говорите в OCP). И то, что вы будете хранить в одном из модулей не одежду, а инструменты - не будет нарушать SRP на уровне отдельных модулей.
Даже хранить инструменты в шкафу для одежды - это вообще не зазорно. Для этого там может быть ниша снизу, например, в которой может лежать вообще ничего (т.к. вы решили что в этом шкафу кроме одежды ничего не будет).
А поиск конкретной вещи может и не стать медленнее. Если у вас шкафов меньше - то и мест для поиска тоже меньше.
Ну хорошо, допустим мы считаем что шкаф для одежды - только для одежды. А верхняя одежда в этом шкафу - будет являться нарушением SRP? Это одежда, но мы обычно выделяем такую одежду. Или обувь: технически, это вещь, которую мы надеваем.
Допустим, мы купили шкаф! Хех. Опять шкаф, да. Когда у нас стало больше одежды (да-да, одежды, а не продукты мы тоже захотели в нем хранить), не нужно разбирать шкаф и делать новый. Мы просто покупаем дополнительные полки, ящик или секцию, расширяя таким образом функциональность шкафа и не ломая его структуру.
Здесь тоже шкаф - это плохой пример. "Просто покупаем" полки/ящики может привести к тому, что у вас некуда будет их вставлять, или уже висящая одежда по высоте не будет помещаться. И с этим в принципе ничего не сделать - это физическое ограничение. Секции тоже не получится ставить бесконечно по тем же причинам.
Представьте, что мы купили телевизор. К этому телевизору шел пульт, с помощью которого им можно управлять. Но оказалось, что этот пульт управляет не только телевизором, но и кондиционером, и обогревателем, т.е. на нем гораздо больше кнопок. А ты хотел простой и понятный, минималистичный пульт, который управляет только телевизором.
И это плохой пример. Точнее, ISP он конечно иллюстрирует, но в реальности многим людям наоборот не хочется иметь много пультов, потому что они теряются и необходимо разрабатывать стратегию менеджмента пультами.
Представь, что у тебя есть розетка в доме. Ты не задумываешься о том, какое именно устройство ты будешь подключать в неё — фен, телефон, зарядку для ноутбука — всё будет работать, потому что розетка стандартизирована (абстракция) - не надо шутить тут про американские вилки))))). Если бы тебе нужно было менять розетку каждый раз под новое устройство, это было бы крайне неудобно.
Да, но нет. Опять же, мир достаточно сложен и без американских розеток. Технически, розетка одна, но максимальный ток может быть разным. Особенно это на удлинителях заметно. Удлинителей на 16А очень мало, а на 10А и меньше - хоть ложкой ешь.
Примеры должны быть такими, чтобы было как можно меньше разночтений. Но придумывать хорошие примеры - сложно.
Первый принцип понят неверно. Хотя это обычное дело, даже с учётом того, что в книге, чуть дальше, всё расписано. У меня, может, весь микросервис выполняет одну задачу. Что такое задача вообще?
Правильно так: у кода должна быть только одна причина для изменений. Более того, дядя Боб её буквально уточняет: эта причина - пользователь системы (бухгалтер например). Т.е. мы должны зависеть только от бизнес-требований, а не от каких-то API и уж тем более имплементаций чего-то. Это принцип влияет на все остальные принципы, особенно второй. В итоге любые детали реализации (API breakage например) не должны заставлять нас что-то переписывать, только написать новый адаптер.
В целом статья неплохая, но на Барбару Лисков очень странный пример "из жизни". Не знаю, у кого в жизни случалось такое, что пылесос брызгал маслом. И даже если у кого-то в жизни случалось такое, хз как это относится к теме.
Я бы предложил другой пример: допустим, вы купили шкаф (опять), да не простой, а серверный. В шкафу стоят стандартные рейки от системы крепления и вы можете купить что угодно с таким креплением, и оно подойдёт в этот шкаф, что бы это ни было: сервер, накопитель, UPS, маршрутизатор.
Кстати, если у брызжущего маслом пылесоса из чьей-то жизни окажется аналогичное крепление, то он как раз будет в тему.
Что касается примеров из жизни - вот вообще всё не так. По порядку.
Здесь рассказано про наборы коллекций, это вполне может быть уместно. Более корректный пример был бы если вы например зимой использовали бы шкаф для хранения одежды, а летом, ну не знаю, как доску для серфинга например.
Сам принцип связан с наследованием, как можно отнаследоваться от шкафа, не совсем понятно. В примере про замену секций рассказывается про композицию. Более корректный пример был бы если мы взяли бы чертежи шкафа и дорисовали бы к ним что-то дополнительное на основе существующего.
Тут вообще описан баг и несоответствие ТЗ, причем тут L?
В примере про I из SOLID на самом деле рассказывается про S.
В примере с розеткой на самом деле описывается интерфейс и его преимущества, и не более того.
Пример с телевизором не очень. Был у меня телевизор и видеомагнитофон, так пульт от магнитофона управлял ещё и телевизором, и это было чертовски удобно! А ещё сейчас в руках я держу смартфон, а он и звонки делает и сообщения пишет в самые разные мессенджеры (и вот прямо сейчас на Хабр) и видео проигрывает и музыку, а ещё фотографирует и даже видео снимает и ещё кучу всего делает... и это тоже удобно.
Другое дело, что и на пульте кнопки были визуально отделены друг от друга и в телефоне всё делается в собственных приложениях.
В общем не на всех уровнях хорошо и полезно разделять интерфейсы. Иногда их нужно наоборот объединять, для этого, например, существует паттерн агрегатор.
Пока что лучшее описание принципов SOLID, что я встречал. Спасибо!
Приято, что нашел комментарии выше, которые правят статью.
Дополню:
SRP - НЕ о том, что "Каждый класс должен выполнять только одну задачу." Автор SOLID так же писал, что этот принцип понимается наиболее неоднозначно.
Цитата автора SOLID (книга Читая Архитектура):
"Услышав это название, многие программисты решают: оно означает, что каждый модуль должен отвечать за что-то одно .
...
Но он не является одним из принципов SOLID — это не принцип единственной ответственности"
А конечное определение он же дает как : "Модуль должен отвечать за одного и только за одного актора", где актор - лицо/группа лиц заинтересованные в изменениях.
И автор там же дает пример, где "Класс, который выполняет только одну задачу" будет нарушать принцип SRP.
В интернете почти не найти правильного определения и описания этого принципа. Почему авторы не обращаются к первоисточнику, не понятно.
Принципы SOLID на примерах из жизни и разработки