Подтвердить что ты не робот

Как вы создаете класс для наследования?

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

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

EDIT: спасибо за все отличные ответы. Я считаю, что консенсус в том, что для типичных классов приложений (подклассы WinForm, одноразовые классы утилиты и т.д.) Нет необходимости рассматривать повторное использование любого типа, а тем более повторное использование через наследование, тогда как для классов библиотек важно рассмотреть возможность повторного использования через наследование в дизайне.

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

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

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

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

4b9b3361

Ответ 1

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

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

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

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

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

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

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

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

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

Это, кстати, только точки с манжетами. Я могу придумать больше, если думаю, достаточно долго. Джош Блох отлично справляется с эффективностью Java 2, кстати.

Ответ 2

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

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

Ответ 3

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

Код лучше:

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 constructor
    System.out.println(this.data);  
  }
}

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

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

Ответ 4

Я не думаю, что когда-либо создавал класс для наследования. Я просто пишу как можно меньше кода, а затем, когда пришло время копировать и вставлять для создания другого класса, я помещаю этот метод в суперкласс (где он имеет больше смысла, чем композиция). Таким образом, я разрешаю принцип "Не повторяй себя" (DRY) диктовать мой класс.

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

Ответ 5

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