Почему многие разработчики программного обеспечения нарушают принцип открытого / закрытого?

74

Почему многие разработчики программного обеспечения нарушают принцип открытия / закрытия , изменяя многие вещи, такие как переименование функций, которые нарушают работу приложения после обновления?

Этот вопрос приходит мне в голову после быстрой и непрерывной версий в библиотеке React .

Каждый короткий период я ​​замечаю множество изменений в синтаксисе, именах компонентов и т. Д.

Пример в следующей версии React :

Новые устаревшие предупреждения

Самое большое изменение заключается в том, что мы извлекли React.PropTypes и React.createClass в их собственные пакеты. Оба они по-прежнему доступны через основной объект React, но использование любого из них приведет к одноразовому выводу предупреждения об устаревании на консоль в режиме разработки. Это позволит в будущем оптимизировать размер кода.

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


  • Считаются ли эти изменения нарушением этого принципа?
  • Как новичку в React , как мне научиться этому с такими быстрыми изменениями в библиотеке (это так расстраивает)?
Anyname Donotcare
источник
6
Это явный пример наблюдения за этим, и ваше утверждение «так много» является необоснованным. Проекты Lucene и RichFaces являются печально известными примерами и API-интерфейсом порта связи Windows, но я не могу думать о каких-либо других от руки. И действительно ли React является «крупным разработчиком программного обеспечения»?
user207421
62
Как и любой принцип, OCP имеет свою ценность. Но это требует от разработчиков бесконечного предвидения. В реальном мире люди часто ошибаются в своем первом дизайне. Со временем некоторые предпочитают обходить свои старые ошибки во имя совместимости, другие предпочитают в конечном итоге исправлять их, имея компактную и свободную от кода базу.
Теодорос Чатзигианнакис
1
Когда в последний раз вы видели объектно-ориентированный язык "как изначально задумано"? Основным принципом была система обмена сообщениями, которая означала, что каждая часть системы может быть расширена любым пользователем. Теперь сравните это с вашим типичным ООП-подобным языком - сколько из них позволяет вам расширить существующий метод извне? Сколько из них позволяют достаточно легко быть полезными?
Луаан
Наследие отстой. 30-летний опыт показывает, что вы должны полностью отказаться от наследия и начать все заново. Сегодня у всех есть связь везде и всегда, поэтому наследие сегодня просто не имеет значения. окончательный пример был «Windows против Mac». Microsoft традиционно пыталась «поддерживать наследие», вы видите это во многих отношениях. Apple всегда просто говорила «F- - You» старым пользователям. (Это относится ко всему, от языков до устройств и ОС.) Фактически, Apple была абсолютно права, а MSFT была совершенно не права, понятна и проста.
Толстяк
4
Потому что есть ровно ноль «принципов» и «шаблонов проектирования», которые работают 100% времени в реальной жизни.
Матти Вирккунен

Ответы:

148

ИМХО, ответ Жака Б, хотя и содержит много правды, показывает фундаментальное неправильное понимание OCP. Справедливости ради, ваш вопрос уже выражает и это недоразумение - переименование функций нарушает обратную совместимость , но не OCP. Если нарушение совместимости кажется необходимым (или поддержание двух версий одного и того же компонента, чтобы не нарушать совместимость), OCP уже был сломан раньше!

Как уже упоминал Йорг Миттаг в своих комментариях, принцип не гласит: «Вы не можете изменить поведение компонента» - он говорит, что нужно пытаться проектировать компоненты так, чтобы они были открыты для повторного использования (или расширения). несколькими способами, без необходимости модификации. Это может быть сделано путем предоставления правильных «точек расширения» или, как упоминалось @AntP, «путем разложения структуры класса / функции до точки, где каждая естественная точка расширения находится там по умолчанию». ИМХО, следование OCP не имеет ничего общего с тем, чтобы «сохранить прежнюю версию без изменений для обратной совместимости» ! Или, цитируя комментарий @DerekElkin ниже:

OCP - это совет о том, как написать модуль [...], а не о реализации процесса управления изменениями, который никогда не позволяет модулям меняться.

Хорошие программисты используют свой опыт для разработки компонентов с «правильными» точками расширения (или, что еще лучше, без необходимости использования искусственных точек расширения). Однако, чтобы сделать это правильно и без лишних усилий, вам нужно заранее знать, как могут выглядеть варианты использования вашего компонента в будущем. Даже опытные программисты не могут заглянуть в будущее и заранее знают все предстоящие требования. И именно поэтому иногда необходимо нарушать обратную совместимость - независимо от того, сколько точек расширения имеет ваш компонент или насколько хорошо он соответствует OCP в отношении определенных типов требований, всегда будет требование, которое не может быть легко реализовано без изменения компонент.

Док Браун
источник
14
По мнению IMO, главная причина «нарушения» OCP заключается в том, что для его правильного выполнения требуется много усилий. У Эрика Липперта есть отличное сообщение в блоге о том, почему многие классы платформы .NET, по-видимому, нарушают OCP.
Би Джей Майерс
2
@BJMyers: спасибо за ссылку. У Джона Скита отличный пост об OCP, так как он очень похож на идею защищенной вариации.
Док Браун
8
ЭТО! OCP говорит, что вы должны написать код, который можно изменить, не трогая! Почему? Так что вам нужно только один раз протестировать, просмотреть и скомпилировать его. Новое поведение должно исходить из нового кода. Не прикручивая старый проверенный код. Как насчет рефакторинга? Ну, рефакторинг - явное нарушение OCP! Вот почему писать код - грех, думая, что вы просто измените его, если ваши предположения изменятся. Нет! Положите каждое предположение в свою маленькую коробочку. Если это не так, не почините коробку. Напиши новый. Почему? Потому что вам может понадобиться вернуться к старому. Когда вы это сделаете, было бы хорошо, если бы это все еще работало.
candied_orange
7
@CandiedOrange: спасибо за ваш комментарий. Я не вижу рефакторинга и OCP настолько противоречивыми, как вы это описали. Для написания компонентов, которые следуют OCP, часто требуется несколько циклов рефакторинга. Целью должен быть компонент, который не нуждается в модификациях для решения целого «семейства» требований. Тем не менее, не следует добавлять произвольные точки расширения к компоненту «на всякий случай», что слишком легко приводит к переобработке. Опираясь на возможность рефакторинга может быть лучшей альтернативой этому во многих случаях.
Док Браун
4
Этот ответ хорошо справляется с вызовом ошибок в (в настоящее время) топ-ответе - я думаю, что ключевой момент для успеха с открытым / закрытым - это перестать думать в терминах «точек расширения» и начать думать о разложении своего структура класса / функции до точки, где каждая естественная точка расширения находится там по умолчанию. Программирование «вовне» - это очень хороший способ достижения этой цели, когда каждый сценарий, который обслуживает ваш текущий метод / функция, выталкивается на внешний интерфейс, который образует естественную точку расширения для декораторов, адаптеров и т. Д.
Ant P
67

Принцип открытого / закрытого типа имеет свои преимущества, но он также имеет некоторые серьезные недостатки.

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

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

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

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

Возьмите что-то вроде .NET Framework - он по-прежнему содержит набор классов коллекций, которые были разработаны до того, как дженерики были представлены более десяти лет назад. Это, безусловно, благо для обратной совместимости (вы можете обновить фреймворк без необходимости что-либо переписывать), но это также раздувает фреймворк и предоставляет разработчикам большой набор опций, многие из которых просто устарели.

По-видимому, разработчики React чувствовали, что не стоит затрат на сложность и раздувание кода, чтобы строго следовать принципу открытого / закрытого типа.

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

(Моя интерпретация этого принципа основана на принципе открытого и закрытого типа Роберта К. Мартина)

JacquesB
источник
37
«Принцип в основном гласит, что вы не можете изменять поведение компонента. Вместо этого вы должны предоставить новый вариант компонента с желаемым поведением и оставить прежнюю версию неизменной для обратной совместимости». - Я не согласен с этим. Принцип гласит, что вы должны проектировать компоненты таким образом, чтобы не было необходимости изменять их поведение, потому что вы можете расширить его, чтобы делать то, что вы хотите. Проблема в том, что мы еще не выяснили, как это сделать, особенно с языками, которые в настоящее время широко используются. Проблема выражения является одной из частей ...
Йорг Миттаг
8
... это, например. Ни Java, ни C♯ не имеют решения для Expression. Haskell и Scala делают, но их пользовательская база намного меньше.
Йорг Миттаг
1
@ Джорджио: В Haskell решением являются классы типов. В Scala решение подразумевается и объектами. Извините, у меня нет ссылок под рукой, в настоящее время. Да, мультиметоды (фактически, они даже не должны быть «множественными», это скорее «открытая» природа методов Lisp) также являются возможным решением. Обратите внимание, что существует множество формулировок проблемы выражения, поскольку обычно статьи пишутся таким образом, что автор добавляет ограничение к проблеме выражения, в результате чего все существующие в настоящее время решения становятся недействительными, а затем показывает, как его собственное…
Йорг Миттаг
1
… Язык может даже решить эту «более сложную» версию. Например, Вадлер первоначально сформулировал проблему выражения так, чтобы она касалась не только модульного расширения, но и статически безопасного модульного расширения. Обычные мультиметоды Lisp, однако, не являются статически безопасными, они только динамически безопасны. Затем Одерский еще больше усилил это, заявив, что он должен быть статически модульно безопасным, то есть безопасность должна проверяться статически, не смотря на всю программу, а только на модуль расширения. Это на самом деле не может быть сделано с классами типа Haskell, но это может быть сделано с помощью Scala. А в…
Йорг Миттаг
2
@ Джорджио: Точно. То, что заставляет мультиметоды Common Lisp решать EP, на самом деле не множественная рассылка. Это факт, что методы открыты. В типичном FP ​​(или процедурном программировании) различение типов связано с функциями. В типичном ОО методы привязаны к типам. Общие методы Lisp открыты , их можно добавлять в классы по факту и в другом модуле. Это особенность, которая делает их пригодными для решения EP. Например, протоколы Clojure предназначены для однократной отправки, но также решают проблемы с EP (если вы не настаиваете на статической безопасности).
Йорг Миттаг
20

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

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

Известный пример этого можно найти в диспетчере памяти Windows 95. В рамках маркетинга Windows 95 было заявлено, что все приложения Windows 3.1 будут работать в Windows 95. Microsoft фактически приобрела лицензии на тысячи программ для их тестирования в Windows 95. Одним из проблемных случаев был Sim City. В Sim City действительно была ошибка, из-за которой он записывал в нераспределенную память. В Windows 3.1 без «правильного» менеджера памяти это было незначительной ошибкой. Тем не менее, в Windows 95 диспетчер памяти перехватит это и вызовет ошибку сегментации. Решение? В Windows 95, если имя вашего приложения такое simcity.exe, ОС фактически ослабит ограничения диспетчера памяти, чтобы предотвратить ошибку сегментации!

Настоящая проблема, лежащая в основе этого идеала, заключается в ограниченности концепций продуктов и услуг. Никто на самом деле не делает ни того, ни другого. Все выстраивается где-то в серой области между ними. Если вы думаете о подходе, ориентированном на продукт, открытие / закрытие звучит как отличный идеал. Ваши продукты надежны. Однако, когда дело доходит до услуг, история меняется. Легко показать, что по принципу «открыт / закрыт» количество функций, которые должна поддерживать ваша команда, должно асимптотически приближаться к бесконечности, потому что вы никогда не сможете очистить старую функциональность. Это означает, что ваша команда разработчиков должна поддерживать все больше и больше кода каждый год. В конце концов вы достигнете предела.

Большинство программного обеспечения сегодня, особенно с открытым исходным кодом, следует общепринятой версии открытого / закрытого принципа. Очень часто можно увидеть, что открытые / закрытые следуют рабски для небольших релизов, но заброшены для крупных релизов. Например, Python 2.7 содержит много «плохих вариантов» из Python 2.0 и 2.1 дней, но Python 3.0 сместил их все. (Кроме того , переход от Windows 95. кодового в кодовую Windows NT , когда они выпустили Windows 2000 побил все виды вещей, но это же означает , что мы никогда не должны иметь дело с менеджером памяти проверяя имя приложения , чтобы решить поведение!)

Корт Аммон
источник
Это отличная история про SimCity. У тебя есть источник?
Би Джей Майерс
5
@BJMyers. Это старая история, о которой упоминает Джоэл Сполеки в конце этой статьи . Первоначально я прочитал это как часть книги о разработке видеоигр много лет назад.
Корт Аммон
1
@BJMyers: я почти уверен, что у них были подобные «хаки» совместимости для десятков популярных приложений.
Док Браун
3
@BJMyers, есть много подобных вещей, если вы хотите хорошо прочитать, зайдите в блог The Old New Thing Раймонда Чена , просмотрите тег History или найдите «совместимость». Есть воспоминания о множестве историй, в том числе о чем-то явно близком к вышеупомянутому делу SimCity - Addentum: Чен не любит называть имена виноватыми.
Theraot
2
Очень мало что сломалось даже при переходе 95-> NT. Оригинальный SimCity для Windows по-прежнему прекрасно работает на Windows 10 (32-разрядная версия). Даже игры для DOS по-прежнему отлично работают, если вы отключите звук или используете что-то вроде VDMSound, чтобы консольная подсистема могла правильно обрабатывать звук. Microsoft очень серьезно относится к обратной совместимости , и они также не используют ярлыки «давайте поместим это в виртуальную машину». Иногда требуется обходной путь, но это все же довольно впечатляет, особенно в относительном выражении.
Луаан
11

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

Чтобы явно сформулировать недоразумение, кажется , есть мнение , что OCP означает , что вы не должны делать назад несовместимые изменения (или даже какие - либо изменения или что - то вдоль этих линий.) ОСР о проектировании компонентов , так что вам не нужно , чтобы внесите в них изменения, чтобы расширить их функциональность, независимо от того, являются ли эти изменения обратно совместимыми или нет. Помимо добавления функциональных возможностей, существует множество других причин, по которым вы можете вносить изменения в компонент независимо от того, являются ли они обратно совместимыми (например, рефакторинг или оптимизация) или обратно несовместимыми (например, не рекомендуется использовать или удалять функциональность). То, что вы можете внести эти изменения, не означает, что ваш компонент нарушил OCP (и определенно не означает, что вы нарушают OCP).

На самом деле, это вовсе не исходный код. Более абстрактное и актуальное утверждение OCP: «компонент должен допускать расширение без необходимости нарушать границы абстракции». Я бы пошел дальше и сказал бы, что более современное представление: «компонент должен обеспечивать свои границы абстракции, но допускает расширение». Даже в статье об OCP Боба Мартина, когда он «описывает» «закрытый для модификации» как «исходный код нерушим», он позже начинает говорить об инкапсуляции, которая не имеет ничего общего с модификацией исходного кода и всем, что связано с абстракцией границы.

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

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

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

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

Что касается вашего последнего вопроса, то он, возможно, не является темой для StackExchange в целом, поскольку он в первую очередь основан на мнениях, но если не считать его, можно приветствовать технологию и, в частности, JavaScript, где в последние несколько лет описываемое вами явление называлось усталостью JavaScript . (Не стесняйтесь в Google, чтобы найти множество других статей, некоторые сатирические, говорящие об этом с разных точек зрения.)

Дерек Элкинс
источник
3
«Вы, как создатель, не нарушаете OCP, изменяя или даже удаляя компонент». - Можете ли вы дать ссылку на это? Ни одно из приведенных мною определений принципа не гласит, что «создатель» (что бы это ни значило) не освобождается от этого принципа. Удаление опубликованного компонента - это явное изменение.
JacquesB
1
@JacquesB Люди и даже изменения кода не нарушают OCP, компоненты (то есть фактические части кода) делают. (И, чтобы быть совершенно ясным, это означает, что компонент не соответствует самому OCP, а не нарушает OCP какого-либо другого компонента.) Весь смысл моего ответа состоит в том, что OCP не говорит об изменениях кода взлом или иначе. Компонент либо открыт для расширения и закрыты для модификации, или это не так , так же , как метод может быть privateили нет. Если автор делает privateметод publicпозже, это не значит, что они нарушили контроль доступа, (1/2)
Дерек Элкинс
2
... и это не значит, что метод не был на самом деле privateраньше. «Удаление опубликованного компонента - явное изменение», - это не секвитур. Либо компоненты новой версии удовлетворяют требованиям OCP, либо нет, вам не нужна история кодовой базы, чтобы определить это. По твоей логике я никогда не смог бы написать код, который удовлетворял бы OCP. Вы связываете обратную совместимость, свойство изменений кода, с OCP, свойство кода. Ваш комментарий имеет столько же смысла, сколько говорит, что быстрая сортировка не имеет обратной совместимости. (2/2)
Дерек Элкинс
3
@JacquesB Во-первых, еще раз отметим, что речь идет о модуле, соответствующем OCP. OCP - это совет о том, как написать модуль, чтобы, учитывая ограничение на невозможность изменения исходного кода, модуль можно было расширить. Ранее в статье он говорил о разработке модулей, которые никогда не меняются, а не о реализации процесса управления изменениями, который никогда не позволяет модулям меняться. Что касается редактирования вашего ответа, вы не «нарушаете OCP», изменяя код модуля. Вместо этого, если «расширение» модуля требует от вас изменения исходного кода, (1/3)
Дерек Элкинс
2
«OCP - это свойство определенного фрагмента кода, а не эволюционная история кодовой базы». - отлично!
Док Браун