Pull to refresh

Comments 94

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

  1. Single responsibility. В вашем примере нарушается инкапсуляция класса непонятно за какими целями. Вообще вполне нормально может быть, что робот может и move и speak. Вполне возможно, что у него эти методы завязаны на одних и тех же данных внутри класса. Если слепо следовать S, то у класса вылезают кишки наружу, логика переносится в другие классы, плодятся контроллеры, сервисы и прочие странные мутанты. Я уж не говорю про DDD, где класс соответствует термину предметной области - тут прямой конфликт с принципом единственной ответственности.

  2. Вы приводите в качестве примера простое использование наследования. Никакого отношения к OCP это не имеет. OCP - это когда часть класс спроектирован так, что можно без труда поменять логику в унаследованном классе, не трогая базовый класс.

  3. LSP. А что, если подставить экземпляр студента вместо персоны, он не будет работать? Пример нарушения - это как раз когда наследуют прямоугольник от квадрата и удивляются, почему при задании одного размера его не видно. Конечно, квадрат - это частный случай прямоугольника.

  4. ISP. Интерфейс вообще-то строится для клиентского кода. Соответственно, интерфейс может дробиться только до уровня требований этого кода. Если он требует объект, который должен и говорить и двигаться, то разделять эти интерфейсы не нужно. В вашем примере вы рассматриваете неправильную проектировку самого класса, а не интерфейсов. Ну на этапе создания Robot уже должно дойти, что он не умеет летать, верно?

  5. Интерфейс ради интерфейса? Вот зачем? Внедрять лишнюю сущность, когда нет ещё уверенности, что будет другая база данных - это раздувание кода ради культа карго.

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

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

 Но цель статьи, ещё раз - совсем новичков с SOLID познакомить.

что может и не надо знакомить новичков с этим? Чтобы нормально понять данные принципы, надо на практике столкнуться с теми проблемами, которые они решают. А у новичков чаще всего такие проблемы просто не стоят.

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

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

Хоть я и не автор, к которому вы обращались, но мне стало интересно. Поэтому хочу уточнить, это ли вы имели в виду (ссылка)?

In software engineering, SOLID is a mnemonic acronym for five design principles intended to make object-oriented designs more understandable, flexible, and maintainable.

К слову, в википедии на русском (ссылка) это тоже есть:

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

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

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

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

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

Ну а как тогда карму на хабре зарабатывать - про гипсовые камни писать? :)

Интерфейс вообще-то строится для клиентского кода

Почему? Можно делать интерфейс "что требуется моему апи", как контракт входной. И тогда интерфейс может просто быть, без реализаций. И таки могут быть интерфейсы как в статье - чисто "IMove", когда нужно выполнить логику движения условно. Это тоже нормально.

А если надо, можете собрать из 2-3 интерфейсов свой предметный - IRobot : IMove, ISpeak например.

Интерфейс ради интерфейса? Вот зачем? Внедрять лишнюю сущность, когда нет ещё уверенности, что будет другая база данных - это раздувание кода ради культа карго.

Как минимум для тестирования удобнее. Да, раздувание, но удобно.

Потому, что интерфейсом пользуются. Если им не пользуются, то "you ain't gonna need it". Тебе не нужен интерфейс без реализации, потому что им пользоваться не будут.

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

Если бы не одно но. Нет и не должно быть таких рекомендаций по использованию интерфейсов везде. Не надо, правда. Я насмотрелся на код, где каждый чих делается через интерфейс. Это НЕ удобно. Это НЕ правильно. Это НЕ даёт никакого выигрыша.

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

В случае с тестами оказывается, что мок класс - тоже класс. Внезапное такое откровение. И он станет второй реализацией.

Я бы вообще не рекомендовал фанатично следовать этим принципам

Лучше уж фанатично следовать, чем не следовать вообще :)

Интерфейс ради интерфейса? Вот зачем? Внедрять лишнюю сущность, когда нет ещё уверенности, что будет другая база данных - это раздувание кода ради культа карго.

Тут как раз есть ответ - для тестов.

Бывает (реже) что и не для тестов. Например, интерфейс может разрабатываться раньше реализации (contract first) чтобы его клиента и реализацию можно было разрабатывать параллельно (разными разработчиками, а то и командами). Еще, например, интерфейс и его реализацию можно разместить в разных проектах - тогда клиент API становится зависим только от проекта с интерфейсом и проект с реализацией можно обновлять независимо. Но, вот, что реально выбешивает, это когда начинают прицеплять интерфейсы к каким-нибудь DTO и прочим "anemic models" - но это уж, как говорится, "научили дурака богу молиться".

Интерфейсы нужны для DI. Даже если сейчас только одна реализация.

DI-ить можно не только интерфейсы. Просто DI-ить интерфейсы это наиболее "естественный" путь.

В вашем примере нарушается инкапсуляция класса

А можно подробностей? Что-то я посмотрел на пример из статьи и не увидел там нарушения инкапсуляции :(

Single responsibility он про reason to change, в данном случае не ходить и говорить, а про то, как он ходит и говорит. Условно, если в классе Robot мы формируем команды, то способ их передачи конкретным железкам должен быть вынесен в отдельный класс.

Тут хороший пример SRP в секции про Dependency Inversion: UserService не знает, в какие конкретно колонки кладутся отдельные поля, может, там вообще документная база, или просто в текстовый файл пишется.

Конечно, квадрат - это частный случай прямоугольника

это классический пример антипаттерна Rectangle-Square. Его также часто называют антипаттерном Ellipse-Circle.

Оба ответа автора неверные: ни класс Rectangle нельзя делать наследником Square, ни класс Square нельзя делать наследником Rectangle.

Принцип подстановки Лисков (Liskov Substitution Principle) это уточнëнная формулировка принципа "is a" (является), обычно рассматриваемого вместе с принципом "has a" (содержит).

Если объект типа A всегда является по поведению s(a) объектом типа F, с поведением s(f), то A можно наследовать от F. При этом имеется ввиду абстракция поведения.

Например, яблоко типа Apple всегда является ПО ПОВЕДЕНИЮ фруктом Fruit.

Но ни прямоугольник типа Rectangle не является по поведению (абстракции поведения) квадратом Square, ни наоборот. Ошибка в том, что путают а) классы и объекты; б) абстракцию поведения (относится к классу и наследуется) и состояние объекта (значение полей данных, не имеет отношения к наследованию). Прямоугольник может находиться в СОСТОЯНИИ квадрата - но это не имеет никакого отношения к наследованию классов.

Square вполне себе является Rectangle, и если его сделать иммутабельным (ну хотя бы не давать менять ему размер), то можно и LSP соблюсти.

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

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

Наследование квадрата от прямоугольника может быть правильным. Как и наследования прямоугольника от квадрата. Смотря чего хотите.

Ошибка в том, что подменяют понятия. В [верном] утверждении "квадрат есть частный случай прямоугольника" речь идет об иммутабельных объектах, а в примерах нарушения LSP — уже об имеющих состояние.

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

"Оба ответа автора неверные: ни класс Rectangle нельзя делать наследником Square, ни класс Square нельзя делать наследником Rectangle."

А почему?
Квадрат это прямоугольник с равными сторонами.
И в принципе можно наследовать от такового.

если квадрат - это частный случай прямоугольника - то можно использовать класс прямоугольника напрямую и не надо ничего наследовать. А если это фигура у которой задается только один размер (свойство "длина стороны A") то никак это поведение из класса прямоугольника не сделать (где уже есть два свойства "длина А и длина Б")

то никак это поведение из класса прямоугольника не сделать (где уже есть два свойства "длина А и длина Б")

У квадрата есть тоже и "ширина", и "высота". Просто они всегда одинаковы (инвариант класса).

этот самый инвариант и мешает сделать квадрат производным классом прямоугольника, а прямоугольник - производным классом квадрата

Нет, не мешает (если квадрат производный от прямоугольника). Производный класс имеет право добавлять свои инварианты если следует всем инвариантам базового. Если бы, например, квадрат мог иметь непрямые углы - вот это было бы нарушение.

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

Нет проверки инварианта равенства всех сторон.

Потому что нет контекста. Если мы будем добавлять еще сущности типа ромб. Ромб можно наследовать от прямоугольника или от квадрата? Квадрат это частный случай ромба, или квадрат от ромба можно наследовать? Можете сказать нет, ведь ромб может совсем не содержать прямой угол. Но почему мы берем за основу именно прямой угол, а не равность сторон? Если мы хотим отображать фигуры в приложении. То должен быть базовый класс четырех угольник. От него уже можно наследовать квадраты, прямоугольники, ромбы, параллелограммы и тд и тп. Наследовать их друг от друга это делать их зависимыми от частных случаев

класс четырехугольника покрывает все случаи - квадраты, ромбы, паралелограммы.. Зачем тогда от четырехуольника что то еще наследовать?

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

Я бы добавил, что для понимания SOLID (которые, имхо, суть формализованное описание принципов без понимания сути, особенно на вики), необходимо изучить первоисточник - работы Дяди Боба. То, как описано S в данной статье и как понимается подавляющим большинством разработчиков, совершенно неверно. Он даже в каком-то видео самолично ругается и объясняет, что такое на самом деле S. Доходит до того, что в классе оставляют только один метод Invoke, что возвращает нас обратно в процедурное программирование. В классе может быть и сотня методов, и при развитии еще полсотни добавляется, и при этом S выполняется. Уменьшение количество методов в классе - не самоцель и при формальном подходе только вредит архитектуре приложения.

С годами начал испытывать отвращение к красивым акронимам. В большинстве случаев "красота" акронима ущемляет смысловое наполнение. В частности какой нахрен "Liskov Substitution Principle", вы ваще о чем? Надо было что-то ткнуть на "L"?

Принцип подстановки Лисков (англ. Liskov Substitution Principle, LSP) — принцип организации подтипов в объектно-ориентированном программировании, предложенный Барбарой Лисков в 1987 году.
SOLID (сокр. от англ. single responsibility, open–closed, Liskov substitution, interface segregation и dependency inversion) в программировании — мнемонический акроним, введённый Майклом Фэзерсом (Michael Feathers) для первых пяти принципов, названных Робертом Мартином[1][2] в начале 2000-х.

(c) wiki

Видимо всё-таки сначала придумали сокращение LSP, и только потом его воткнули в SOLID. Не наоборот.

С чего вы взяли что я утверждаю будто LSP появился после SOLID? Я утверждаю, что если бы этот акроним шел бы к примеру из испанского "SOLIDO" то там был бы еще один супер-важный термин начинающийся на "O", а если бы на немецком "SOLIDE", то был бы супер-важный термин начинающийся на "E". А если бы в английском языке SOLID не было б "L" (предположим любая другая буква типа SOXID), то и LSP ваще бы никто не знал. Классический пример "хвост виляет собакой".

Я утверждаю, что если бы этот акроним шел бы к примеру из испанского "SOLIDO" то там был бы еще один супер-важный термин начинающийся на "O", а если бы на немецком "SOLIDE", то был бы супер-важный термин начинающийся на "E". А если бы в английском языке SOLID не было б "L" (предположим любая другая буква типа SOXID), то и LSP ваще бы никто не знал. Классический пример "хвост виляет собакой".

Конкретно c SOLID это неверно(хотя справедливо для многих других аббервиаутр). Сами принципы были сформулированы в 2000г Мартином, а позже, в 2004, Фэзерс увидел что эти буквы составляют аббревиатуру.

Просто я не умею читать мысли. А из вашего первоначального сообщения можно было предположить, что LSP назвали именно так только потому, что нужно было что-то придумать на букву L в акрониме SOLID.

Но разве это верно в случае LSP?
Код должен работать, а как он будет это делать - зависит от конкретной реализации в наследнике.
Иначе, для чего тогда по вашему, нужен полиморфизм?

Полиморфизм - это возможность использовать разные реализации в одном и том же клиентском коде который их использует без его изменения. Если, например, класс на входной параметр "null" выводит сообщение: "Ололо, вот он null", а его наследник роняет все приложение, то это плохой полиморфизм, потому что как раз нарушается LSP (а с т.з. "контрактного программирования", которое с LSP сильно связано, нарушается правило "не усиливать предусловия").

"не усиливать предусловие" - Вы имеете ввиду - реализовать в наследниках логику, соблюдая контракт?
Не могу сказать, что хорошо разбираюсь, но отталкиваюсь от здравого смысла (автор привёл семантическую ошибку как нарушение LSP) и посмотрел ещё пару статей про SOLID на Хабре и авторы указывают, что переопределение общих методов в наследниках возможно.

"не усиливать предусловие" - Вы имеете ввиду - реализовать в наследниках логику, соблюдая контракт?

Да, именно (в основном) об этом LSP и есть. "Контракт" это не только сигнатуры методов (которые и так в наследнике будут такие же) но и некоторые правила их поведения.

Спасибо, но всё же пока не нашёл ответа на свой вопрос.
В статье Принципы SOLID на примерах приводятся классы Account, SalaryAccount, DepositAccount в разделе про LSP и каждый повторно реализует общую логику с учётом своих особенностей.
В статье Принципы SOLID в картинках автор явно пишет

Необходимо, чтобы класс-потомок был способен обрабатывать те же запросы, что и родитель, и выдавать тот же результат. Или же результат может отличаться, но при этом относиться к тому же типу.

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

Спасибо, но всё же пока не нашёл ответа на свой вопрос.

А вы сформулируйте вопрос четче. Пока не очень понятно, что вам непонятно.

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

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

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

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

Если виртуальный метод не объявлен как private или final, то он может быть переопределен (в смысле override), не нарушая никаких принципов. Это разве не очевидно?

1) тесты могут проверять и документ кровать собой пред- и пост-условия контракта

2) вроде как в новом С++ будут добавлять это прямо в язык. Кажись)

"и документ кровать собой" - это "документировать собой" )

Это норм, но на время разработки: "Просто еще не реализовали". Если реализовывать и не собираются, то правильно кидать NotSupportedException - в общем-то, и это не совсем хорошо, и совсем хорошо перепроектировать интерфейсы чтобы такого метода в классе вообще не было, но это далеко не всегда рационально, а то и вообще возможно. Пришлось бы, например, для System.IO.Stream лепить отдельные интерфейсы типа "IReadableStream", "IWritableStream", "ISeekableStream", и еще выдумывать какой-то API чтобы с ними по отдельности работать и т.п. И, в принципе, если в базовом классе (Stream) прописано "при таких-то условиях метод сей мечет NotSupportedException", то нарушения контрактов, так-то, и нет. Или, вот, еще у GoF в разделе про паттерн "Composite" есть обсуждение на подобную тему: как быть с свойством Children для листового узла - и, в общем-то, приходят к тому, что идеального решения тут придумать не получается.

Чем плохо "лепить отдельные интерфейсы"? Ошибки компиляции всегда лучше исключений.

Ну вот смотрите: у Stream могут быть интерфейсы: IReadableStream, IWritableStream, ISeekableStream, IBufferedStream, IAsyncReadableStream. IAsyncWritableStream, которые могут быть по-разному скомбинированы. А ешё это может быть какой-нибудь NetworkStream со своими интерфейсами.

Ну да, можно обмазаться дженериками и писать что-то типа:

void MyMethod<TStream>(TStream stream) where TStream : IReadableStream, IAsyncReadableStream, IBufferedStream, ...

Но это реально уродство получается, ещё и ограничениями, потому что нельзя перегружать функции с одниаковыми сигнатурами, но разными трайтами. Куда проще просто один if внутри функции воткнуть.

Короче, баланс разумного должен быть.

у Stream могут быть интерфейсы: ..., которые могут быть по-разному скомбинированы.

Справедливости ради, там только три вариации, на которые бросается NotSupportedException: CanRead, CanWrite, CanSeek. То есть интерфейсов будет здесь всего три: IReadable, IWritable, ISeekable.

реально уродство

Вы не видите проблемы с огромным Stream с мешаниной функционала, который еше и опционален, так почему просто список интерфейсов вас огорчает?

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

Технически это можно решить сделав общий интерфейс, объединяющий необходимое (interface IGodStream: IReadable, IWritable, ISeekable {}) и оно будет работать так, как ожидается - недостаток функциональности будет алармить на этапе компиляции.

Другое дело - какой практический смысл иметь перегрузки для совершенно разного функционала? Разве такие методы могут иметь одинаковое имя?

Куда проще просто один if внутри функции воткнуть.

Не проще, т.к. поддержка механизации этой вариативности ложится на наследников, да и на ровном месте рождается абстрактный класс с этими if внутри. Далеко ходить не надо, можно оценить, как это устроено в дотнете.

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

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

Именно так. Функционал потока может измениться прямо в процессе работы. Например, сокет можно зашатдаунить только в одном направлении, и он резко из IReadableWritable становится или IReadable, или IWritable. Можно зашатдаунить в обоих направлениях - какой у него в этом случае должен быть тип?

Вот, например, stdin вполне себе stream, но умеет только read. А stdout - только write. А TCP сокет может read+write, но всё равно не seek. То есть, если вам в аргумент функции write_hello(IStream writer) положили некий неизвестный stream - вы явно должны проверить его, потому что он может не уметь write.

С другой стороны, в Rust сделано именно так, как вам не нравится: Stdin - только Read, TcpStream - Read+Write, File - Read+Write+Seek, и вроде даже это "API чтобы с ними по отдельности работать" проблем не вызывает, наоборот понятно кто что умеет, безо всяких if(stream.can_write())

Ну а как туда вписать что-то что "только Write", или "только Read + Seek", только "Write + Seek"? И как еще и сделать все это действительно полиморфным, чтобы код которому, например, нужен только "Read" мог (без изменений) работать и с сокетом и с файлом?

По сути, нам надо будет заводить семь отдельных интерфейсов (три базовых и четыре комбинированных) и в случае, к примеру, файлов еще и по отдельному методу создания потока на каждый интерфейс (e.g. OpenForRead(), OpenForWrite(), OpenForReadWriteAndSeek(), etc). Может оно как-то с совсем уж академической точки зрения и правильно, но с практической выглядит как-то так не так.

Ну и чего там хорошего? Прямо в IOBase есть:

  • fileno - может бросить исключение, а может и не бросить;

  • isatty - то есть это реализовано не через интерфейс, а флажком;

  • readable, seekable - всё как в C# (CanRead, CanSeek);

Хорошего что она есть и есть документация.

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

Как кстати istty делать через интерфейсы? Это же параметр запуска приложения.

положили некий неизвестный stream, [...] он может не уметь write

Так должны класть как раз известный - "IWritableStream", грубо говоря.

Интерфейсы - история про контракты. А тут подкладывают явно то, что контракт нарушает, и это почему-то геморой принимающей стороны?

В типичном современном языке далеко не все можно выразить формально, поэтому контакт штука немного условная. И да, конечно принимающей - у нее же сломается то?

сломается у неё, но результат-то нужен не ей, а вызывающей стороне. Поэтому это должно быть проблемой вызывающей стороны.
Как говорится: garbage in - garbage out

А если просто сделать пустой метод, то можно забыть его написать.

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

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

Говнокод плохой код:

public abstract class Character
{
    protected Character() {}

    public abstract void SayHello();
}

public class Wizard: Character
{
    public override void SayHello() => Console.WriteLine("Hello, I'm the Wizard!");
}

public class Knight: Character
{
    public override void SayHello() => Console.WriteLine("F*** you, I'm the Knight!");
}

Хороший код:

public abstract class Character
{
    protected Character() {}

    public abstract void SayHello(TextWriter output);
}

public sealed class Wizard: Character
{
    public override void SayHello(TextWriter output) => output.WriteLine("Hello, I'm the Wizard!");
}

public sealed class Knight: Character
{
    public override void SayHello(TextWriter output) => output.WriteLine("F*** you, I'm the Knight!");
}

Зачем тут абстрактный класс? Это же может быть интерфейс. Вынести writter наружу это типичная инверсия зависимостей. Для OCP пример должен быть в трёх частях. Старый класс - старый класс с новым кодом - старый класс и новый класс.

Single Responsibility Principle

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

Еще чтобы было проще прочитать и понять. Переключение контекста проблемы в мозгу очень затратная операция. А еще задачи можно разделит между разными программистами.

Open Closed Principle 

Опять же смотри полиморфизм

Принцип подстановки Барбары Лисков

почти что = определение подтипов

Полиморфизм подтипов для этого и вводят. Как тут подсказывают, LSP = полиморфизм типов + правило что подтип не должен "усиливать предусловия"

Но есть и другие виды полиморфизма, не менн полезные:

Interface Segregation Principle

это практически определение интерфейса: чтобы уменьшить зависимость между классами выдели зависимость в интерфейс. YAGNI в чистом виде

Dependency Inversion Principle

Тут все сложно. И то что определение не поместилось в одном предложении намекает на это. Для борьбы с зависимостями можно использовать абстракции. Но есть еще следующие методы:

  • DI (зависимости внедрены внешним кодом и класс их помнит всю свою жизнь при инжекции в конструктор или только в контексте вызова при инжекции в метод)

  • IoC (зависимости знает внешний код, а класс их вообще не знает)

  • Factory method (для управления зависимостями создается специальный класс, который больше ничего не умеет)

  • Контекстная зависимость (Contextual Dependency): Модуль может определить зависимость, которая будет разрешена в контексте выполнения. Например, модуль может определить интерфейс зависимости, а внешний код может предоставить конкретную реализацию этой зависимости в контексте выполнения. Модуль будет использовать эту зависимость в своей работе, но не будет знать о конкретной реализации.

Но и это еще не все про DIP

Пресловутую проблему "You wanted a banana but what you got was a gorilla holding the banana and the entire jungle" можно обойти тем что банану не нужно знать кто его владелец. Т.е. если код следует предметной области, то DIP не нужен.

Вотъ(С)

DI считается частным случаем IoC. Даже картинку где-то встречал на эту тему :)

Неверная трактовка:

Single Responsibility Principle

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

Верная трактовка:

The single-responsibility principle (SRP) is a computer programming principle that states that "A module should be responsible to one, and only one, actor."

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

Так вот почему через раз я стал читать статьи с комментов.... Комменты становятся интереснее статьи! Это уровень комментариев подрос?

Пора хабру генерировать первый автокоммент по статье, при помощи gpt ))

Комменты становятся интереснее статьи! 

Always has been

Я не припомню ни одной статьи на Хабре про SOLID, комментарии к которой не начинались бы с "Вы неверно понимаете принцип .... На самом деле это ....")

Ещё обязательно должен быть комментарий "если столько людей понимают неверно, то от него больше вреда, чем пользы".

Я когда-то пытался сформулировать эти принципы своими словами, может быть, кому-то это покажется полезным:

SRP

Single-responsibility principle, принцип единственной ответственности. Предполагает проектирование классов, имеющих только одну причину для изменения, позволяет вести проектирование в направлении, противоположном созданию «Божественных объектов». Класс должен отвечать за удовлетворение запросов только одной группы лиц.

OCP

Open–closed principle, принцип открытости/закрытости. Классы должны быть закрыты от изменения (чтобы код, опирающийся на эти классы, не нуждался в обновлении), но открыты для расширения (классу можно добавить новое поведение). Вкратце — хочешь изменить поведение класса — не трогай старый код (не считая рефакторинга, т. е. изменение программы без изменения внешнего поведения), добавь новый. Если расширение требований ведет к значительным изменениям в существующем коде, значит, были допущены архитектурные ошибки.

LSP

Liskov substitution principle, принцип подстановки Барбары Лисков: поведение наследующих классов должно быть ожидаемым для кода, использующего переменную базового класса. Или, другими словами, подкласс не должен требовать от вызывающего кода больше, чем базовый класс, и не должен предоставлять вызывающему коду меньше, чем базовый класс.

ISP

Interface segregation principle, принцип разделения интерфейса. Клиент интерфейса не должен зависеть от неиспользуемых методов. В соответствии с принципом ISP рекомендуется создавать минималистичные интерфейсы, содержащие минимальное количество специфичных методов. Если пользователь интерфейса не пользуется каким-либо методом интерфейса, то лучше создать новый интерфейс, без этого метода.

DIP

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

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

У вас получилось самое точное изложение. Лаконично и понятно.

Я очень люблю задавать вопрос про SOLID на собеседованиях, не потому что жду четкого и верного ответа, а потому что мне интересно как человек понимает принципы. Даже по комментариям здесь видно что сколько людей - столько трактовок и мнений :) А из трактовки и понимания уже легко понять опыт человека, и проблемы, с которыми он сталкивался.

А я люблю отвечать что не знаю что это когда спрашивают :)

Мы вам перезвоним

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

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

Собес обычно не похож на обычную работу. Да и собеседуют порой не будущие коллеги.

Я для себя не нашел связи между вопросами на собесе и комфорте работы с коллегами. Что является нормой для коллег уже узнаёшь по факту.

Если вам кажется, что вы понимаете принципы SOLID, значит вы не понимаете принципы SOLID.

Hidden text

Если вам кажется, что вы понимаете квантовую теорию, значит вы не понимаете квантовую теорию.

Ричард Фейман.

Никто не знает столько, сколько не знаю я.

Блин, на самом деле в первом примере проще было бы тогда сделать класс с каким нибудь более абстрактным названием NCP (например) и в конструкторе прокинуть нужный указатель на Move и Speaker чем в каждом классе писать руками делегирующий вызов.

Принципы справедливы почти для любого современного ЯП.

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

Строго говоря, они не только для ООП. Ну т.е. понятно, что если в языке нет наследования, и объектов, некоторые вещи в нем обязательно будут сформулированы иначе. Но общую идею вполне можно выделить. Ну вот давайте глянем на LSP. LSP вообще говоря про типы. А типы это совсем не обязательно ООП, это все что угодно, в функциональном языке они тоже есть.

Если верить Вики то солид именно для ООП. У ООП с функциональной парадигмой очень много общего поэтому и принципы работают и там и там.

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

Если у вас есть чистая функция, то SRP по факту соблюдается автоматически - ответственность в том, чтобы вычислить результат. Побочных эффектов и деятельности нет. Интерфейс - это сигнатура функции (для чистой функции на 100%). LSP - ну опять же, для чистой функции подстановка вместо одной функции другой, совместимой про сигнатуре, не должна ломать тех кто использует (другое дело, что есть вещи, невыразимые сигнатурой).

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

Я могу на собесе красиво рассказать про SRP и ISP. Но в жизни это для меня одно и тоже. Интерфейс, это то что определяет ответственность класса, если он не достаточно сегрегейтед, то фиг вам, а не сингл респонсибилити класса. (Под интерфейсом я воспринимаю, то что можно сделать с классом, а не специальный объект язака, как в java).

Sign up to leave a comment.

Articles