Каждый программист хоть раз слышал о принципах SOLID. На собеседованиях и экзаменах в вузах многие из нас пытались вспомнить, о чем же был тот самый принцип Лисков. Однако вряд ли цель преподавателей и интервьюеров — заставить нас заучивать строчки из учебников. SOLID действительно помогает писать качественный код, когда во всем разберешься! Если вы этого еще не сделали, добро пожаловать под кат. Еще раз взглянем на то, как устроены всем известные принципы. Обещаю — без духоты, все рассмотрим на примерах с котиками.
Используйте оглавление, если не хотите читать текст полностью:
→ Принцип единственности ответственности
→ Принцип открытости / закрытости
→ Принцип подстановки Барбары Лисков
→ Принцип разделения интерфейсов
→ Принцип инверсии зависимостей
→ Заключение
Принцип единственности ответственности
Для игры и еды должны быть отдельные места.
Принцип единственности ответственности (Single Responsibility Principle, SRP) гласит: каждый класс или модуль должен иметь только одну причину для изменения. То есть он должен выполнять только одну задачу.
Вроде звучит несложно, но где применить этот принцип? Да и что это вообще значит — «иметь только одну причину для изменения»? Разберемся на кошачьем примере.
Моего котика зовут Боря. Сейчас я попробую его имплементировать.
class BoryaCoolCat:
def eat(self):
print("Омномном")
def play(self):
print("Тыгыдык")
def save_to_database(self):
# Код для сохранения Борика в базу данных
pass
Борян у нас парень продвинутый — и помяукать может, и в базу себя сохранить. Допустим, ветеринар сказал Боре есть влажный корм только с утра. Давайте тогда разделим функцию eat на завтраки и остальные приемы пищи:
class BoryaCoolCat:
def morning_eat(self):
print("Омномном")
def eat(self):
print("Не люблю сухой корм")
def play(self):
print("Тыгыдык")
def save_to_database(self):
# Код для сохранения Борика в базу данных
pass
Теперь допустим, одной из игрушек нашего здоровяка станет утренняя еда со стола. Чтобы отразить это в коде, разделим функцию play, как выше сделали это с функцией eat:
class BoryaCoolCat:
def morning_eat(self):
print("Омномном")
def eat(self):
print("Не люблю сухой корм")
def morning_eat_play(self):
print("О конфетки, буду катать их по полу!")
def play(self):
print("Тыгыдык")
def save_to_database(self):
# Код для сохранения Борика в базу данных
pass
Уже сейчас видно, что код становится не очень-то понятным. Если не знать историю его написания, трудно догадаться, чем morning_eat отличается от morning_eat_play. Теперь сделаем нашего котяру чуть более солидным — объединим методы с похожей тематикой в классы. Те, что связаны с едой (morning_eat и eat) положим в класс CatFeeding. Таким образом, этот класс будет иметь только одну причину для изменения — кормежку. Аналогично, функции morning_eat_play и play перенесем в класс CatPlay, а save_to_database — в CatDatabase.
class CatFeeding:
def morning_eat(self):
print("Омномном")
def eat(self):
print("Не люблю сухой корм")
class CatPlay:
def morning_eat_play(self):
print("О конфетки, буду катать их по полу!")
def play(self):
print("Тыгыдык")
class CatDatabase:
def save_to_database(self, cat):
# Код для сохранения объекта cat в базу данных
print(f"{cat.name} сохранён в базу данных.")
Отлично, теперь каждый из классов имеет только одну причину для изменения — кормежка, игры или сохранение в БД! Но тут главное — знать меру и не наплодить классов под каждый метод.
Принцип открытости / закрытости
Если кот научится шипеть, он не должен разучиться мяукать.
Принцип открытости / закрытости (Open/Closed Principle, OCP) — программные сущности (классы, модули, функции и т. д.) должны быть открыты для расширения, но закрыты для модификации.
Расширения, модификации, бла-бла-бла… Сейчас с помощью Бори во всем разберемся! Одарим нашего парнишку речью:
class BoryaCoolCat:
def speak(self):
return "Мяу!"
Немного пожив с ним, я поняла, что у него с утра есть два настроения: ласковый милашка и киборг-убийца. Будет странно, если и в том, и в другом случае он будет болтать одинаково. Давайте попробуем усовершенствовать Боряна:
class BoryaCoolCat:
def speak(self, nice_mood: bool):
if nice_mood:
return "Мур-мур-мур"
return "Шшшшшшш"
Мы видим, как наша функция становится больше, в ней становится легче сделать ошибку. Если у Бори появится еще какое-то настроение, то функция будет разрастаться. Давайте накинем solidности.
class BoryaCat:
def speak(self):
return "Мяу!"
class NiceCat(BoryaCat):
def speak(self):
return "Мур-мур-мур"
class AngryCat(BoryaCat):
def speak(self):
return "Шшшшшшш"
Посмотрите, как теперь выглядят наши классы. Они открыты для добавления новой логики, но закрыты для изменения текущей. Изменение исходной логики — очень опасная процедура. Метод может использоваться в разных местах в коде, при изменении очень трудно найти все ошибки. А вот если мы будем не изменять изначальную логику, а добавлять новые кейсы, работающий код не сломается, и мы сможем реализовать новый функционал без проблем!
Принцип подстановки Барбары Лисков
Если мы любим игрушки, то и мышек, и фантики.
Принцип подстановки Барбары Лисков (Liskov Substitution Principle, LSP) утверждает, что объекты подкласса должны быть взаимозаменяемыми с объектами суперкласса без изменения желаемых свойств программы. Это означает, что если класс S является подклассом класса T, то объекты класса T должны быть заменяемыми объектами класса S без нарушения корректности программы.
Пожалуй, самый непонятный принцип из всех. Когда я прочла его впервые, почувствовала себя на матане на первом курсе. Какие S? какие T? Классы, подклассы… Вот не сиделось же тебе, Лисков, на месте. Но готовьтесь, сейчас мы победим этот принцип раз и навсегда!
Давайте сделаем класс Бориных вещичек. В нем создадим метод, который будет возвращать, как Боря радуется и любит свои игрушки.
class BoryasStuff:
def enjoy(self):
print("Ура")
У Бори есть фантик, мои руки и все, что лежит на столе. Все это — его вещи, поэтому они должны наследоваться от BoryasStuff.
class CandyWrapper(BoryasStuff):
def enjoy(self):
print("Ура, шелестеть!")
class MommysHand(BoryasStuff):
def enjoy(self):
print("Ура, царапать!")
class TableThings(BoryasStuff):
def enjoy(self):
print("Урааа, скидывать на пол!")
Недавно я купила Боре дорогущую мышь, которая умеет бегать от него и издавать звуки. Но, как водится, чем дороже игрушка, тем меньше она Борю интересует. Так что имплементация ее будет выглядеть так:
class CoolMouse(BoryasStuff):
def enjoy(self):
raise NotImplementedError("Какая мышь? Где мой трижды погрызанный фантик?")
Вот здесь и нарушается принцип Барбары Лисков. Дело в том, что согласно нему мы должны уметь пользоваться всеми дочерними классами так же, как и родительскими. Но в классе BoryasStuff в отличие от дочернего класса CoolMouse метод enjoy выполняется без проблем.
Если мы можем вызвать BoryasStuff().enjoy(), то у нас должна быть возможность вызвать и CoolMouse().enjoy(). Для соблюдения принципа мы можем или исключить метод enjoy из BoryasStuff, или не наследовать CoolMouse от BoryasStuff, вот и все!
class BoryasStuff:
pass
class CandyWrapper(BoryasStuff):
def enjoy(self):
print("Ура, шелестеть!")
class MommysHand(BoryasStuff):
def enjoy(self):
print("Ура, царапать!")
class TableThings(BoryasStuff):
def enjoy(self):
print("Урааа, скидывать на пол!")
class CoolMouse(BoryasStuff):
def hate(self):
print("Какая мышь? Где мой трижды погрызанный фантик?")
Принцип разделения интерфейсов
Незачем тебе умение плавать, если ты никогда не будешь это делать.
Принцип разделения интерфейсов (Interface Segregation Principle, ISP) гласит, что клиенты не должны зависеть от интерфейсов, которые они не используют.
Как-то мы мелочимся с выборами классов. Давайте создадим общий интерфейс котов. Что они умеют? Бегать, плавать, ну и, конечно же, орать в 5 утра.
class Cat:
def run(self):
pass
def swim(self):
pass
def morning_yell(self):
pass
Но вот мой Борик — домашний крепыш. Однажды плавал в ванной и ему очень не понравилось, мы решили его не мучить. Но если мы наследуем Борю от Cat, теоретически кто-то может заставить его плавать, чего ему явно не хотелось бы. Давайте разделим интерфейсы, чтобы мы могли наследовать только то, что нужно:
class Walkable:
def run(self):
pass
class Hateable:
def morning_yell(self):
pass
class Swimmable:
def swim(self):
pass
Теперь все в порядке! Можем просто наследовать Борю от Walkable и Hateable. В этом и есть принцип разделения интерфейсов. Мы не должны наследоваться от того, что не используем.
Принцип инверсии зависимостей
То, что коты мяукают, не влияет на всех остальных зверей. Но то, что животные издают звуки, влияет на котов.
Принцип инверсии зависимостей (Dependency Inversion Principle) гласит:
- «Модули верхнего уровня не должны зависеть от модулей нижнего. Оба должны зависеть от абстракций»,
- «Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций».
Так много слов и так трудно найти в них смысл. Разберем на примере использование этого принципа. Большинство животных издает какие-то звуки. Давайте напишем интерфейс для этого:
from abc import ABC, abstractmethod
class Sound(ABC):
@abstractmethod
def make_sound(self):
pass
Теперь давайте определим кошачий звук:
class CatSound(Sound):
def make_sound(self):
return "Мяу!"
Смотрите, кошачий звук наследуется от звуков животных. Все выглядит логично. Теперь давайте в очередной раз имплементируем Борю:
class BoryaCoolCat:
def __init__(self, sound: Sound):
self.sound = sound
def speak(self):
return self.sound.make_sound()
Заметили самый сок? В конструкторе класса мы получаем звук типа Sound. Значит, если мы в какой-то момент решим, что Боря не кот, а, например, крокодил, нам не нужно будет переписывать класс BoryaCoolCat. Достаточно просто передать в него любой другой класс, который наследуется от Sound!
Этот принцип очень неплохо работает в больших проектах. Тут на помощь приходит DI-контейнер. Возможно, в следующих статьях затрону эту занятную тему.
Заключение
В завершение нашего solidного путешествия по принципам и мискам хочется подчеркнуть, что все это — не просто набор правил. Да, ты можешь выучить их к экзамену или собесу и не вспоминать больше. Но они и правда помогают создавать более качественный и поддерживаемый код.
Хороший код, как и котики, требует внимания и заботы. Применяя принципы SOLID, мы не только улучшаем его структуру, но и делаем более понятным для других разработчиков (а также для нас самих в будущем). В конечном итоге, это позволяет сосредоточиться на решении задач, а не на борьбе с последствиями наших костылей.
Создание качественного кода — это не просто задача, а искусство. Надеюсь, эта статья вдохновила вас взглянуть на принципы SOLID с новой стороны и начать применять их в своей практике.
Комментарии (55)
eulampius
22.10.2024 10:11На мой взгляд в S-примере весь объектно-ориентированный дизайн развалился. Что несколько противоречит его идее.
Здесь, наверное, разумно было бы выделить функциональные классы. Например, "сохранятель в бд", "кормилец" и т.д. Киса для них будет аргументом без оо-связи, ну либо они будут аргументом для кисы )
shaykemelov
22.10.2024 10:11SRP не говорит о том, что модуль должен делать одну задачу.
Сам Мартин написал об этом в своей книге Clean Architecture:
Of all the SOLID principles, the Single Responsibility Principle (SRP) might be
the least well understood. That’s likely because it has a particularly inappropriate
name. It is too easy for programmers to hear the name and then assume that it
means that every module should do just one thing.дальше пишет
Make no mistake, there is a principle like that. A function should do one, and
only one, thing. We use that principle when we are refactoring large functions
into smaller functions; we use it at the lowest levels. But it is not one of the
SOLID principles—it is not the SRP.
Nuflyn
22.10.2024 10:11Я в ООП стиле не пишу, прост зашел поглядеть на котика)
Andrey_Solomatin
22.10.2024 10:11Я в ООП стиле не пишу
А если найду?
Шутки в сторону: идеальной парадигмы нет. Иногда и ООП на что-то годится.
Razoomnick
22.10.2024 10:11Я понимаю, что примеры максимально упрощенные и утрированные, но в процессе упрощения потерялась основная идея объектно-ориентированного программирования: работа с абстракциями и обобщениями.
И получилось, что класс способен описывать только одного конкретного кота Бориса. Хотя, по идее, должен уметь описывать любого кота.
А потом получилось, что для описания одного конкретного кота в двух разных настроениях нужно три класса (по одному на настроение плюс базовый). А в трех - уже четыре класса.
И как следствие из этого, настроение кота Бориса не может меняться. Кот Борис в другом настроении - это другой экземпляр кота Бориса.
Вероятно, это может сбить с толку на этапе, когда как раз ознакомление с принципами солид актуально.
souls_arch
22.10.2024 10:11Я бы в классе кота ввел поля String name, Integer age, Enum Color, Enum Breed, Address address, List <Action> actions , к примеру. И уже в классе Action рассматривал бы разные действия питомца. Его, в свою очередь, можно унаследовать от абстрактного класса или интерфейса AnimalActions. И уже в классе Action, если оно, конечно, сильно надо (?) Заводить классы Feed, Play, Ability. А вот saveToDb() конкретно к классу кота вообще не должен иметь никакого отношения.
Ваш кот прекрасен! Стремитесь, чтобы и код был такмм же.
sswwssww
22.10.2024 10:11class CatFeeding: def morning_eat(self): print("Омномном") def eat(self): print("Не люблю сухой корм") class CatPlay: def morning_eat_play(self): print("О конфетки, буду катать их по полу!") def play(self): print("Тыгыдык") class CatDatabase: def save_to_database(self, cat): # Код для сохранения объекта cat в базу данных print(f"{cat.name} сохранён в базу данных.")
мои глаза...А ведь все что нужно было вам сделать для демонстрации SRP принципа это:
class CoolBorya(Cat): def eat(self): print("Омномном") def play(self): print("Тыгыдык") class CatRepository: def add(cat: Cat): ...
sswwssww
22.10.2024 10:11А касаемо Допустим, ветеринар сказал Боре есть влажный корм только с утра. Давайте тогда разделим функцию eat на завтраки и остальные приемы пищи - нет никакого смысла создавать отдельные методы с повторяющимся частично кодом под это дело, можно просто в зависимости от времени суток реагировать на разный корм разным образом(ничего страшного в if-ах нет если их максимальное количество продиктовано бизнесом и реальным миром. И они никак не нарушают SRP):
class Time_Of_Day(StrEnum): morning = auto() afternoon = auto() evening = auto() night = auto() time_of_day = Time_Of_Day.morning # захардкоим чтобы не переусложнять пример class Food: def __init__(self, dish): self.dish = dish class Dry(Food): pass class Wet(Food): pass class CoolBorya(Cat): def eat(self, food: Food): if time_of_day == Time_Of_Day.morning and isinstance(food, Dry): self.hiss() return print(f"<ест {food}}> омномном") def hiss(): print("шшшшшшш!!") # чтобы не усложнять пример напишем прямо тут создание и "использование" нашего кота borya = CoolBorya() if time_of_day == Time_Of_Day.morning: borya.eat(food=Dry(dish="влажный корм")) else: borya.eat(food=Wet(dish="сухой корм"))
При необходимости, в зависимости от бизнес домена(нам нужно запрогать как кот действует сам по себе или это мы за ним ухаживаем) мы можем перенести логику проверки комбинации "корм + время суток" в сам метод eat
(p.s. кстати, ваш метод morning_eat_play в финальном примере про SRP это и есть нарушение SRP)
guryanov
22.10.2024 10:11Подушню немного. Вот у всем известного товарища, который книги пишет и эти принципы продвигает совсем другое написано про принцип единственной ответственности. Класс должен обслуживать только одно заинтересованное лицо. Там был пример про рабочие часы: в компании рабочие часы сотрудника надо знать сотруднику бухгалтерии для расчета ЗП и, к примеру, службе безопасности. И эти функции/классы для расчета рабочих часов не должны быть одним. Нужна отдельная функция для расчета рабочих часов для бухгалтерии и отдельная - для службы безопасности.
Если для бухгалтерии надо 500 функций - их можно в один класс запихнуть и не будет противоречия с этим принципом.jizuscreed
22.10.2024 10:11ага, только не лицо, а "актора" - то есть лицо или группу лиц, единообразно использующих систему. И смысл srp ни больше ни меньше просто в снижении незапланированного импакта и потенциального регресса на других акторов. Собственно, как и весь солид, который не про "красивую" или "понятную" архитектуру, а именно про снижение регресса и незапланированного импакта.
Это вообще самая забавная часть всего солида, что мало кто читал Мартина и большая часть воспроизводит пересказы с разной степенью искаженности.sshikov
22.10.2024 10:11про снижение регресса и незапланированного импакта.
Ну как бы, примерно 90% авторов таких статей (по моим субъективным оценкам) вообще не помнят о назначении принципов SOLID (которое изложено во втором предложении скажем статьи википедии). Понимаете? Большая часть авторов, решивших писать про эту тему, не дочитала формулировку, что же такое SOLID, дальше первого предложения.
jizuscreed
22.10.2024 10:11ну, справедливости ради, насколько я помню, Мартин в "чистой архитектуре" тоже явно это не проговаривает, больше отделываясь общими словами типа "гибкость, легкость расширения и поддержки". Хотя с тем же SRP можно привести примеры граничных случаев, когда эти самые "гибкость, легкость расширения и поддержки" он как раз таки ломает в угоду изоляции импакта.
sshikov
22.10.2024 10:11А что для вас в этой формулировке не ясно? Это вполне себе цель. И она не совпадает с просто целью "сделать хорошо".
"гибкость, легкость расширения и поддержки" он как раз таки ломает в угоду изоляции импакта.
Вот тут я не уловил. Изоляция импакта, на мой взгляд, это нормальная формулировка всех принципов SOLID в форме одного принципа (просто я это обычно называю чуть длиннее и другими словами, но думаю что мы об одном и том же). Т.е. все принципы, по-идее, направлены на то, чтобы изолировать влияние изменений минимально необходимым куском кода, потому что эта изоляция и дает легкость расширения и поддержки. Просто разные принципы - про разные причины изменений и разные способы их "расползания" куда не следует.
Если мы импакт таки изолировали, то как и где легкость расширения сломалась?
jizuscreed
22.10.2024 10:11А что для вас в этой формулировке не ясно? Это вполне себе цель. И она не совпадает с просто целью "сделать хорошо".
Как цель - да, вполне норм. Но ведь и Мартин и ваша цитата из вики в треде ниже не про цель, а про свойство SOLID. Типа solid вот это все дает. А это не так.
Т.е. все принципы, по-идее, направлены на то, чтобы изолировать влияние изменений минимально необходимым куском кода, потому что эта изоляция и дает легкость расширения и поддержки. Просто разные принципы - про разные причины изменений и разные способы их "расползания" куда не следует.
Я вот знал, что вы спросите. Представьте себе, что у нас в системе есть два различных актора (к примеру, бугхгалтерия и HR) у которых на текущий момент полностью идентичны требования в некой функциональности. Они могут разойтись в будущем, конечно, но сейчас идентичны. Что тут надо сделать с точки зрения компоновки классов? С точки зрения SRP нужно полностью изолировать функциональность друг от друга, но сейчас она совпадает, так что придется сделать пошлый копипаст. С точки зрения гибкости (и, прости госспади, DRY) общее копировать не нужно, нужно просто его выделить, а различия сделать отдельно. Но тогда два актора будут использовать один общий класс и мы нарушаем SRP. Пример слегка синтетический, но общий конфликт передает.
В общем, если коротко, изоляция импакта зачастую мешает переиспользованию. Невозможность переиспользования ломает гибкость.sshikov
22.10.2024 10:11Типа solid вот это все дает. А это не так.
В цитате что я привел, Intended - это намерения, а не обещания.
Что тут надо сделать с точки зрения компоновки классов?
Возможно что ничего. А зачем? Если вы следуете цели повысить гибкость и сопровождаемость, задайте себе вопрос - а оно щас легко сопровождается? Если да - забейте на эту цель, зачем тратить на нее ресурсы? Если мне сейчас такой код норм - значит у меня нет цели повышать его понятность, пока я один разработчик на проекте. Если я завтра найму миддла - возможно у меня такая цель появится, а сейчас ее нет. Ну во всяком случае я так обычно подхожу.
Andrey_Solomatin
22.10.2024 10:11Я вот знал, что вы спросите. Представьте себе, что у нас в системе есть два различных актора (к примеру, бугхгалтерия и HR) у которых на текущий момент полностью идентичны требования в некой функциональности. Они могут разойтись в будущем, конечно, но сейчас идентичны. Что тут надо сделать с точки зрения компоновки классов?
Делать надо было раньше. Сейчас уже ничего не надо, работает не трогайте. Когда придут новые требования нужно будет избежать соблазна его поменять. Как только они расходятся нужно будет разделить на разные классы. Не обязательно скопировать, можно и отнаследоваться и через композицию. Всё это делать нужно держа в голове open-closed, изначальный класс не должен менять поведение.jizuscreed
22.10.2024 10:11мне так нравятся императивные нотки в вашем комменте))
во-первых, это гипотетический пример, ничего трогать я и не собираюсь)
а во-вторых, раскройте пожалуйста мысль, что тут можно было сделать раньше, если у разных акторов идентичная функциональность? Это в любом случае или переиспользование, а значит общая для разных акторов логика в одном месте, а значит нарушение SRP. Или дублирование.
Это принципиальная проблема "переиспользование XOR изоляция изменений". В контексте солида и SRP про нее обычно не вспоминают, зато она часто всплывает в обсуждении микросервисов, например, где обычно считается, что лучше уж дублирование, чем куча общих зависимостей, из-за которых страдает независимость разработки и деплоя.
Либо, если вспомнить метрику "хрупкости" кода и афферентные/эфферентные связи, то всплывет та же самая проблема - переиспользование повышает "хрупкость" кода, так как чем больше входящих связей, тем больше неконтролируемый импакт при изменениях.
То есть это не проблема конкретно солида и SRP. Это именно проблема "переиспользование XOR изоляция изменений". Ну или "переиспользование VS снижение сцепленности", можно и так сказатьAndrey_Solomatin
22.10.2024 10:11мне так нравятся императивные нотки в вашем комменте))
во-первых, это гипотетический пример, ничего трогать я и не собираюсь)
Я тоже не с реальным кодом работаю.
SOLID он о том как двигаясь вперёд ничего не сломать. Солидизировать текущий код смысла нет, кроме как перед следующими изменениями.во-вторых, раскройте пожалуйста мысль, что тут можно было сделать раньше, если у разных акторов идентичная функциональность?
Наверно в плане солида можно пока их считать одним актором.
Это принципиальная проблема "переиспользование XOR изоляция изменений".
Это действительно проблема. Солид он про дублирование. Меньше ломаешь старого, меньше пересечений с изменениями других людей.
На обратной стороне существуют библиотеки. Код которые пишется чтобы быть переиспользуемым. В библиотеках хорошей практикой считается тестирование и поддержка обратной совместимости. И нормально написать библиотеку сложнее чем писать код который решает задачи.
Где границу проводить нужно каждый раз решать по месту.
Пока у вас не будет пары-тройки дубликатов, хорошая библиотека не особо получится, так как её надо будет часто менять. Да и можно не разобраться, что действительно переиспользуется, а что не очень.jizuscreed
22.10.2024 10:11мне кажется, что вы пришли к распространенному подходу - использовать солид, пока все легко и от него нет проблем, а когда начнутся - смотреть по ситуации. Что опять же возвращает нас к моему изначальному тейку - солид не совсем про гибкость, он про разделение, а гибкость он может как повышать, так и снижать, в зависимости от ситуации
ilyatom
22.10.2024 10:11Чем больше я читаю такие статьи, и, что немаловажно, комментарии под ними, тем больше пониманию, что 99% прогеров на самом деле не понимают SOLID. Я тоже :)
sshikov
22.10.2024 10:11А вы прочтите хотя бы статью в википедии. Я серьезно. Вот, смотрите, первый абзац:
In software programming, SOLID is a mnemonic acronym for five design principles intended to make object-oriented designs more understandable, flexible, and maintainable.
Только не читайте русский перевод, посмотрите вот сюда, в английский. Вы видите назначение принципов? Что вам тут непонятно? Во-первых, это акроним, т.е принципов пять. Во-вторых, назначение принципов сделать код более понятным, гибким и сопровождаемым.
В принципе, такого понимания достаточно для того, чтобы вы четко знали - вам это нужно или нет. Вы свой код понимаете? Если да, вы можете не заморачиваться этими принципами. Если нет - то вы можете попробовать применить некоторые из них, чтобы сделать его понятнее (но вы не обязаны - вам ничего не будет, если вы это не сделаете). И тем более вы не обязаны применять их все.
jizuscreed
22.10.2024 10:11простите, влезу, но вот вы поцитировали пример типичного маркетинга про solid. Не делает он код более понятным, вообще ни разу. Более того, если вы попробуете пописать код честно так, как описывал Мартин (с этой его ультимативной композицией), вы, возможно, заметите, что ориентироваться по классам стало наоборот сложнее (по крайней мере многие мои коллеги замечали). И с гибкостью есть нюансы, в некоторых случаях может и вредить.
Все что реально делает solid - изоляция незапланированного импакта. Все. В качестве побочного эффекта в 95% случаев повышается гибкость (просто за счет разделения всего и вся), но не всегда. А поддерживаемость = гибкость + понятность.sshikov
22.10.2024 10:11Гибкость и сопровождаемость - это декларируемая цель. Принципы эти - они ни разу не гарантируют ничего, это не законы физики. Если вам скажут, что они это гарантируют - это будет брехня. И маркетинг.
И в любом случае применение или неприменение принципов (и выбор того, какой конкретно применить) - за автором. И делать это без применения головы я бы не советовал категорически. А если вы в 95% случаев получаете повышение гибкости - так это более чем хороший результат (особенно - если вы этого реально хотели ;)
изоляция незапланированного импакта. Все.
Так это очень много, на самом деле. Это значит, что вы можете вносить изменения в локальный кусок кода, и не беспокоиться о том, что сломается соседний. Во всех бы проектах так было.
jizuscreed
22.10.2024 10:11И делать это без применения головы я бы не советовал категорически.
Это значит, что вы можете вносить изменения в локальный кусок кода, и не беспокоиться о том, что сломается соседний
на мой взгляд как раз "делать с применением головы" чтобы "не беспокоиться о том, что сломается соседний" и означает - понимать вышеуказанные противоречия, понимать, когда у тебя вылезут эти оставшиеся 5 процентов и понимать, что ты с этим будешь делать и почему.
whoisking
22.10.2024 10:11Так и есть. Солид это карго-культ, он должен быть пересмотрен. Во-первых, за ним стоит один человек и при спорах про солид между любого уровня разработчиками с некоторых пор аргументом становится призыв к авторитету этого человека, а не к каким-то объективным метрикам или научным статьям. Во-вторых, он трактуется всеми по-разному, сложно доказать и показать его реальную необходимость и положительные последствия его применения к реальному коду. Зачастую, код, на котором показываются примеры применения солида, можно оптимизировать более всем привычными конструкциями структурного/процедурного/ООП/функционального программирования.
Andrey_Solomatin
22.10.2024 10:11SOLID это маркетинг. Попытка рассказать важные идеи в виде лёгких фраз. Попытка провалилась, так как простого объяснения нет. (Про то, что термины не удачные, это Дядя Боб сам говорит)
Чтобы начать что-то понимать нужно написать много кода, и потом его поддерживать и развивать. На коротких дистанция от него толку не так много.
Я навидался разного кода. Есть еще люди которые функции по сотни строк пишут. Таким SOLID просто на зайдёт. Он в общем-то о разделении на части, просто на более высоком уровне.
nin-jin
22.10.2024 10:11Наследовать ли маленькую коробку от большой или большую от маленькой? Игрушка, влезающая в маленькую, влезет и в большую. А вот Борис, влезающий во вторую, в первую уже не влезет.
Femistoklov
22.10.2024 10:11Если основа - вместимость, то надо наследовать большую от маленькой. Маленькая вмещает, а большая ещё лучше вмещает.
Если основа - ограничение вместимости, то маленькую от большой. У большой ограничение, а у маленькой ещё лучше ограничение.
Andrey_Solomatin
22.10.2024 10:11Не наследовать.
Можно наследовать оба класса от БазовойКоробки, если у них есть общий код.
Да и вообще вопрос нужно ли тут два класса? Одного класса и метода BorisFit может и хватить.nin-jin
22.10.2024 10:11То есть подставить большую коробку вместо маленькой нельзя будет?
Вы предлагаете в рантайме паниковать, если что-то не влезает в коробку?
Andrey_Solomatin
22.10.2024 10:11То есть подставить большую коробку вместо маленькой нельзя будет?
Если вы используете интерфейс то можно.
Вы предлагаете в рантайме паниковать, если что-то не влезает в коробку?
Большая коробка не гарантирует, что котик туда влезет. Паниковать не надо, просто обработать такую ошибку.
nin-jin
22.10.2024 10:11Ладно, вот правильный ответ: https://page.hyoo.ru/#!=dynrai_6jc0xi
Femistoklov
22.10.2024 10:11Если честно, не очень понятная статья. Напр., в разделе никак не раскрывается, какой смысл иметь подтипы коробки Pet, если и в супертип засовывается Cat или Dog - получается, весь нужный функционал уже есть в супертипе. Выглядит как бессмысленный дизайн, что ставит весь пример под сомнение.
В примере C# как-то лихо перескочили от вариантности параметров к вариантности параметров-типов в обобщённых типах.
gochaorg
22.10.2024 10:11Извиняюсь, но я по душню...
первое что бросается в глаза - классы, которые выводят сообщения, не случайные сообщения, а строго определенные..
ну вот как бы я не любил котов, но проблема в объяснениях, зачем такое упрощение ? я конечно понимаю что бы доступно было, но не в ущерб же здравому смыслу, ну то есть, тот кто будет читать, он как то, должен извлечь для себя практическую пользу, sorry за резкость
Просто, тут вот... ключевое... это например напишите программу FizzBuss (тут не к вам.. просто для демонстрации здравости или отсутствия) ... так, как бы всем результат известен, зачем писать программу если результат известен, он прям в условии повторяется (FizzBuss), ну вот хоть немного прагматики, например, каких цифр больше в первых 10000 цифр числа Пи - тут, уже есть... хоть смысл написать программу, по скольку считать долго вручную
1) Первое что в голову прилетает, зачем вообще этот код, он, ничего не делает. достаточно txt файла..
2) А зачем классы в первом примере ? зачем они вообще, когда все это же достаточно функций или просто txt
ну хорошо, захотелось про ООП, ну давай те классы будут хоть, что-то содержать... например окраску #RGB... длину усов, лап, качество зрения, и еще 10 параметров...
и вот... у нас бы были бы данные, с которыми работала программа, а так, просто txt
потом объяснение того же single...
например, в примере есть разделение по времени суток, чем кормить, как кормить.... данные о коте из базы, а его поведение или кормежка зависит от времени суток - вот другой источник данных, который вне базы... а вот чем кормить, зависит от болезней желудка, а вот длина усов ни как не влияет, вот третий фактор... и есть метод... кормить, что бы ответить чем кормить и когда кормить - для этого нужно несколько разных видов информации... время, состояние желудка кота, и меню..., а вот другой вид информации, как то, окрас или длина усов не влияет....
Принципиальное разделение информации, по области ее применения, вот никогда не слышу в этих упрощенных объяснениях... это все время отдается на откуп читателю... пусть мол сам догадается, что хотел сказать автор и как это применять на практике, в коде
извиняюсь... не смог удержаться, котики понравились, я на оборот рад, что есть статья и возможно я не так читаю и не так понимаю, было бы время, я может по другому описал бы пример с котиками и solid
k4ir05
22.10.2024 10:11Принцип единственности ответственности (Single Responsibility Principle, SRP) гласит: каждый класс или модуль должен иметь только одну причину для изменения. То есть он должен выполнять только одну задачу.
Простите, но как вы из первого предложения вывели второе?
Причина для изменений - это люди. Вот тут разъяснение этого принципа от самого автора.
VPryadchenko
22.10.2024 10:11Что называется, начали за здравие, закончили за упокой
k4ir05
22.10.2024 10:11Очень интересно, но ничего не понятно.
VPryadchenko
22.10.2024 10:11Принцип единственности ответственности (Single Responsibility Principle, SRP) гласит: каждый класс или модуль должен иметь только одну причину для изменения.
Да
То есть он должен выполнять только одну задачу.
Нет
k4ir05
22.10.2024 10:11А, я подумал, что это мне адресовано. У меня то поэтому и возник вопрос. Почти в каждой подобной статье авторы, почему-то, делают такой вывод.
wasil
22.10.2024 10:11Принцип single responsibility явно нарушен судя по названию :-)
def morning_eat_play(self)
olivera507224
22.10.2024 10:11Самое сложное в SOLID - это вспомнить расшифровку аббревиатуры.
Andrey_Solomatin
22.10.2024 10:11Не плохая стратегия выучить это для собесов. Просто базовые формулировки. Большинство дальше и не захочет спрашивать, тут можно уплыть всем дружно.
Andrey_Solomatin
22.10.2024 10:11Допустим, ветеринар сказал Боре есть влажный корм только с утра.
Боря только слова эти пропустил мимо ушей. Единственная ответственность Бори это кушать.
А что и когда, это будет решать его FoodProvider. Что даст ему то даст.
У FoodProvider есть два заказчика Mommy и ветиринар. И требования у них пересекаются. Вот тут-то самое время накидать классы.
Может сработать такой пайплайн из трёх частей:available_food = Fridge().get_all_food_at_home() allowed_food = Veterinar().filter_healthy_food(available_food ) cat_food = Mommy().give_yammy(allowed_food ) if available_food else Mommy().run_for_food(Veterinar().get_recommendations()
Получается сложно. Зато теперь после каждого визита к ветеринару нужно будет поменять только один класс.
Стоит ли это гибкость той дополнительной сложность? Этот вопрос заказчику.
whoisking
Я бы просто остановился на morning_play. То, что там как-то пересекается с едой - это не важно, важно что это утренняя игра, что это игра и всё. Сегодня с едой, а через неделю с засохшими какашками.
И не надо классы добавлять. Нейминг очень важен. Хороший нейминг даст больше профита. Плюс название метода всё равно осталось путающим. Да и вообще, во втором случае будет food, а не eat.
2128507
слишком СОЛИДно получилось в итоге, денормализация нужна с момента различия настроения котов, имхо)