[Перевод] OCP против YAGNI

?v=1

Эта статья является переводом материала OCP vs YAGNI.

В этом посте хочется осветить тему OCP и YAGNI — противоречия между принципом открытости/закрытости и принципом «вам это не понадобится».

OCP

Давайте начнем с того, что вспомним, что такое OCP. Принцип открытости/закрытости гласит, что: Объекты программного обеспечения (классы, модули, функции и т.д.) должны быть открыты для расширения, но закрыты для модификации.

Впервые он был представлен Бертраном Мейером в его канонической книге «Конструирование объектно-ориентированного программного обеспечения». С тех пор его популяризировал Боб Мартин, когда он представил принципы SOLID.

Официальное определение довольно расплывчато и на самом деле не помогает нам понять основной смысл. Итак, давайте углубимся в этот принцип.

В настоящее время существует две интерпретации данного принципа: Бертрана Мейера и Боба Мартина.

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

Обычно это реализуется с помощью полиморфизма. Например, следующий фрагмент кода нарушает версию OCP Боба Мартина:

public void Draw(Shape shape)
{
    switch (shape.Type)
    {
        case ShapeType.Circle:
            DrawCircle(shape);
            break;
 
        case ShapeType.Square:
            DrawSquare(shape);
            break;
 
        default:
            throw new ArgumentOutOfRangeException();
    }
}

Здесь, чтобы ввести новую фигуру, вам нужно будет изменить метод Draw. Вы можете сказать, что добавление новой формы будет распространяться по существующей кодовой базе в том смысле, что вам потребуется изменить приведенный выше оператор switch.

Чтобы исправить это, вы можете создать абстрактный класс Shape и затем переместить логику рисования в его подклассы:

public abstract class Shape
{
    public abstract void Draw();
}
 
public class Circle : Shape
{
    public override void Draw()
    {
        /* … */
    }
}
 
/* etc. */

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

Первоначальное намерение Бертрана Мейера, лежащее в основе этого принципа, иное. В то время как интерпретация Боба Мартина направлена на уменьшение количества изменений, Бертран Мейер говорит об обратной совместимости.

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

Например, если у вас есть библиотека, которая предоставляет метод

CreateCustomer(string email)

Вы не можете в любой момент добавить к нему новый обязательный параметр, например:

CreateCustomer(string email, string bankAccountNumber)

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

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

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

Вот полный список того, что представляет собой API:

  • Сигнатура метода: имя, параметры, возвращаемое значение.

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

  • Постусловия: список гарантий, которые дает модуль. Примером может служить обещание отправить поздравительное электронное письмо вновь созданному клиенту.

  • Инварианты: список условий, которые должны выполняться всегда.

Изменение сигнатуры метода, усиление его предусловий, ослабление постусловий или изменение инвариантов приведет к нарушению изменений.

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

Все вопросы, касающиеся управления версиями веб-API, по сути, связаны с принципом OCP Мейера, применяемым к крупномасштабным проектам. Если у вас есть Микросервис 1, который зависит от Микросервиса 2, вы не можете внести критическое изменение в Микросервис 2, его API должен быть закрыт для таких изменений. Но вы все равно можете создать его новую версию и предоставить существующим клиентам выбор: либо остаться со старой версией, либо перейти на новую.

Еще один важный момент, который следует отметить, заключается в том, что версия OCP Мейера имеет смысл только в контексте нескольких команд разработчиков, когда каждый модуль разрабатывается разными командами. В типичной среде корпоративного программного обеспечения, когда вы одновременно являетесь автором и клиентом написанного кода, нет необходимости придерживаться таких сложных методов. Вам не нужно закрывать свой код, так как у вас есть все необходимое для исправления любых критических изменений, которые вы можете внести. Только когда вы опубликуете свой модуль/библиотеку/сервис и сделаете его доступным для других команд, вы должны действительно закрыть его API. В противном случае критические изменения не являются проблемой.

Автор оригинала затронул эту тему более подробно в одной из предыдущих своих статей: Shared library vs Enterprise development

Итак, две разновидности OCP, несмотря на одно и то же название, различаются по своему смыслу. Это важно для нашего обсуждения OCP vs YAGNI. Код, который приводился ранее:

public void Draw(Shape shape)
{
    switch (shape.Type)
    {
        case ShapeType.Circle:
            DrawCircle(shape);
            break;
 
        case ShapeType.Square:
            DrawSquare(shape);
            break;
 
        default:
            throw new ArgumentOutOfRangeException();
    }
}

нарушает интерпретацию OCP Мартина, но не противоречит интерпретации Мейера. Это потому, что добавление новых фигур не требует от нас изменения API метода Draw. Все существующие клиенты останутся без изменений, критических изменений не будет.

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

YAGNI

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

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

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

Существуют ли ситуации, когда YAGNI неприменим? Да.

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

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

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

OCP vs YAGNI

Обратите внимание, что YAGNI кроме рекомендации не реализовывать неиспользуемые функции, также запрещает изменять существующие функции для учета возможных новых возможностей в будущем. В этом и заключается противоречие. Этот «учет возможных новых функций в будущем» — это именно то, что предлагает версия OCP Боба Мартина.

Давайте еще раз посмотрим на метод Draw:

public void Draw(Shape shape)
{
    switch (shape.Type)
    {
        case ShapeType.Circle:
            DrawCircle(shape);
            break;
 
        case ShapeType.Square:
            DrawSquare(shape);
            break;
 
        default:
            throw new ArgumentOutOfRangeException();
    }
}

С одной стороны, у нас есть YAGNI, который говорит, что этот оператор switch допустим, если результирующий код прост и его легко понять и поддерживать. С другой стороны, у нас есть OCP Боба Мартина, который говорит, что нам нужно иметь возможность расширять его без изменения исходного кода, то есть без изменения самого оператора switch.

Какой из этих принципов имеет более высокий приоритет?

Чтобы ответить на этот вопрос, давайте вернемся назад. Обратите внимание, что речь идет о противоречии между YAGNI и OCP Боба Мартина, а не о его версии Бертрана Мейера. Это потому, что YAGNI не противоречит последнему, они в основном говорят о разных вещах.

Что касается версии Боба Мартина, то ее можно рассматривать с двух разных точек зрения. Первая — это когда вы одновременно являетесь автором и клиентом написанного кода. Это ситуация, в которой находится большинство разработчиков корпоративных приложений. Вторая — когда вам нужно опубликовать свой код для внешнего использования. Пакет NuGet или фреймворк являются типичными примерами.

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

Это также имеет смысл, если вы посмотрите более внимательно на OCP Боба Мартина. Почему вы должны закладывать точки расширения в свой код преждевременно, даже если это приведет к чрезмерному усложнению? Действительно ли стоит тратить усилия и дополнительные затраты на замену простого оператора switch на отдельную иерархию классов? Конечно нет. Гораздо лучше заложить эти точки расширения постфактум, когда у вас уже есть полная картина и когда вы видите, что оператор switch стал слишком раздутым. В этом случае вы можете применить рефакторинг и извлечь эту иерархию классов. Но не раньше, чем необходимость в этом станет очевидной.

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

В такой ситуации вам действительно нужно определить потенциальные точки вариации и создать вокруг них интерфейс, который позволит потребителям расширять ваши классы, а не изменять их. Итак, в приведенном выше примере кода, если вы ожидаете, что Draw будет методом, ориентированным на потребителя, и хотите предоставить средства для его расширения, рекомендуется заранее заменить его базовым классом Shape и позволить вашим потребителям создавать свои собственные формы.

Версия OCP Боба Мартина имеет гораздо больше смысла, если вы поместите ее в исходную точку зрения Бертрана Мейера. Точки расширения стоит закладывать только тогда, когда вам придется выставлять свой код для внешнего использования в той или иной форме. В любом другом случае придерживайтесь YAGNI и не вводите дополнительную гибкость без реальной необходимости.

Резюме

Существует две интерпретации принципа Открытости/Закрытости:

  • Исходная интерпретация Бертрана Мейера касается обратной совместимости. Вам необходимо закрыть API вашего модуля/библиотеки/службы, если он предназначен для внешнего использования.

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

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

YAGNI противоречит версии OCP Боба Мартина.

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

YAGNI превосходит OCP Боба Мартина, когда вы единственный потребитель своего кода (разработка корпоративного программного обеспечения).

OCP Боба Мартина превосходит YAGNI, когда вы не единственный потребитель своего кода.

© Habrahabr.ru