Русский
Русский
English
Статистика
Реклама

Принципы разработки

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

05.05.2021 14:10:44 | Автор: admin
Фото от https://unsplash.com/@lazycreekimagesФото от https://unsplash.com/@lazycreekimages

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

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

Мне лично нравится идея, лежащая в основе принципов SOLID и я многому из нее научился.

Тем не менее

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

Чтобы проиллюстрировать, как эта проблема влияет на принципы SOLID, давайте рассмотрим каждый из принципов.

Принцип единственной ответственности

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

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

class Calculate {    fun add (a, b) = a + b    fun sub (a, b) = a - b    fun mul (a, b) = a * b    fun div (a, b) = a / b }

Для некоторых это идеально, так как у него одна обязанность - вычислить.

Но кто-то может возразить: Эй!Он делает 4 вещи!Сложить, вычесть, умножить и разделить!

Кто прав?Я скажу, это зависит от обстоятельств.

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

Но кто знает, в будущем кто-то может просто захотеть выполнить операцию сложения без необходимости использовать классCalculate.Тогда код выше нужно изменить!

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

Принцип открытости/закрытости

Программные объекты ... должны быть открыты для расширения, но закрыты для модификации.

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

Давайте посмотрим на код ниже:

interface Operation {   fun compute(v1: Int, v2: Int): Int}class Add:Operation {   override fun compute(v1: Int, v2: Int) = v1 + v2}class Sub:Operation {   override fun compute(v1: Int, v2: Int) = v1 - v2}class Calculator {   fun calculate(op: Operation, v1: Int, v2: Int): Int {      return op.compute(v1, v2)   } }

В приведённом выше коде есть классCalculator, который принимает объект Operation для вычисления.Мы можем легко расширить этот класс с помощью операций Mul и Div без изменения кода самого классаCalculator.

class Mul:Operation {   override fun compute(v1: Int, v2: Int) = v1 * v2}class Div:Operation {   override fun compute(v1: Int, v2: Int) = v1 / v2}

Отлично, мы соблюдаем принцип открытости/закрытости!

Но однажды появилось новое требование. Cкажем, нам нужна новая операция Inverse.Она просто возьмет один операнд, например X, и вернет результат 1/X.

Кто на земле мог подумать, что это произойдет?Мы исправили функцию сompute нашего рабочего интерфейса, чтобы она имела 2 параметра.Теперь нам нужна новая операция, у которой всего 1 параметр.

Как теперь избежать модификации класса Calculator?Если бы мы знали это заранее, возможно, мы не писали бы наш класс калькулятора и интерфейс операций как таковые.

Изменения никогда нельзя полностью спланировать.Если бы это можно было полностью спланировать, возможно, программное обеспечение нам больше и не понадобилось бы :)

Принцип подстановки Лискоу

Функции, использующие указатели или ссылки на базовые классы, должны иметь возможность использовать объекты производных классов, не зная об этом.

Когда мы были молоды, мы узнавали основные атрибуты животных.Они подвижны.

interface Animal {   fun move()}class Mammal: Animal {   override move() = "walk"}class Bird: Animal {   override move() = "fly"}class Fish: Animal {   override move() = "swim"}fun howItMove(animal: Animal) {   animal.move()}

Это соответствует принципу замены Лискоу.

Но мы знаем, что сказанное выше не совсем правильно.Некоторые млекопитающие плавают, некоторые летают, а некоторые птицы ходят.Итак, мы меняем код на:

class WalkingAnimal: Animal {   override move() = "walk"}class FlyingAnimal: Animal {   override move() = "fly"}class SwimmingAnimal: Animal {   override move() = "swim"}

Круто, все по-прежнему хорошо, так как наша функция все еще может использовать Animal:

fun howItMove(animal: Animal) {   animal.move()}

Но сегодня я кое-что обнаружил.Есть животные, которые вообще не двигаются.Они называются Sessile.Может нам стоит изменить код так:

interface Animalinterface MovingAnimal: Animal {   move()}class Sessile: Animal {}

Теперь это нарушит приведенный ниже код.

fun howItMove(animal: Animal) {   animal.move()}

У нас нет возможности гарантировать, что мы никогда не изменим функциюhowItMove.Мы можем достичь этого, основываясь на том, что мы знаем на данный момент.Но по мере того, как мы осознаем новые требования, нам нужно меняться.

Даже в реальном мире существует множество исключений.Мир программного обеспечения - это не реальный мир.Все возможно.

Принцип разделения интерфейса

Многие клиентские интерфейсы лучше, чем один интерфейс общего назначения.

Давайте посмотрим на животное царство.У нас есть интерфейс Animal, как показано ниже.

interface Animal {   fun move()   fun eat()   fun grow()   fun reproduction()}

Однако, как мы поняли выше, есть некоторые животные, которые не двигаются, и это Sessile.Поэтому мы должны выделить функциюmove как отдельный интерфейс.

interface Animal {   fun eat()   fun grow()   fun reproduction()}interface MovingObject {   fun move()}class Sessile : Animal {   //...}class NonSessile : Animal, MovingObject {   //...}

Затем мы хотели бы иметь еще и PlantВозможно, нам следует отделитьgrowиreproduction:

interface LivingObject {   fun grow()   fun reproduction()}interface Plant: LivingObject {   fun makeFood()}interface Animal: LivingObject {   fun eat()}interface MovingObject {   fun move()}class Sessile : Animal {   //...}class NonSessile : Animal, MovingObject {   //...}

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

Однако, кто-то начинает кричать: Дискриминация!Некоторые животные бесплодны, это не значит, что они больше не LivingObject!.

Похоже, теперь нам нужно отделитreproductionот интерфейсаLivingObject.

Если мы это сделаем, у нас будет буквально одна функция на один интерфейс!Это очень гибко, но может быть слишком гибко, если нам не нужно такое тонкое разделение.

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

Принцип инверсии зависимостей

Положитесь на абстракции, а не на что-то конкретное.

Мне нравится этот принцип, поскольку он относительно универсален.Он применяет идеюконцепции независимого решения, в которой некоторая сущность в программе хотя и зависит от класса, но по-прежнему независима от него.

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

Давайте посмотрим на пример ниже.Он действительно применяет принцип инверсии зависимостей.

interface Operation {    fun compute (v1: Int, v2: Int): Int    fun name (): String }class Add: Operation {    override fun compute (v1: Int, v2: Int) = v1 + v2    override fun name () = "Add" }class Sub: Operation {    override fun compute (v1: Int, v2: Int) = v1 - v2    override fun name () = "Subtract" }class Calculator {    fun calculate (op: Operation, v1: Int, v2: Int): Int {       println ("Running $ {op.name ()}")       return op.compute (v1, v2)    } }

Calculator Не зависит отAdd илиSub.Новместо этогоон выполняетAdd иSub , которые зависят отOperation.Это выглядит хорошо.

Однако, если кто-то из группы разработчиков Android использует его, это будет проблемой.println не работает в Android.Нам понадобитсяLod.d взамен.

Чтобы решить эту проблему, мы должны сделатьCalculator независящим напрямую от println.Вместо этого мы должны внедрить интерфейс Printer:

interface Printer {   fun print(msg: String)}class AndroidPrinter: Printer {   override fun print(msg: String) = Log.d("TAG", msg)}class NormalPrinter: Printer {   override fun print(msg: String) = println(msg)}class Calculator(val printer: Printer) {   fun calculate(op: Operation, v1: Int, v2: Int): Int {      printer.print("Running ${op.name()}")      return op.compute(v1, v2)   } }

Это решает проблему соблюдения принципа инверсии зависимостей.

Но если Android никогда не будет использовать этотCalculator, и мы создадим такой интерфейс заранее, возможно, мы нарушилиYAGNI.


TL; DR;

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

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

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

Программное обеспечение по своей природе МЯГКОЕ и сделать его навсегда следующим SOLID сложно.Для программного обеспечения применение принципов SOLID - это цель, а не судьба.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru