На прошлой неделе услышал фразу от нашего лида: "Если в компании работают по SOLID, то бегите с их собеседования". Но я же учил, что это основа всех приложений... ☝️
Решил узнать еще мнений и понять - почему так?
🟢Принцип единственной ответственности
«У класса не должно быть более одной причины для изменения». Другими словами, у каждого класса должна быть только одна ответственность
class Calculate:
def add(a: int, b: int) -> int:
return a + b
def sub(a: int, b: int) -> int:
return a - b
Вроде бы ответственность у класса одна - вычислять, но тут 2 разных метода 🤔
На самом деле, этот принцип работает в моменте - что нужно от класса на данный момент (YAGNI). Но что, если через месяц бизнесу потребуется, чтобы мы добавили методы умножения и деления?... 😈
🟢Принцип открытости/закрытости
«Программные объекты должны быть открыты для расширения, но закрыты для модификации»
class Operation(Protocol):
def compute(self, a: int, b: int) -> int:
...
class Add:
def compute(self, a: int, b: int) -> int:
return a + b
class Sub:
def compute(self, a: int, b: int) -> int:
return a - b
def calculate(op: Operation, a: int, b: int) -> int:
return op.compute(a, b)
Тут все ок, принцип открытости/закрытости соблюдается 😎
Но приходит бизнес и говорит - в следующем релизе нужно добавить расчет отношения a от 100 и b от 100... Но у нас же уже сделан интерфейс с 2 аргументами, а теперь надо еще и с одним 😫
🟢Принцип подстановки Барбары Лисков
«функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа, не зная об этом»
class Animal(Protocol):
@staticmethod
def move() -> None:
...
class Mammal:
@staticmethod
def move() -> None:
print("walk")
class Fish:
@staticmethod
def move() -> None:
print("swim")
def how_it_move(animal: Animal) -> None:
animal.move()
Все опять по принципу 👍 Но некоторые млекопитающие плавают или летают, а есть птицы которые ходят... А есть которые вообще не двигаются и наш метод move не подойдет для них 😍
Опять же, в моменте можно сделать по требованию, но спланировать потребности на будущее - невозможно 🤦♂️
🟢Принцип разделения интерфейса
«Программные сущности не должны зависеть от методов, которые они не используют»
class Animal(Protocol):
def move(self):
...
def eat(self):
...
def grow(self):
...
def reproduction(self):
...
Сделал ты такой по принципу класс, но тут вспоминаешь "есть же животные, которые не двигаются!" - делаешь отдельный класс для движимых животных с этим методом.
Приходит заказчик и просит добавить еще растения 😲 Отделяешь
grow и reproduction.Пользователи начинают строчить в техподдержку: "Почему вы не учитываете бесплодных животных???". Надо отделять метод
reproduction 😠Насколько надо гибко настроить интерфейсы, чтобы все учесть сразу?🤦♂️
🟢Принцип инверсии зависимостей
«Положитесь на абстракции, а не на что-то конкретное»
class Operation(Protocol):
@staticmethod
def compute(a: int, b: int) -> int:
...
@staticmethod
def name() -> str:
...
class Add:
@staticmethod
def compute(a: int, b: int) -> int:
return a + b
@staticmethod
def name() -> str:
return "Add"
def calculate(op: Operation, a: int, b: int) -> int:
print(f"Running {op.name}")
return op.compute(a, b)
Пример утрированный, но для понимания подойдет)
Наша программа должна запускаться на интерпретаторе от 3.6 и выше, так как f-строки поддерживаются только с этой версии. Но появляются заказчики, которые не могут обновиться, но хотят пользоваться и заплатить за наш продукт 🤑
Получается, мы должны предусмотреть использование и на старых версиях питона? А что, если этого никогда не случится, то мы нарушим YAGNI? 😄
Все это написано по материалам изученным в интернетах и не является дискредитацией SOLID. Он упрощает разработку и систематизирует ее, но упарываться им не стоит 100% 🧘
