Почему классы не должны быть «открытыми»?

44

При чтении различных вопросов о переполнении стека и чужого кода общее мнение о том, как проектировать классы, закрыто. Это означает, что по умолчанию в Java и C # все закрыто, поля являются окончательными, некоторые методы являются окончательными, а иногда даже классы являются окончательными .

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

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

  • Не расширяйте класс, просто обойдите проблему, ведущую к более сложному коду
  • Скопируйте и вставьте весь класс, убивая повторное использование кода
  • Форк проект

Это не хорошие варианты. Почему не protectedиспользуется в проектах, написанных на языках, которые его поддерживают? Почему некоторые проекты явно запрещают наследование от своих классов?

TheLQ
источник
1
Я согласен, у меня была эта проблема с компонентами Java Swing, это действительно плохо.
Джонас
3
Существует большая вероятность того, что если у вас возникла эта проблема, классы были спроектированы плохо, или, возможно, вы пытаетесь использовать их неправильно. Вы не просите у класса информацию, вы просите его сделать что-то для вас - поэтому вам, как правило, не нужны его данные. Хотя это не всегда так, но если вам нужно получить доступ к данным классов, есть большая вероятность, что что-то пошло не так.
Билл К
2
"большинство языков ООП?" Я знаю намного больше, где классы не могут быть закрыты. Вы пропустили четвертый вариант: изменить язык.
Кевин Клайн
@Jonas Одна из главных проблем Swing заключается в том, что он слишком много использует для реализации. Это действительно убивает развитие.
Том Хотин - Tackline

Ответы:

55

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

Так что классы не должны быть открытыми, но иногда не стоит того, чтобы их открывать.

Конечно, также возможно, что авторы библиотеки просто ленились. Зависит от того, о какой библиотеке вы говорите. :-)

Аарон
источник
13
Да, именно поэтому классы запечатаны по умолчанию. Поскольку вы не можете предсказать, каким образом ваши клиенты могут расширять класс, безопаснее его запечатать, чем взять на себя обязательство поддерживать потенциально неограниченное количество способов расширения класса.
Роберт Харви
3
См. Также blogs.msdn.com/b/ericlippert/archive/2004/01/22/…
Роберт Харви
18
Вам не нужно предсказывать, как ваш класс будет переопределен, вы просто должны предположить, что люди, расширяющие ваш класс, знают, что они делают. Если они расширяют класс и устанавливают что-либо в null, когда это не должно быть null, то это их вина, а не ваша. Единственное место, в котором этот аргумент имеет смысл, это супер-критические приложения, где что-то пойдет ужасно неправильно, если поле пустое.
TheLQ
5
@TheLQ: Это довольно большое предположение.
Роберт Харви
9
@TheLQ Чья это вина, это очень субъективный вопрос. Если они устанавливают для переменной, казалось бы, приемлемое значение, и вы не задокументировали тот факт, что это недопустимо, и ваш код не выдал ArgumentException (или эквивалентный), но это заставляет ваш сервер отключаться или приводит в отношении безопасности, тогда я бы сказал, что это ваша вина, так же как и их. И если вы действительно задокументировали действительные значения и поставили проверку аргументов на месте, то вы приложили именно то усилие, о котором я говорю, и вы должны спросить себя, действительно ли это усилие использовало ваше время.
Аарон
24

Делать все приватным по умолчанию звучит грубо, но взгляните на это с другой стороны: когда по умолчанию всё приватно, создание чего-то публичного (или защищенного, что почти то же самое) должно быть сознательным выбором; это авторский договор класса с вами, потребителем, о том, как использовать класс. Это удобно для вас обоих: автор может свободно изменять внутреннюю работу класса, пока интерфейс остается неизменным; и вы точно знаете, на какие части класса вы можете положиться, а какие могут быть изменены.

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

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

tdammers
источник
Это имеет некоторый смысл. Тем не менее, есть проблема, когда вам нужно что-то очень похожее, но с 1 или 2 изменениями в реализации (внутренняя работа класса). Я также изо всех сил пытаюсь понять, что если вы переопределяете класс, разве вы уже не сильно зависите от его реализации?
TheLQ
5
+1 за преимущества слабой связи. @TheLQ, это интерфейс, от которого вы должны сильно зависеть, а не реализация.
Карл Билефельдт
19

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

Что касается "окончательного" соотв. «Запечатанные» классы, один типичный случай - неизменяемые классы. Многие части каркаса зависят от неизменяемости строк. Если бы класс String не был конечным, было бы легко (даже заманчиво) создать изменяемый подкласс String, который бы вызывал всевозможные ошибки во многих других классах при использовании вместо неизменяемого класса String.

user281377
источник
4
Это настоящий ответ. Другие ответы касаются важных вещей, но по-настоящему важная часть заключается в следующем: все, что не finalи / или privateзаблокировано и никогда не может быть изменено .
Конрад Рудольф
1
@Konrad: пока разработчики не решат все переделать и не будут обратно совместимы.
JAB
Это правда, но стоит отметить, что это гораздо более слабое требование, чем для publicчленов; protectedчлены формируют контракт только между классом, который их выставляет, и его непосредственными производными классами; напротив, publicучастники для договоров со всеми потребителями от имени всех возможных настоящих и будущих унаследованных классов.
суперкат
14

В ОО есть два способа добавить функциональность в существующий код.

Первый - по наследству: вы берете класс и наследуетесь от него. Однако наследование следует использовать с осторожностью. Вы должны использовать публичное наследование, главным образом, когда у вас есть отношение isA между базой и производным классом (например, Rectangle - это Shape). Вместо этого вам следует избегать публичного наследования для повторного использования существующей реализации. По сути, публичное наследование используется для того, чтобы убедиться, что производный класс имеет тот же интерфейс, что и базовый класс (или больший), так что вы можете применять принцип подстановки Лискова .

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

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

knulp
источник
5
Отличный момент с композицией против наследования.
Жолт Тёрёк
+1, но мне никогда не нравился пример «это форма» для наследования. Прямоугольник и круг могут быть двумя совершенно разными вещами. Мне нравится больше использовать, квадрат - это прямоугольник, который ограничен. Прямоугольники могут быть использованы как формы.
Tylermac
@tylermac: действительно. Случай Square / Rectangle фактически является одним из контрпримеров LSP. Вероятно, я должен начать думать о более эффективном примере.
knulp
3
Квадрат / Прямоугольник только контрпример, если вы разрешаете модификацию.
звездно-синий
11

Большинство ответов имеют право: классы должны быть запечатаны (окончательно, NotOverridable и т. Д.), Когда объект не предназначен или не предназначен для расширения.

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

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

Вместо этого вы можете сделать одно из двух: вы можете получить из ConsoleWriter форму ConsoleAndFileWriter и расширить метод Write (), чтобы сначала вызывать базовую реализацию, а затем также записывать в файл. Или вы можете извлечь интерфейс IWriter из ConsoleWriter и переопределить этот интерфейс для создания двух новых классов; FileWriter и «MultiWriter», который может быть передан другим IWriters и будет «транслировать» любой вызов, сделанный его методами, всем указанным авторам. Какой из них вы выберете, зависит от того, что вам нужно; простой вывод, ну, проще, но если у вас есть смутное предвидение того, что в конечном итоге вам понадобится отправить сообщение сетевому клиенту, три файла, два именованных канала и консоль, продолжайте и попытайтесь извлечь интерфейс и создать "Y-адаптер"; Это'

Теперь, если функция Write () никогда не объявлялась виртуальной (или была запечатана), теперь у вас проблемы, если вы не контролируете исходный код. Иногда, даже если вы делаете. Обычно это плохая позиция, и это разочаровывает пользователей API с закрытым или ограниченным исходным кодом, когда это происходит. Но есть законная причина, по которой Write () или весь класс ConsoleWriter запечатаны; в защищенном поле может храниться конфиденциальная информация (в свою очередь переопределяемая из базового класса для предоставления указанной информации). Там может быть недостаточно проверки; когда вы пишете API, вы должны предполагать, что программисты, которые потребляют ваш код, не умнее и не доброжелательнее, чем средний «конечный пользователь» конечной системы; таким образом, вы никогда не будете разочарованы.

Keiths
источник
Хорошая точка зрения. Наследование может быть хорошим или плохим, поэтому нельзя утверждать, что каждый класс должен быть запечатан. Это действительно зависит от проблемы.
knulp
2

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

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

Спенсер Рэтбун
источник
7
Кстати, ОО и процедурные, безусловно, НЕ являются взаимоисключающими. Вы не можете иметь ОО без процедур. Кроме того, композиция - это такая же концепция ОО, как и расширение.
Майкл К
@ Майкл, конечно, нет. Я заявил, что вам может понадобиться процедурная система, то есть система без ОО-концепций. Я видел, как люди используют Java для написания чисто процедурного кода, и он не работает, если вы пытаетесь взаимодействовать с ним в режиме OO.
Спенсер Рэтбун,
1
Это можно было бы решить, если бы в Java были функции первого класса. Мех.
Майкл К
Новым студентам сложно преподавать языки Java или DOTNet. Я обычно преподаю Процессуальный с процедурным Паскалем или «Простым C», а позже переключаюсь на ОО с Объектным Паскалем или «C ++». И оставьте Java на потом. Обучение программированию с помощью процедурных программ выглядит так, будто один (одиночный) объект работает хорошо.
umlcat
@Michael K: В ООП функция первого класса - это просто объект с ровно одним методом.
Джорджио
2

Потому что они не знают ничего лучше.

Авторы оригинала, вероятно, цепляются за неправильное понимание принципов SOLID, которые возникли в запутанном и сложном мире C ++.

Надеюсь, вы заметите, что в мире ruby, python и perl нет проблем, которые, как утверждают ответы здесь, являются причиной запечатывания. Обратите внимание, что это ортогонально к динамической типизации. Модификаторы доступа просты в работе на большинстве (всех?) Языках. Поля C ++ можно изменить, приведя к другому типу (C ++ более слабый). Java и C # могут использовать отражение. Модификаторы доступа делают вещи достаточно сложными, чтобы помешать вам делать это, если вы действительно этого не хотите.

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

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

  1. Высокомерие - они действительно верили, что они открыты для расширения и закрыты для модификации
  2. Самодовольство - они знали, что могут быть другие варианты использования, но решили не писать для этих вариантов использования

Я думаю, что в корпоративной структуре # 2, вероятно, так. Эти платформы C ++, Java и .NET должны быть «сделаны», и они должны следовать определенным правилам. Эти рекомендации обычно означают запечатанные типы, если тип не был явно разработан как часть иерархии типов и закрытых членов для многих вещей, которые могут быть полезны для использования другими ... но поскольку они не имеют прямого отношения к этому классу, они не раскрываются , Извлечение нового типа было бы слишком дорого для поддержки, документирования и т. Д.

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

Я очень предпочитаю подход искажения имени питона. Вы можете легко (намного легче, чем размышление) заменить рядовых, если вам нужно. Отличная рецензия доступна здесь: http://bytebaker.com/2009/03/31/python-properties-vs-java-access-modifiers/

Приватный модификатор Руби на самом деле больше похож на защищенный в C # и не имеет частного как модификатор C #. Защищенный это немного другое. Здесь есть отличное объяснение: http://www.ruby-lang.org/en/documentation/ruby-from-other-languages/

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

jrwren
источник
5
«Инкапсуляция никогда не демонстрировала абсолютного успеха». Я бы подумал, что все согласятся с тем, что отсутствие инкапсуляции вызвало много проблем.
Джо
5
-1. Дураки устремляются туда, где ангелы боятся наступать. Празднование способности изменять частные переменные показывает серьезный недостаток в вашем стиле кодирования.
riwalk
2
Помимо спорных и, вероятно, неправильных, эта публикация также довольно высокомерна и оскорбительна. Красиво сделано.
Конрад Рудольф
1
Есть две школы мысли, сторонники которых, кажется, не ладят. Те, кто занимается статической типизацией, хотят, чтобы о программном обеспечении было легко рассуждать, а разработчики динамического набора хотят, чтобы они легко добавляли функциональность. Какой из них лучше, в основном зависит от ожидаемой продолжительности проекта.
Джо
1

РЕДАКТИРОВАТЬ: Я считаю, что классы должны быть разработаны, чтобы быть открытым. С некоторыми ограничениями, но не следует закрывать для наследования.

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

Я купил / скачал сторонние библиотеки с классами (большинство визуальных элементов управления), и очень благодарен, что ни одна из этих библиотек не использовала «final», даже если она поддерживается языком программирования, на котором они были закодированы, потому что я закончил расширение этих классов, даже если я скомпилировал библиотеки без исходного кода.

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

class MyBaseWrapper {
  protected FinalClass _FinalObject;

  public virtual DoSomething()
  {
    // these method cannot be overriden,
    // its from a "final" class
    // the wrapper method can  be open and virtual:
    FinalObject.DoSomething();
  }
} // class MyBaseWrapper


class MyBaseWrapper: MyBaseWrapper {

  public override DoSomething()
  {
    // the wrapper method allows "overriding",
    // a method from a "final" class:
    DoSomethingNewBefore();
    FinalObject.DoSomething();
    DoSomethingNewAfter();
  }
} // class MyBaseWrapper

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

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

PS Мне не нравится «конечный» в качестве ключевого слова в C #, я скорее использую «запечатанный», как Java, или «стерильный», следуя метафоре наследования. Кроме того, «финал» - это слово неоднозначности, используемое в нескольких контекстах.

umlcat
источник
-1: Вы заявили, что вам нравятся открытые дизайны, но это не отвечает на вопрос, поэтому классы не должны быть открытыми.
Джо
@Joh Извините, я плохо объяснил себя, я пытался выразить противоположное мнение, не должно быть запечатано. Спасибо за описание, почему вы не согласны.
umlcat