Долгое время я задумывался, что же не в порядке с некоторыми частями кода. Раз за разом, в каждом из проектов находится некий «особо уязвимый» компонент, который все время «валится». Заказчик имеет свойство периодически менять требования, и каноны agile завещают нам все хотелки воплощать, запуская change request-ы в наш scrum-механизм. И как только изменения касаются оного компонента, через пару дней QA находят в нём несколько новых дефектов, переоткрывают старые, а то и сообщают о полной его неработоспособности в одной из точек применения. Так почему же один из компонентов все время на устах, почему так часто произносится фраза а-ля «опять #компонент# сломался»? Почему этот компонент приводится как антипример, в контексте «лишь бы не получился ещё один такой же»? Из-за чего этот компонент так неустойчив к изменениям?

Когда находят причину, приведшую к, или способствовавшую развитию такого порока в приложении, эту причину обозначают, как антипаттерн.
В этот раз камнем преткновения стал паттерн Strategy. Злоупотребление этим паттерном привело к созданию самых хрупких частей наших проектов. Паттерн сам по себе имеет вполне «мирное» применение, проблема скорее в том, что его суют туда где он подходит, а не туда, где он нужен. «Если вы понимаете о чем я» (с).

Классификация


Паттерн существует в нескольких «обличьях». Суть его от этого меняется не сильно, опасность его применения существует в любом из них.
Первый, классический вид — это некий долгоживущий объект, который принимающий другой объект по интерфейсу, собственно стратегию, через сеттер, при некотором изменении состояния.
Второй вид, вырожденный вариант первого — стратегия принимается один раз, на все время жизни объекта. Т.е. для одного сценария используется одна стратегия, для другого другая.
Третий вид, это исполняемый метод, либо статический, либо в короткоживущем объекте, принимающий в качестве входного параметра стратегию по интерфейсу. У «банды четырёх» этот вид назван как «Template method».
Четвертый вид — интерфейсный, ака UI-ный. Иногда называется как паттерн «шаблон», иногда как «контейнер». На примере веб-разработки — представляет из себя некий, кусок разметки, содержащий в себе плейсхолдер (а то и не один), куда во время исполнения отрендерится изменяемая, имеющая несколько разных реализаций, часть разметки. Параллельно разметке, в JavaScript коде также живут параллельные вью-модели или контроллеры, в зависимости от архитектуры, принятой в приложении, организованные по второму виду.

Общие черты этих всех случаев:
1)некоторая неизменяемая часть ака имеющая одну реализацию, далее, я буду называть ее контейнером
2)изменяемая часть, она же стратегия
3)место использования, создающее/вызывающее контейнер, определяющее какую стратегию контейнер должен использовать, далее буду называть это сценарием.

Развитие болезни


Поначалу, когда компонент, с использованием этого паттерна только реализовывался в проекте, он не казался таким уж плохим. Применен он был, когда было нужно создать две одинаковые страницы(опять же, на примере веб-разработки), которые лишь немного отличаются контентом в середине. Даже наоборот, разработчик порадовался, насколько красиво и изящно получилось реализовать принцип DRY, т.е. полностью избежать дублирования кода. Именно такие эпитеты я слышал о компоненте, когда он был только-только создан. Тот самый, который стал попаболью всего проекта несколькими месяцами позже.
И раз уж я начал теоретизировать, зайду чуть дальше — именно попытки реализовать принцип DRY, через паттерн strategy, собственно как и через наследование, приводят к тьме. Когда в угоду DRY, сам того не замечая, разработчик жертвует принципом SRP, первым и главным постулатом из SOLID. К слову, DRY не является частью SOLID, и при конфликте, надо жертвовать именно им, ибо он не благоприятствует созданию устойчивого кода в отличие от, собственно, SOLID. Как оказалось — скорее даже наоборот. Переиспользование кода должно быть приятным бонусом тех или иных дизайн-решений, а не целью принятия оных.
А соблазн переиспользования возникает, когда заказчик приходит с новой историей на создание третьей страницы. Ведь она так похожа на первые две. Еще этому немало способствует желание заказчика реализовать все «подешевле», Ведь переиспользовать ранее созданный контейнер быстрее, чем реализовать страницу полностью. История попала к другому разработчику, который быстро выяснил, что функционала контейнера недостаточно, а полноценный рефакторинг не вписывается в оценки. Ещё одна из ошибок тут в том, что разработчик продолжает следовать плану, поставленным оценкам, и это происходит «в тишине», ведь ответственности как бы нет, она лежит на всей команде, принявшей такое решение и такую оценку.
И вот в контейнер добавляется новый функционал, в интерфейс стратегии добавляются новые методы и поля. В контейнере появляются if-ы, а в старых реализациях стратегии появляются «заглушки», чтоб не сломать уже имеющиeся страницы. На момент сдачи второй истории, компонент уже был обречен, и чем дальше, тем хуже. Все сложнее разработчикам понимать как он устроен, в том числе тем, которые «совсем недавно в нём что то делали». Все сложнее вносить изменения. Все чаще приходится консультироваться с «предыдущими потрогавшими», чтобы спросить как это работает, почему были внесены некоторые изменения. Все больше вероятность того, что даже малейшее изменение внесёт новый дефект. Собственно уже речь начинает идти о том, что все больше вероятность внесения двух и более дефектов, ибо один дефект появляется уже с околоединичной вероятностью. И вот настает момент, когда новое требования заказчика реализовать невозможно. Выхода два: либо полностью переписать, либо сделать явный хак. А в ангуляре как раз есть подходящий хак-инструмент — можно сделать emit события снизу вверх, затем broadcast сверху вниз, когда закончил грязные делишки наверху. Технический долг при этом уже не увеличивается, он и так уже давно равен стоимости реализации этого компонента с нуля.

Сухая альтернатива


Наследование нередко порицается, а корпорация добра в своем языке Go вообще решила обойтись без него, и как мне кажется, негатив к наследованию частично исходит из опыта реализации принципа DRY через него. «Стратежный» DRY так же приводит к печальным результатам. Остается прямая агрегация. Для иллюстрации я возьму простой пример и покажу как он может быть представлен в виде стратегии, то есть шаблонного метода и без него.
Допустим у нас есть два сильно похожих сценария, представленных следующим псевдокодом:
В них повторяются 10 строчек X в начале и 15 строчек Y в конце. В середине же один сценарий имеет строчки А, другой — строчки B
СценарийА{
строчка X1
...строчка X10
Строчка А1
...Строчка А5
строчка Y1
...Строчка Y15
}

СценарийВ{
строчка X1
...строчка X10
Строчка B1
...Строчка B3
строчка Y1
...Строчка Y15
}


Вариант избавления от дублирования через стратегию


Контейнер{
строчка X1
...строчка X10
ВызовСтратегии()
строчка Y1
...Строчка Y15
}

СтратегияА{
Строчка А1
...Строчка А5
}

СтратегияВ{
Строчка B1
...Строчка B3
}

СценарийА{
Контейнер(new СтратегияА)
}

СценарийВ{
Контейнер(new СтратегияB)
}


Вариант через прямую агрегацию


МетодХ{
строчка X1
...строчка X10
}

МетодY{
строчка Y1
...Строчка Y15
}

МетодА{
Строчка А1
...Строчка А5
}

МетодВ{
Строчка B1
...Строчка B3
}

СценарийА{
МетодХ()
МетодA()
МетодY()
}

СценарийВ{
МетодХ()
МетодB()
МетодY()
}

Здесь предполагается, что все методы в разных классах.
Как я уже говорил, на момент реализации, первый вариант смотрится не так уж и плохо. Недостаток его не в том, что он изначально плох, а в том, что он неустойчив к изменениям. И, все же, хуже читается, хотя на простом примере это может быть и не очевидно. Когда нужно реализовать третий сценарий, который похож на первые два, но не на 100%, возникает желание переиспользовать код, содержащийся в контейнере. Но его не получится переиспользовать частично, можно лишь взять его целиком, поэтому приходится вносить изменения в контейнер, что сразу несет риск сломать другие сценарии. Тоже самое происходит, когда новое требование предполагает изменения в сценарии А, но это не должно затрагивать сценарий B. В случае же с агрегацией, метод X можно с легкостью заменить на метод X' в одном сценарии, совершенно не затрагивая другие. При этом нетрудно предположить, что методы X и X' могут также почти полностью совпадать, и их также можно подразбить. При «стратежном» подходе, если каскадировать таким же «стратежным» образом, то зло, помещаемое в проект возводится во вторую степень.

Когда можно


Многие примеры использования паттерна strategy лежат на виду, и часто используются. Их все объединяет одно простое правило — в контейнере нет бизнес логики. Совсем. Там может быть алгоритмическое наполнение, например хэш-таблица, поиск или сортировка. Стратегия же содержит бизнес-логику. Правило, по которому один элемент равен другому или больше-меньше есть бизнес-логика. Все операторы linq также являются воплощением паттерна, например оператор .Where() также является шаблонным методом, и принимаемая им лямбда есть стратегия.
Кроме алгоритмического наполнения, это может быть наполнение связанное с внешним миром, асинхронные запросы например, или, в примере от «банды четверых» — подписка на событие нажатия мыши. То что называют коллбэками по сути есть та же стратегия, надеюсь мне простят все мои гипер-обобщения. Также, если речь идет о UI, то это могут быть табы, или всплывающее окно.
Словом, это может быть что угодно, полностью абстрагированное от бизнес-логики.
Если же вы в разработке используете паттерн strategy, и в контейнер бизнес-логика попала — знайте, линию вы уже перешагнули, и стоите по щиколотку в… ммм, болоте.

Запахи


Иногда непросто понять, где черта между бизнес-логикой и общими задачами программирования. И поначалу, когда компонент только создан, определить, что он в будущем принесет геморроя, непросто. Да и если бизнес-требования никогда не будут меняться, то этот компонент может никогда и не всплыть. Но если изменения будут, то неизбежно будут появляться следующий code-smells:
1. Количество передаваемых методов. Параметр обсуждаемый, сам по себе не вредный, но таки может намекнуть. Два-три еще нормально, но если в стратегии содержится с десяток методов, то наверное что-то уже не так.
2. Флаги. Если в стратегии кроме методов есть поля/свойства, стоит обратить на то как они называются. Такие поля как Name, Header, ContentText — допустимы. Но если видите такие поля как SkipSomeCheck, IsSomethingAllowed, это означает, что стратегия уже попахивает.
3. Условные вызовы. Cвязано с флагами. Если в контейнере есть похожий код, значит вы уже ушли в болото по пояс
if(!strategy.SkipSomeCheck)
{
  strategy.CheckSomething().
}

4. Неадекватный код. На примере из JavaScript —
if(strategy.doSomething)

Из названия видно, что doSomething это метод, а проверяется как флаг. То есть разработчики поленились создать флаг, обозначающий тип, а использовали в качестве флага наличие/отсутствие метода, причем внутри блока if даже не он вызывался. Если вы встречаете такое, знайте — компонент уже по горло в техническом долге.

Заключение


Еще раз хочу обозначить свое мнение, что первопричина всего того, что я описал, не в паттерне как таковом, а в том, что он использовался ради принципа DRY, и оный принцип был поставлен превыше принципа единственной ответственности, ака SRP. И, кстати, не раз уже сталкивался, с тем, что принцип единственной ответсвтенности как то не совсем адекватно трактуется. Примерно как «мой божественный класс управляет спутником, управлять спутником — это единственная его ответственность». На сей ноте хочу закончить свой опус и пожелать пореже в ответ на «почему так», слышать фразу «исторически так сложилось».

Комментарии (48)


  1. aikixd
    31.01.2016 15:47
    +23

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


    1. MacIn
      31.01.2016 16:28
      +9

      Это модно. Берется набор самых обычных, разумных правил, ему дается красивое название — и — добро пожаловать в новую технологию/метод разработки/еще что-нибудь. Ну здорово же, прищурив глаз, спросить, что думает собеседник по поводу SRP, SOLID, DRY, TDD и т.д.? Или сколько паттернов он знает наизусть.


      1. aikixd
        31.01.2016 16:39
        +15

        — Я уметь инкапсулировать, не копипастить и проверять свой код. ?\_(?)_/?
        — Мы с вами свяжемся.


      1. Flammar
        01.02.2016 13:35

        А что ещё, кроме баззвордов, можно спросить на собеседовании за ограниченное время?..


        1. MacIn
          01.02.2016 17:32
          +2

          Тестовое задание все равно определяет.


    1. Athari
      31.01.2016 16:51
      +19

      Я замечал интересную закономерность: чем больше человек произносит сокращений слов вроде SOLID, DRY, KISS, YAGNI, я уж молчу про более сложные вещи, тем больше в коде говна. Хотя казалось бы…


      1. EngineerSpock
        01.02.2016 09:28
        -6

        Я замечал интересную закономерность: чем меньше человек знает сокращений вроде SOLID, DRY, KISS, YAGNI, я уж молчу про более сложные вещи, тем больше в коде говна. Хотя казалось бы… всё же так просто и кому нужны аббревиатуры и смыслы под ними скрывающиеся.


    1. RomeoGolf
      31.01.2016 16:53
      +14

      Вот-вот, и особенно насчет аббревиатур.

      Когда в угоду DRY, сам того не замечая, разработчик жертвует принципом SRP, первым и главным постулатом из SOLID.
      Вот так живешь — живешь, сядешь в ванну, спинку себе потрешь… А потом оказывается, что ты, сам того не зная, ЁКЛМН в угоду ПРСТ, и это еще не говоря о ФХЦ. Хотя, какой я, в торец, программист, если не в курсе этих буковок.
      И, кстати, да — правильная аббревиатура всенепременнейше должна быть сама по себе словом — DRY, KISS и так далее. Тогда ее автор, вероятно, прется неимоверно и считает себя ну просто писателем, не меньше.


      1. Flammar
        01.02.2016 13:31

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


    1. Flammar
      01.02.2016 13:33
      +3

      Большая часть паттернов — это про эмуляцию ФП средствами ООП через механизм абстракции/наследования.


  1. TheShock
    31.01.2016 17:02
    +21

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

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

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

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

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

    Вариант избавления от дублирования через...

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

    abstract class ThreeViewPageLayout {
      methodA () { /* code here */ } 
      methodB () { /* code here */ }
      methodC () { /* code here */ }
      runMethod () {
        methodA();
        methodB();
        methodC();
      }
    }
    
    class FirstPage extends ThreeViewPageLayout {
      methodB () { /* new code here */ }
    }
    
    class SecondPage extends ThreeViewPageLayout {
      methodB () { /* new code here */ }
    }
    
    class ThirdPage extends ThreeViewPageLayout {
      methodA () { /* new code here */ }
      methodB () { /* new code here */ }
    }
    


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

    Еще этому немало способствует желание заказчика реализовать все «подешевле», Ведь переиспользовать ранее созданный контейнер быстрее, чем реализовать страницу полностью.

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

    Когда программисты говорят: «Заказчик лучше знает, что его прям сейчас устроит менее качественный код» меня это удивляет. Код — ответственность программиста. Он сейчас сказал и забыл об этом. И через три месяца спросит именно у вас, почему код не качественный. И у вас не будет прикрыться им, ибо он — непрофессионал и не знает среднесрочного влияния подобных решений. А вы, как профессионал не должны дать ему допустить этой ошибки.


    1. VladVR
      31.01.2016 23:24
      -2

      На самом деле вы забыли ещё более приятный вариант — через полиморфизм.
      Этот вариант худший. Его «обвинили, и наложили на него табу» уже давно. И я его таки упомянул. Наследование — зло. А проблема ровно та же самая. То, разработчик посчитал неизменяемым и вынес в базовый класс рано или поздно получит свой change request, в котором нужно будет изменить метод X для сценария А, но не для сценария В
      То есть вы сами утверждаете, что допустили ошибку в планировании, но обвиняете при этом не собственную лень и непрофессиональное поведение, а принцип DRY.
      Да, мы допустили ошибку, кто их не допускает? И нет, мы не обвиняем, а предлагаем метод ну если не решения, то улучшения, хотя даже скорее метод избежания заведомого ухудшения.
      Есть такой параметр, малопонятный — вязкость, и мы, то есть я, утверждаю, что вязкость в указанном подходе высокая. Выше чем.


      1. TheShock
        01.02.2016 02:57
        +16

        Его «обвинили, и наложили на него табу» уже давно

        Хах. Подтвердили мои слова. Видите — проблема не в методах, подходах кое в чем другом.

        Наследование — зло

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

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

        получит свой change request, в котором нужно будет изменить метод X для сценария А, но не для сценария В

        Вы правда считаете это проблемой? Это рядовая задача в программировании. Опишите бизнес-цель и я дам вам ряд изящных способов решить её.

        Да, мы допустили ошибку, кто их не допускает?

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


        1. chumakov-ilya
          01.02.2016 11:50

          Безальтернативно объявлять наследование злом — конечно, неправильно. Но и обратная «абсолютная» точка зрения ничем не лучше. К примеру, еще во всем известной книге GoF по паттернам проектирования было аргументировано правило «предпочитайте композицию наследованию»: en.wikipedia.org/wiki/Composition_over_inheritance


          1. TheShock
            01.02.2016 14:17
            +1

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


        1. VladVR
          01.02.2016 17:10
          -1

          Извините, но это бред. И абсолютно не аргументирован.
          аргументов предостаточно, и я не автор этого высказывания, просто с ним согласен.
          Порицается наследование уже давно и много кем. вот например. Повторюсь, есть даже язык программирования, где наследования изначально нет.
          Вместо того, чтобы осознать и починить ошибки — вы обвиняете подходы.
          Я не обвиняю дождь в том, что он мокрый. Я лишь говорю, что он мокрый. Из трех вариантов разбиения кода вариант с наследованием имеет наибольшую вязкость, я его лишь упомянул, но не стал приводить псевдокодовый пример, ибо он заведомо худший.
          Вариант с агрегацией имеет наименьшую вязкость, т.е. наибольшую устойчивость к изменениям, он лучше читается и проще модифицируется. Это все, что «накипело».
          То, что я предлагаю — в условиях часто меняющейся бизнес логики — выбирать наименее вязкое решение изначально.
          Оно будет накапливать наименьшее количество технического долга из трех озвученных вариантов
          Всего лишь… необходимо было реструктуировать код… Да, это рефакторинг,
          Мыши кололись, плакали, но продолжали грызть кактус. Выбирая менее устойчивое к изменениям решение, вы чаще будете нуждаться в рефакторинге. Ваш технический долг будет расти быстрее, только и всего.
          Плохо то, что вы делаете неправильный вывод.
          Вязкость такая характеристика, которую не измерить линейкой, килобайтами и строчками кода. Она видна лишь с течением времени и лишь в условиях постоянно меняющихся требований.
          Я эмпирически увидел закономерность. Случай далеко не один, это происходит раз за разом, от проекта к проекту. Каждый раз такой код сложно читать, каждый раз трудно понять где и какое изменение нужно внести, каждый раз такие компоненты являются эпицентрами технического долга. Я сделал вывод из этого. Отличный от вывода «вы слишком много гогнокодите, просто перестаньте, и рефакторите код почаще».


    1. areht
      01.02.2016 04:59
      +3

      > На самом деле вы забыли ещё более приятный вариант — через полиморфизм. Стратегия — более узкий паттерн, чем универсальный метод для DRY. И полиформизм одновременно позволил бы избежать дублирования и не осложнил приложение как использование стратегии.

      То, что вы противопоставляете стратегию полиморфизму, а примером полиморфизма приводите наследование, говорит мне о том, что наследование от полиморфизма вы не отличаете. Не надо называть наследование красивым словом «полиморфизм».

      Ну и преимуществ у вашего кода над стратегией тоже никаких, кроме вашей уверенности, что так «не осложнили».


      1. TheShock
        01.02.2016 05:58
        +1

        Вы правы, что «наследование» — более подходящий термин.

        Я не противоставляю стратегию наследованию, я противоставлял свой код тому, что приводит автор. И сказал лишь о том, что стратегия не есть универсальное средство для DRY.

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

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

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


        1. areht
          01.02.2016 11:12
          +1

          Концепция «Composition over inheritance» не изобретение авторов Go, ей минимум лет 20


      1. TheShock
        01.02.2016 06:47
        +4

        Кстати, я сделаю отсылку к Фаулеру, где он применяет термин «полиморфизм» именно в том смысле, в котором применил его я. «Рефакторинг», «Замена условного оператора полиморфизмом». И там есть интересная цитата:

        Создать иерархию наследования можно двумя способами: «Заменой кода типа подклассами» ( Replace Type
        Code with Subclasses) и «Заменой кода типа состоянием/стратегией» (Replace Type Code with State/Strategy).
        Более простым вариантом является создание подклассов, поэтому по возможности следует выбирать его.
        Однако если код типа изменяется после того, как создан объект, применять создание подклассов нельзя, и
        необходимо применять паттерн «состояния/стратегии»


        И эта статья, в целом, описывает мой подход, так что я не понимаю, почему вы прицепились к моему термину полиморфизм (хотя он и более многозначен, чем «наследование»).


        1. areht
          01.02.2016 11:02

          Я вижу, что Фаулер
          1. описывает другую ситуацию
          2. Называет и стратегию полиморфизмом


          1. TheShock
            01.02.2016 14:10

            2. Называет и стратегию полиморфизмом

            Стратегию он называет «способом создать иерархию наследования».

            1. описывает другую ситуацию

            Почитайте в книге всю статью с соответствующим заголовком.


            1. areht
              01.02.2016 14:46

              > Стратегию он называет «способом создать иерархию наследования»

              Это переводчик шалит. Эта фраза звучит как «To create the inheritance structure you have two options...».
              И для описания решаемой задачи ухода от ифчиков это его описание корректно. Любая стратегия подразумевает inheritance structure, если нужна inheritance structure, то стратегия — один из вариантов.

              Но не надо отрывать это предложение от предыдущего абзаца. Там задача ставиться именно в такой формулировке, «Before you can begin with Replace Conditional with Polymorphism you need to have the necessary inheritance structure.». Поэтому и в процитированном тексте вылезает «способом создать иерархию наследования». И это не определение стратегии.

              И в этих абзацах ни слова про сам полиморфизм.

              Полиморфизм — это не свойство класса/иерархии, это свойство кода, работающего с иерархией. В отличии от LSP.


              1. TheShock
                01.02.2016 14:59

                Конечно, никто и не говорит, что это определение стратегии. Откуда вы такое додумали? Процитированный мною участок был к тому, что, согласно Фаулеру, в общем «Наследование» проще чем «Стратегия». Я акцентировал это жирным. Конечно, есть широкий ряд ситуаций, где применение Стратегии более уместно и это описано в этом же абзаце книги.

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

                И эта статья, в целом, описывает мой подход, так что ...

                В этих абзацах — ни слова, конечно. Зато вся статья «Замена условного оператора полиморфизмом» про полиморфизм.


                1. areht
                  01.02.2016 15:41
                  +1

                  > Процитированный мною участок был к тому, что, согласно Фаулеру, в общем «Наследование» проще чем «Стратегия»

                  После вводного

                  > Кстати, я сделаю отсылку к Фаулеру, где он применяет термин «полиморфизм» именно в том смысле, в котором применил его я.

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

                  Да, статья про полиморфизм. И там описывается стратегия, как один из вариантов применения полиморфизма. Если вы с Фаулером понимаете одинаково, то почему вы при этом комментируете статью про стратегии (которые и есть про полиморфизм) как «вы забыли ещё более приятный вариант — через полиморфизм» — я не очень понимаю.

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

                  Поэтому надо различать «проще» (по фаулеру) и «не осложняет» (у Вас). С первым я согласен, второе — требует обоснования.


                  1. TheShock
                    01.02.2016 15:57
                    +1

                    я ожидал цитату именно с этим.

                    Сперва не поняли, теперь разобрались?)

                    стратегии (которые и есть про полиморфизм)

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

                    проблему «быстро выяснил, что функционала контейнера недостаточно, а полноценный рефакторинг не вписывается в оценки»

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

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

                    ваш подход весьма ограничен

                    И это хорошо. Подход полностью покрывает описанное в статье. Абстрактные «шестая задача похожа на 89%» — это уже дополнительный вопрос, который можно обсудить на живом примере. Когда этого не хватает — тогда растем. Без рефакторинга при изменяемых требованиях любой код рано или поздно превратится в неподдерживаемый.


                    1. areht
                      01.02.2016 16:57

                      > Абстрактные «шестая задача похожа на 89%» — это уже дополнительный вопрос, который можно обсудить на живом примере.

                      Они не «абстрактные», это как раз то, что приводит к коллапсу. Первые 2 одинаково хорошо решались хоть наследованием, хоть стратегиями — там много не сэкономишь.


  1. VolCh
    31.01.2016 19:14
    +8

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


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

    Если же вы в разработке используете паттерн strategy, и в контейнер бизнес-логика попала — знайте, линию вы уже перешагнули, и стоите по щиколотку в… ммм, болоте.


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

    Про запахи: первый признак неправильного использования стратегии, имхо — наличие у неё собственного состояния и/или неявная модификация состояния контейнера.


    1. VladVR
      01.02.2016 00:03
      +1

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


      1. stalkerg
        01.02.2016 09:01

        Все что не специфично, а алгоритм сортировки явно не специфичен ни для какого бизнеса, бизнес-логикой не является.


        Ну вот с этим я не согласен. А сортировка строк ФИО по второй букве фамилии то же не будет бизнес логикой (при том, что если буквы совпадают начинается сортировка по имени)?
        А сколько вариантов сортировок просто строк где могут быть текст+цифры? Это всё явно бизнес логика и она явно реализуется в контейнере.
        Посему я и обычную сортировку чисел, хоть пузырьком хоть qsort так или иначе запишу в бизнес логику… просто с известными оптимальными реализациями.


      1. Flammar
        01.02.2016 15:27

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


      1. VolCh
        01.02.2016 19:45

        Бизнес-логика моделирует бизнес-процессы и иного понимания мы не мыслим. Если мы используем сортировку для представления (сортировка фамилий, например, в табличке, чисто для удобства), то она не является частью бизнес-логики. Если мы используем сортировку для моделирования предметной области (например, финансовые транзакции сортируем по времени проведения), то это бизнес-логика.


  1. lair
    31.01.2016 19:59
    +8

    Если же вы в разработке используете паттерн strategy, и в контейнер бизнес-логика попала — знайте, линию вы уже перешагнули,

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


    1. VladVR
      31.01.2016 23:41

      Я именно об этом и говорил. Пока ваша бизнес-логика неизменна, вам будет казаться, что ваше решение хорошее. Собственно в условиях неизменной бизнес-логики оно весьма вероятно действительно хорошее. Недостаток этого не в изначальной плохости, а в неустойчивости к изменениям.
      Другими словами, вопрос в том, что вы будете делать, когда заказчик скажет, что ее надо изменить. Именно ту логику, которая в «контейнере». Причем для одного сценария, допустим для поштучного, а для остальных сценариев оставить так как есть.

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


      1. lair
        31.01.2016 23:48
        +7

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

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

        Скилл у него [другого разработчика] другой, мышление другое, результат не заставит себя ждать.

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


        1. VladVR
          01.02.2016 00:18

          Если разделение ответственностей между компонентами было изначально верным,
          Как же можно узнать наперед, какие задачи в будущем будет ставить бизнес. Разделение ответственности основано на допущениях. И чем больше допущений, тем больше вероятность, что одно из них окажется неверным.
          Если у вас в команде есть такие проблемы, то они будут проявляться на любых шаблонах, Надо бороться с тем, что «мышления и скиллы» у разных разработчиков конфликтуют.
          Клонировать людей? Бороться с вязкостью как мне кажется проще, чем бороться с тем, что все люди разные.


          1. lair
            01.02.2016 00:21
            +2

            Как же можно узнать наперед, какие задачи в будущем будет ставить бизнес.

            Основываясь на предыдущем опыте. Но вообще — никак, конечно. Что, как ни странно, не мешает строить модифицируемые системы.

            Клонировать людей?

            Зачем? Просто обучать.

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

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


          1. TheShock
            01.02.2016 02:59
            +3

            Как же можно узнать наперед, какие задачи в будущем будет ставить бизнес.

            Так и не нужно это. Необходимо исходить из существующих и известных задач. А по мере изменений требований к системе — изменять саму систему.


      1. VolCh
        31.01.2016 23:48
        +1

        Если новый разработчик начнёт изменять существующие стратегии, например, добавляя заглушки, значит он не понял задание — сказано же «для остальных сценариев оставить так как есть» :) А если серьёзно, то ситуация «пять сценариев похожи на 90%, а шестой лишь на 10%» — это повод вводить особый случай или двухуровневые стратегии. Как вариант — отказаться от стратегий вообще.


        1. VladVR
          01.02.2016 14:42

          «пять сценариев похожи на 90%, а шестой лишь на 10%»
          Предполагалось, что шестой на 85%. Ну или, скажем 89%. Как в том анекдоте «N мало, пусть будет M»©.
          Вобщем достаточное совпадение, чтобы захотелось переиспользовать, а оставшийся процентик реализовать воткнув условный оператор.


          1. VolCh
            01.02.2016 19:47

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


      1. stalkerg
        01.02.2016 09:05
        -1

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


        Вот по этому я ярый противник этих подходов в разработке. Или как минимум близкое следованием их гайдлайнам.


  1. Scf
    01.02.2016 01:29
    +7

    В программировании UI есть один тонкий момент — нужно уметь различать одинаковые части интерфейса и похожие части интерфейса.

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

    Проблема же в том, чтобы понимать, когда надо обобщать, а когда надо плюнуть на DRY и скопипастить.


  1. areht
    01.02.2016 04:39
    +7

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

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

    Так что это в кансерватории проблемы. Ну там, если вы генерити код страничек из БЛ — стратегия вам не поможет, конечно.


  1. vbif
    01.02.2016 09:13
    +1

    Когда в угоду DRY, сам того не замечая, разработчик жертвует принципом SRP

    Скорее, «пытается применить DRY, игнорируя SRP, а потом удивляется, почему получилась тяжеловесная конструкция, которая тем не менее валится от малейшего неосторожного движения».


  1. MagicWolf
    01.02.2016 13:36

    Есть мнение из лагеря Clojure, что проблема не в DRY самом по себе, а в DRY + мейнстрим ООП.

    То есть DRY + наследование кода, жестко привязанное к иерархии.

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


    1. Flammar
      01.02.2016 14:08

      Ну, ООП без интерфейсов — это вообще беда. Лечится множественным наследованием, но с большими побочными эффектами. Ну или пиханием агрегации всюду, куда только можно. Иначе — «God Object» и забудьте про SRP.


  1. Flammar
    01.02.2016 14:04
    +1

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