Pull to refresh

Comments 60

Disclaimer: Всем можно, ну а я чем хуже!?


А чем лучше?
Вангую следующее названия для «цикла статей о солид» — очень очень очень (PG-13) простое объяснение solid.
«об этой картине можно говорить бесконечно!» (с) Корона Российской Империи
:)
UFO just landed and posted this here

Добавить в тот же уровень privare & Мосфильм. Множественное наследование? Ничего что наиболее массовые ООП языки его не поддерживают?

Наследование представляет собой отношение IS-A. Объясните пожалуйста. Как мне замапить в своей голове структуру видео\мосфильм\про_сантехников\Афоня на структуру классов. Что тут is-a чего?
Лично я не могу говорить ничего об OCP ибо «простые примеры» вызывают неслабый такой диссонанс с привычными паттернами ооп.
Афоня is a видео,
Афоня is a из_мосфильма,
Афоня is a про_сантехников
Афоня is a про_сантехников

Как минимум тут что то лишнее, не находите?
И да, такая структура не соответствует «видео\мосфильм\про_сантехников\Афоня».
> Как минимум тут что то лишнее, не находите?

в каком смысле?

> И да, такая структура не соответствует «видео\мосфильм\про_сантехников\Афоня».

class Афоня: видео, из_мосфильма, про_сантехников

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

Всмысле is a тут лишнее, «Афоня» про сантехников, про сантехников — это категория видео, «афоня» != категория видео
class Афоня: видео, из_мосфильма, про_сантехников

Ну так получается что видео, из_мосфильма, про_сантехников никак между собой не связанны. Хотя это не так, структура то подразумевает вложенность
«видео\мосфильм\про_сантехников\...», а не «видео+мосфильм+про_сантехников\Афоня».
Если вы хотите поговорить про адекватность аналогии с файлами

Да я как раз об этом.
> про сантехников — это категория видео

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

Да и как-бы не существенно сделаете вы «видео\мосфильм\про_сантехников\» или «видео\про_сантехников\мосфильм\»

Можете сделать вложенную структуру:
class мосфильм: видео;
class про_сантехников: мосфильм;
class Афоня: про_сантехников;

Но сам класс Афоня от этого не поменяется, как и его «is-a»
В коде нет «категории». Про сантехников — это абстрактный класс, обладающий свойствами-методами специфичными для фильмов про сантехников. Например, ограничение прав на доступ для аккаутов несовершеннолетних.

Про возрастные ограничения не верный пример, от категории видео — не зависит (вернее не только от этого), в контексте мосфильма будет 4+ в контексте private 18+.
Можете сделать вложенную структуру:

Ну вот так как то и думает автор, я вас понял. Для меня же очевидно, что категория видео и производитель являются атрибутами «видео» и место тут имеет связь has-a а не is-a.

Это зависит от целей выделения «про сантехников». Вы говорите возрастные ограничения не зависят, а я, говорю, что не надо детям в листинге выдавать опцию private.


is-a тут не обосновано, но и ничему не противоречит — имеет право быть. Хотя пример так себе.

UFO just landed and posted this here

Множественное наследование очень часто можно заменить на композицию

По-моему, аналогия с файлами на диске — не самая удачная. Особенно странно получается со множественным наследованием, так как в случае файловой системы у оригинального файла (не рассматриваем ярлыки и симлинки) путь к нему всегда один.
Понятно, что если все написанное начать один к одному переводить в код, то волосы дыбом встанут в неожиданных местах. Суть поста (помимо того, что он пятничный) в «Попробуем разобраться в этих принципах на пальцах». Изначально вообще хотел назвать «SOLID для самых маленьких», но со студией Privat такое название не очень бьется.
А так то толковых, но занудных и трудноусвояемых для неподготовленного человека статей про SOLID более чем в достатке.
Ну как бы в большинстве случаев множественное наследование — нарушение SRP по определению.
«Очень простое объяснение»

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

Это я к тому, что будущее постоянно преподносит нам сюрпризы. Абсолютно непредсказуемые. На то оно и будущее. У нас может быть полная определённость относительно того, что книги можно только читать и писать. Напрягши фантазию, мы можем заранее проинтуичить, что их можно ещё покупать, продавать, использовать для хранения денег и для растопки печки. А потом, ближе к сдаче работ, проходит изменение бизнес-процесса и книжки, оказывается, теперь можно ещё и слушать.
Очень хороший комментарий, спасибо!
Одна из основных проблем SOLID (как и любых других рекомендательных принципов в программировании) в том, что в большинстве случаев они не дают абсолютно четких критериев определения, соблюдаются они или нет. При принятии конечного решения, разработчик сам выбирает достаточные для него «причину изменения», «возможность добавления функционала», «уровень абстракции вот этого конкретного метода» и т.д.
Если бы эти правила давали чёткие критерии, то это была бы строгая формализация способов строгой формализации… чего? Да вообще всего, потому что программирование мы применяем во всех мыслимых областях. Боюсь, о таком даже мечтать вредно.

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

1. Когда давным-давно нас учили ООП, основным аргументом за эту штуку было то, что поскольку реальный мир состоит из объектов, то если программы тоже состоят из объектов, они более естественным образом отражают реальный мир. Но в этом рассуждении сразу две ошибки. Во-первых, ни из каких объектов реальный мир не состоит. Наше восприятие для удобства осмысления раскладывает его на объекты, притом разных ситуациях по-разному. Во-вторых, программа — не зеркало, и ей совсем не обязательно что-то там отражать. Программа — это инструмент решения задачи. Молоток не обязан отражать процесс забивания гвоздя, он должен забивать гвоздь. Автомобиль не должен моделировать движение по дороге, он должен ехать.
Можно заметить, что ООП оказалось чрезвычайно эффективным для создания вспомогательных абстрактных сущностей типа «строка», «файл», «массив», «бинарное дерево», «узел бинарного дерева», «функция» и т.п., а при создании прикладных сущностей типа «клиент», «товар», «пользователь» и др. вечно кривизна и геморрой. Объяснение простое: сущности нашего грешного «большого» мира слишком сложны, нечётки, многообразны, изменчивы и контекстно-зависимы, чтобы без проблем быть уложенными в прокрустово ложе любой жёсткой формализации. А абстрактным вспомогательным штучкам эта жёсткость только на пользу, потому что простота и предсказуемость — это как раз то, что нам в первую очередь от них нужно.

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

Соответственно, для себя в дополнение к SOLID я стараюсь придерживаться двух дополнительных правил выживания в сложном мире ООП:
1. Не пытаться моделировать программой реальный мир. Выкинуть вообще из головы эту глупую идею. Пусть все объекты в программе будут абстрактными вспомогательными полезняшками, а завязка на реальный мир пусть идёт через реализацию пользовательской метафоры. На верхнем уровне метафоро-ориентированный дизайн, а на нижнем ООП, но применённое по возможности исключительно к абстрактным инструментальным сущностям.
2. Наворот третьего уровня иерархии — харам. Аллах покарает. Если вышел на то, что третий уровень логичен и полезен, то ищи уже допущенную ошибку в своих рассуждениях.

Не знаю, какие буквы назначить этим двум принципам.
Если бы эти правила давали чёткие критерии, то это была бы строгая формализация способов строгой формализации… чего?
В предыдущем комментарии я несколько некорректно подобрал слово. Это, конечно же, не «проблема», а «особенность» этих принципов. В том смысле, что я невольно придал негативный оттенок тому, что не считаю плохим.
Однако можно пофантазировать, что это были бы четкие критерии, позволяющие определить, соответствует ли код данному принципу. Например это позволило бы писать автоматические анализаторы на соответствие SOLID.
любая иерархия с количеством уровней больше 2 неизбежно содержит внутри себя логическую ошибку. А когда начинают наворачиваться уровень на уровень, снова и снова, вся конструкция становится одним большим памятником человеческой глупости.
Вы рассматриваете иерархию как дерево — эдакий развесистый граф на плоскости, тогда как более правильно (имхо) рассматривать ее как фрактал (плохая аналогия, но что уж придумалось), по которому можно провалиться вниз, либо подняться наверх. И, при работе с конкретным уровнем абстракции, вы работаете только с ним и с интерфейсами выше- и ниже-лежащего уровней, если они есть.
позволило бы писать автоматические анализаторы на соответствие SOLID
Боюсь, некоторые из SOLIDных принципов не прочекать без понимания решаемой задачи, а это понимание автоматическому анализатору просто неоткуда взять. В явном виде оно в коде может быть не выражено никак.

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

Рассмотрим дедукцию. Имеем некий сложный объект и пытаемся его декомпозировать на составные части. Для того, чтобы это сделать, нам нужно придумать принцип, который будем использовать при делёжке, а потом его применить. Например, человеческую популяцию можно поделить по половому признаку. Или по гражданству. Или по возрастным категориям. Имущество организации можно поделить на движимое и недвижимое. Или по способу учёта — основные средства или малоценка. Человеческий организм можно декомпозировать по функциям, т.е. на органы. Или по типам тканей. Какой бы мы принцип дедукции ни выбрали, его применение нам сразу даёт существенный профит. Появляется возможность говорить про взаимоотношения полов, про влияние госполитики на национальное самосознание, про межпоколенческие конфликты, про особенности хозяйственного использования разных предметов, и т.д. Имеем два уровня абстракции и кучу позитива. Природная жадность не даёт нам остановиться на достигнутом и подталкивает к тому, чтобы развить успех. То есть нарастить снизу ещё один уровень абстракции (напоминаю, мы занимаемся дедукцией). Применить тот же принцип делёжки мы не можем, потому что на предыдущем шаге мы его уже полностью исчерпали. Нужно брать другой. Если на предыдущем шаге мы популяцию делили по полам, то теперь нужно поделить каждое подмножество по, например, возрастам. А теперь, внимание, вопрос: есть ли какой-то реальный смысл в том, что половой уровень абстракции оказался выше возрастного, а не наоборот? Очевидно, нет. Мы с таким же успехом могли бы первую декомпозицию провести по возрастам, а вторую по полам. В итоге имеем: верхний уровень абстракции заслуженно занимает своё положение, а последовательность нижележащих — чистой воды волюнтаризм.

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

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

Для того, чтобы до глубины души прочувствовать абсурдность многослойных абстракций, ознакомьтесь с шедевром — общероссийским классификатором основных фондов (известен в народе как ОКОФ). Моя любимая фишка — элемент, к которому можно отнести Большой Адронный Коллайдер. Называется «Ускорители заряженных частиц кольцевые». Лежит в группе, в названии которой присутствует словосочетание «оборудование для балансировки шин» :)))
> Объяснение простое: сущности нашего грешного «большого» мира слишком сложны, нечётки, многообразны, изменчивы и контекстно-зависимы, чтобы без проблем быть уложенными в прокрустово ложе любой жёсткой формализации.

Вот потому один из принципов DDD — выделять чётко ограниченные контексты и моделировать сущности и вообще только в рамках них. Как раз в соответствии с SRP — сущность не должна моделировать книгу и в контексте физического объекта, и в контексте печатного издания, и в контексте товара одновременно. Вот в этой «папочке» Book у нас разновидность физического объекта, имеющая например массу и три измерения, а в другой Book — печатное издание, имеющее тираж, коды, копирайты и т. п., а в третьей товар, имеющий цену закупки, продажи, остатки на складе и пр. Причём в каких-то из папочек даже сущностью может не быть Book, если нам не интересен в данный момент поштучный учёт книг, что бы под словом «книга» мы не имели в данном контексте.
Контекстная зависимость — самое главное западло. Контексты меняются, и с этим ничего поделать нельзя. Се ля ви. Чётко ограничить контексты, обнести их колючей проволокой и поставить вышки с автоматчиками — не вариант. Система или гибкая, или мёртвая. Нужно, чтобы каждая мелкая корректировка постановки задачи не вызывала масштабной перетряски всего кода. При этом предложение «заранее проинтучить возможные уточнения» не принимается. Заранее проинтуичить можно далеко не всё и не всегда. Если технология разработки ставит нас перед выбором или простое, но негибкое решение, или дико переусложнённая гибкая жуть, то, ИМХО, не надо использовать эту технологию.

(Ага, легко сказать «не использовать», но если все вокруг только это и используют, то нам тупо некуда деваться. Плачем, колемся и продолжаем жрать этот кактус.)
Отражать изменения контекстов в коде не вариант? Вышки с автоматчиками передвинуть? Ну или новые поставить, если понял, что это не небольшое изменение существующего контекста, а абсолютно новый пускай и маленький пока?
По-всякому бывает. Опытным путём установлено, что доделки/переделки происходят намного проще и безболезненнее когда количество уровней в иерархии классов не больше двух. На учебных и демонстрационных (а также на тщательно отобранных из жизни) примерах, конечно, смотрится круто, как маленькое подкручивание фичи в базовом классе сразу даёт решение задачи, но в реальности бывает так, что сначала полдня роешься в горе исходников выясняешь, куда оно закопано, потом полдня придумываешь, как сделать так, чтобы докрутка чего не похерила в неожиданном месте, а в итоге приходишь к выводу, что «новая вводная» отчаянно просит вообще переделать нахрен всю схему наследования.
Бывает, да, всякое. Но нередко как раз множество уровней наследования говорит о нарушении SOLID в целом и SRP в частности. И о нечётком или вообще неправильном выделении контекстов и декомпозиции в целом.
Правильная с одной точки зрения декомпозиция оказывается в корне неверной с другой. Предположим, рассмотренное в статье дерево мы делаем как часть автоматизации радиостанции. На голубом глазу делаем простое обобщение: музыку можно пускать по радио, книжки можно читать по радио, а фото и видио — ну никак. Потом наш клиент немножко диверсифицируется и открывает киноконцертный зал. Музыку в нём можно слушать, видео можно смотреть, фотки выставлять, а вот книжки — мимо кассы. Совсем другое обобщение. Два немножко разных противоречащих друг другу видения предметной области, и оба правильные.

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

Контекстами, кстати, в данном случае я называю ситуации, в которых нам нужно провести декомпозицию. Мой любимый пример того, как эти контексты переключаются прямо внутри одной фразы: «Если принесёшь стакан воды, я его выпью». В первой половине фразы стаканом у нас является вода+тара, а во втором только вода.
Так другая (не обновленная, а совсем другая) версия правды и является очень хорошим индикатором необходимости создания нового контекста. Вот в вашем любимом примере напрашиваются две иерархии отражающие, например, переносимые в таре вещества и вещества, употребляемые животными перорально. Собственно вещества можно выделить в третью независимую иерархию, которую будут использовать путём композиции или наследования две первых, а можно создать каждому контексту свою, отражающие чётко его потребности, а связь осуществлять через какие-то примитивные адптеры на сонове DTO/VO, а не прямыми ссылками на сущности из «чужой» иерархии.
А можно с примерами кода? Может, ссылочку на статью?!
Не все до конца понял ;(
Как потом выяснилось, ваша подруга решила поднять градус милоты и отнаследовала «Котики» новой подпапкой «Книги», грубо нарушив LSP, так как подкласс «Книги» ну никак нельзя использовать вместо базового класса «Фото».
После это она больше тебе не подруга, она тебе враг)))
Простой пример из практики: попробуйте на ООП спроектировать поведение геометрических фигур. На вычислении пересечений всплывают определенные неуклюжести подхода. А когда надо еще оптимизировать производительность, то концепции рушатся как карточный домик.
А у геометрических фигур есть поведение? Какое поведение у квадрата? :)
Зависит от задачи, долгий разговор на самом деле. Есть задачи графики, есть задачи разных расчетов.
UFO just landed and posted this here
Очень коротко.

SRP — не мешаем в одном месте работу разных по возможностям и уровням доступа пользователей. У которых разные желания и разные причины для изменения.

OCP — то что часто меняется или добавляется выносим наружу, внутрь это дело прокидываем как стратегию/шаблон. Например, повар и рецепты которые он выполняет лучше хранить и развивать отдельно, навигатор и алгоритм расчета пути из точки А в точку Б.

LSP — производный класс при привидении его к базому должен вести себя как базовый класс.

ISP — не пытаемся засунуть все в один интерфейс, на то и позволена множественная реализация.

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

> LSP — производный класс при привидении его к базому должен вести себя как базовый класс.

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

Абстракция иерархической файловой системы для хранения файлов то подходит с большим трудом, не то что для объяснения архитектурных метапринципов.

Ваши аналогии подобны роялю на лыжах.


Беда, в общем-то в самих принципах. Ну что вот это такое "A class should have only one reason to change". Любой класс меняют по одной единственной причине: он делает не то что от него хотят чтобы он делал. Всё, магическим образом все классы в мире стали solid?


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


Ну и где тут only one reason? В завистмости от того на каком уровне абстракции формулирую причины, их будет разное количество.

> а) изменилось ТЗ на приложение и надо поменять логику класса

Какого? Вы смаппите пункт ТЗ на класс и получите one reason to change. Без этого как вы выбираете какой класс менять, монеткой?

> б) в классе баг и он не соответствует ТЗ
> в) надо сделать класс более удобным для тестирования

Это вы в предыдущий раз работу недоделали. «Я комичу какую-то фигню, поэтому SRP не работает» — это слабый аргумент.

> г) поменялось апи используемое классом

А вот и пункт ТЗ с reason to change нашелся.

> он делает не то что от него хотят чтобы он делал.

Это «хотят» — что-то коллективное-бессознательное? Сегодня от него хотят, что бы класс складывал 2 числа, а завтра захотят, что бы отрисовывал кнопку на экране? Если вы не школьную лабу делаете и класс не называется MyFirstClass — у вас конкретный класс меняется по более конкретной причине (причинам), чем «хотят что-то другое». Вот и выберите одну.

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

*поперхнувшись*
Нет, от того, что вы одну причину сформулируете разными словами — причин больше не станет.

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

Ну вот и не формулируйте таких глупостей.

К чему эта софистика?

К тому что принцип формулировку не дает и в этом его проблема.

Если всем дает, а вам не даёт — это не его проблема, а ваша.

За доведение до абсурда медалек не дают.
А потом появляется Pink Floyd — The Wall, который и музыка и одновременно с этим видео, и простая стройная система ломается?
А это будет нарушением принципа инверсии зависимостей. Модуль высокого уровня «статья про SOLID» определяет детали реализации (аналогии с ФС), а не наоборот. Следовательно при разработке этой статьи в принципе не могло появиться «Pink Floyd — The Wall, который и музыка и одновременно с этим видео»
UFO just landed and posted this here
Видимо в статье про устройство файловых систем, не? Тут их нет нет потому, что в контексте статьи они не нужны ;)
UFO just landed and posted this here
Я уже высказывался о том, что формулировка «отвественность должна быть единственной» — мутно определена. Также мутно, как и «водитель должен убедиться в безопасности маневра».

А вот формулировка «слишком много ответственности» уже более понятна. И хотя бы есть критерии, по которым количество ответственности можно мерять, хотя бы косвенно. Количество строчек кода — если их много, значит ответственности много — надо декомпозировать. Слишком много импортов — тот же вывод. Слишком много тестов нужно написать, чтобы протестировать — опять тот же вывод.
Пороги между «еще норм» и «слишком много», конечно же, индивидуальны. Кому то 800 строчек в компоненте dropdown это норм, две страницы импортов это норм, что такое тесты знают только понаслышке.
А кому то 20 строчек в теле функции это уже сигнал.

SRP — делим программы на минимальные единицы композиции, функции и типы. Сложное поведение достигается путём композиции этих функций.


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


LSP — тип реализует Interface1 И Interface2 => тип реализует Interface1 ИЛИ Interface2


ISP — SRP для интерфейсов.


DIP — зависимость на интерфейсах вместо конкретных типов.


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

не важна реализация, важен интерфейс

Является ли big-O сложность частью интерфейса?

Нет. Собственно часто это используется, когда реализации с разной сложностью и разными константами меняются даже в рантайме.
Спасибо за статью. Спасибо за новый взгляд на SOLID.
Читая про сантехников, сразу подумалось «это же LSP». Оказалось не про то.
LSP — это когда интерфейс, вроде, одинаков — фильмы. Вроде и общее есть — сантехники, а вот поведение другое. Ждут от фильмов рассказ о немецких сантехниках, а получают Афоню.
С инверсией зависимости как-то совсем плохо получилось.
А пасхалку то так никто и не нашел… Печально.
Sign up to leave a comment.

Articles