Как вы разрабатываете класс для наследования? - PullRequest
12 голосов
/ 17 января 2009

Я слышал, что это «сложно» «спроектировать для наследования», но я никогда не обнаруживал, что это так. Может ли кто-нибудь (и кем-либо, я имею в виду Джона Скита) объяснить, почему это якобы сложно, каковы подводные камни / препятствия / проблемы, и почему простые смертные программисты не должны пытаться это делать и просто закрывают свои классы для защиты невинных?

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

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

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

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

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

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

Ответы [ 5 ]

19 голосов
/ 17 января 2009

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

  • Трудно (или потенциально невозможно) правильно реализовать равенство, если только вы не ограничите его «оба объекта должны иметь одинаковый тип». Трудность связана с симметричным требованием, что a.Equals(b) подразумевает b.Equals(a). Если a и b представляют собой «квадрат 10x10» и «a красный 10x10 квадрат» соответственно, тогда a вполне может думать, что b равно ему - и что может быть всем, что вы действительно хотите проверить, во многих случаях. Но b знает, что у него есть цвет, а a нет ...

  • Каждый раз, когда вы вызываете виртуальный метод в своей реализации, вы должны документировать эти отношения и никогда не изменять их. Человек, производный от класса, тоже должен прочитать эту документацию - в противном случае он может реализовать вызов наоборот, что быстро приведет к переполнению стека X, вызывающему Y, вызывающему X, вызывающему Y. Во многих это моя главная проблема - во многих В таких случаях вам необходимо задокументировать вашу реализацию , что приводит к отсутствию гибкости. Это значительно снижается, если вы никогда не вызываете один виртуальный метод из другого, но вам все же приходится документировать любые другие вызовы виртуальных методов, даже не виртуальных, и никогда не изменять реализацию в этом смысле.

  • Безопасности потоков трудно достичь даже без какого-либо неизвестного кода, являющегося частью вашего класса времени выполнения. Вам не только нужно документировать модель потоков вашего класса, но вам также может понадобиться предоставлять блокировки (и т. Д.) Производным классам, чтобы они могли быть поточно-ориентированными таким же образом.

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

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

  • Должны ли вы реализовать какую-то клонирующую способность? Если вы этого не сделаете, это может затруднить правильное клонирование подклассов. Иногда клон для каждого члена достаточно хорош, но в других случаях это может не иметь смысла.

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

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

Между прочим, это просто тупые точки. Возможно, я смогу придумать больше, если подумаю достаточно долго. Джош Блох (Josh Bloch) очень хорошо описывает все это в Effective Java 2, кстати.

5 голосов
/ 17 января 2009

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

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

3 голосов
/ 19 января 2009

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

Что касается потребителей, то они должны действовать разумно. Я стараюсь не диктовать, как кто-то может использовать мои классы, хотя я стараюсь задокументировать, как я их планировал использовать.

2 голосов
/ 17 января 2009

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

Код лучше:

class Base {
  Base() {
    method();
  }

  void method() {
    // subclass may override this method
  } 
}

class Sub extends Base {
  private Data data;

  Sub(Data value) {
    super();
    this.data = value;
  }

  @Override
  void method() {
    // prints null (!), when called from Base's constructor
    System.out.println(this.data);  
  }
}

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

Сводка: не вызывать переопределенные методы из конструктора

1 голос
/ 17 января 2009

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...