Как придерживаться принципа открытого-закрытого на практике

14

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

Однако у меня возникли проблемы с пониманием того, как этот принцип применяется на практике. Насколько я понимаю, есть два способа его применения. До и после возможного изменения:

  1. До: программируйте на абстракции и «предсказывайте будущее» столько, сколько сможете. Например, метод drive(Car car)должен будет измениться, если Motorcycleв будущем в систему будут добавлены s, поэтому он, вероятно, нарушает OCP. Но drive(MotorVehicle vehicle)в будущем этот метод вряд ли изменится, поэтому он придерживается OCP.

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

  2. После: когда требуется изменение, расширяйте класс вместо изменения его текущего кода.

Практику № 1 не сложно понять. Однако на практике № 2 у меня возникают проблемы с пониманием того, как подать заявку.

Например (я взял его из видео на YouTube): допустим , у нас есть метод в классе , который принимает CreditCardобъекты: makePayment(CraditCard card). Один день Voucherдобавляются в систему. Этот метод не поддерживает их, поэтому его необходимо изменить.

При реализации метода в первую очередь нам не удалось предсказать будущее и программировать в более абстрактных терминах (например makePayment(Payment pay), теперь мы должны изменить существующий код.

Практика № 2 говорит, что мы должны добавить функциональность, расширяя, а не модифицируя. Что это обозначает? Должен ли я создать подкласс существующего класса вместо простого изменения его существующего кода? Должен ли я сделать вокруг него какую-то обертку, чтобы избежать переписывания кода?

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

Авив Кон
источник
1
Принцип Open / Closed не диктует механизм, который вы используете. Наследование, как правило, неправильный выбор. Кроме того, невозможно защититься от всех будущих изменений. Лучше не пытаться предсказать будущее, но как только потребуется изменение, измените дизайн, чтобы можно было учесть будущие изменения такого же рода.
Доваль

Ответы:

14

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

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

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

Карл Билефельдт
источник
1
Спасибо за ответ. Позвольте мне понять, понимаю ли я то, что вы говорите: то, что вы говорите, заключается в том, что я должен заботиться об OCP главным образом после того, как меня заставили внести изменения в класс. Смысл: при реализации класса в первый раз мне не стоит сильно беспокоиться об OCP, так как в любом случае трудно предсказать будущее. Когда мне нужно расширить / изменить его в первый раз, может быть, это хорошая идея, чтобы немного изменить рефакторинг, чтобы быть более гибким в будущем (больше OCP). И в третий раз мне нужно расширить / изменить класс, пришло время провести рефакторинг, чтобы сделать его более подходящим для OCP. Это то, что вы имеете в виду?
Авив Кон
1
Это идея.
Карл Билефельдт
2

Я думаю, что вы смотрите слишком далеко в будущее. Решите текущую проблему гибким способом, который придерживается, чтобы открыть / закрыть.

Допустим, вам нужно реализовать drive(Car car)метод. В зависимости от вашего языка у вас есть несколько вариантов.

  • Для языков, которые поддерживают перегрузку (C ++), просто используйте drive(const Car& car)

    В какой-то момент позже вам может понадобиться drive(const Motorcycle& motorcycle), но это не помешает drive(const Car& car). Нет проблем!

  • Для языков, которые не поддерживают перегрузку (Цель C), включите имя типа в метод -driveCar:(Car *)car.

    В какой-то момент позже вам может понадобиться -driveMotorcycle:(Motorcycle *)motorcycle, но опять же, это не помешает.

Это позволяет drive(Car car)быть закрытым для модификации, но открыто для распространения на другие типы транспортных средств. Это минималистическое будущее планирование, которое позволяет вам выполнять работу сегодня, но не дает вам блокировать себя в будущем.

Попытка представить самые основные типы, которые вам нужны, может привести к бесконечному регрессу. Что происходит, когда вы хотите управлять Segue, велосипедом или джамбо-джетом. Как создать единый общий абстрактный тип, который может учитывать все устройства, которые люди используют и используют для мобильности?

Джеффри Томас
источник
Изменение класса для добавления нового метода нарушает принцип Open-Closed. Ваше предложение также исключает возможность применения принципа замены Лискова ко всем транспортным средствам, которые могут двигаться, что по существу исключает одну из самых сильных частей ОО.
Данк
@Dunk Я основывал свой ответ на полиморфном принципе открытия / закрытия, а не на строгом принципе открытия / закрытия Мейера. Допустимо обновлять классы для поддержки новых интерфейсов. В этом примере автомобильный интерфейс хранится отдельно от интерфейса мотоцикла. Они могут быть оформлены в виде отдельных абстрактных классов вождения для автомобилей и мотоциклов, которые может поддерживать реализующий класс.
Джеффри Томас
@Dunk Принцип подстановки Лискова полезен, но не бесплатен. Если для оригинальной спецификации требуется только автомобиль, то создание более универсального автомобиля может не стоить дополнительных затрат денег, времени и сложности. Кроме того, маловероятно, что более универсальное транспортное средство будет идеально подходить для обработки незапланированных подклассов. Либо интерфейс для мотоцикла необходимо вставить в интерфейс транспортного средства (который был разработан только для управления автомобилем), либо вам нужно будет изменить транспортное средство для управления мотоциклом (реальное нарушение открытого / закрытого положения).
Джеффри Томас
Принцип Лискова-Подстановки не бесплатен, но и не требует больших затрат. И обычно он окупается намного больше, чем когда-либо, во много раз, даже если другой подкласс никогда не наследуется от него в основном приложении. Применение LSP значительно упрощает автоматизированное тестирование, что уже является победой. Кроме того, хотя вы, конечно, не должны быть в дураках и предполагать, что LSP понадобится всем, если вы создаете приложение и не понимаете, что может понадобиться в будущем, вы этого не делаете. достаточно знать о вашем приложении или его домене.
Данк
1
Что касается определения OCP. Это могут быть отрасли, в которых я работал, которые, как правило, требуют более высокого уровня проверки, чем обычная коммерческая компания, но, вообще говоря, если файл / класс изменяется, то вам нужно не только повторно протестировать файл / класс, но и все, что использует этот файл / класс в вашем регрессионном тестировании. Таким образом, не имеет значения, если кто-то говорит, что полиморфное открытие / закрытие - это хорошо, изменение интерфейса имеет широкий спектр последствий, поэтому не все так хорошо.
Данк
2

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

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

Что это обозначает? Должен ли я создать подкласс существующего класса вместо простого изменения его существующего кода?

Ага.

«Только принимает кредитные карты» определяется как часть поведения этого класса через его открытый интерфейс. Программист объявил миру, что метод этого объекта принимает только кредитные карты. Она сделала это, используя не совсем понятное имя метода, но оно сделано. Остальная часть системы полагается на это.

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

Новое поведение = новый класс

В качестве отступления . Хороший способ предсказать будущее - подумать о названии, которое вы дали методу. Вы дали действительно общее название метода, такого как makePayment, методу с конкретными правилами в методе относительно того, какой именно платеж он может сделать? Это запах кода. Если у вас есть определенные правила, это должно быть ясно из названия метода - makePayment должна быть makeCreditCardPayment. Делайте это, когда вы пишете объект в первый раз, и другие программисты будут вам благодарны за это.

Кормак Мулхолл
источник