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

Разница между экземпляром класса и классом, представляющим экземпляр уже?

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

Давайте возьмем IOException в Java в качестве примера. Почему существует класс FileNotFoundException? Не должен ли быть экземпляр IOException, где причина - FileNotFound? Я бы сказал, что FileNotFoundException является экземпляром IOException. Где это заканчивается? FileNotFoundButOnlyCheckedOnceException, FileNotFoundNoMatterHowHardITriedException..?

Я также видел код в проектах, в которых я работал, где существуют классы, такие как FirstLineReader и LastLineReader. Для меня такие классы фактически представляют экземпляры, но я вижу такой дизайн во многих местах. Посмотрите на исходный код Spring Framework, например, сотни таких классов, где каждый раз, когда я вижу один, я вижу экземпляр вместо чертежа. Разве классы не предназначены для чертежей?

То, что я пытаюсь спросить, заключается в том, как вы принимаете решение между этими двумя очень простыми вариантами:

Вариант 1:

enum DogBreed {
    Bulldog, Poodle;
}

class Dog {
    DogBreed dogBreed;
    public Dog(DogBreed dogBreed) {
        this.dogBreed = dogBreed;
    }
}

Вариант 2:

class Dog {}

class Bulldog extends Dog {
}

class Poodle extends Dog {
}

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

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

4b9b3361

Ответ 1

отредактированный

Прежде всего: мы знаем определение Inheritance, и мы можем найти множество примеров в SO и Интернете. Но, я думаю, мы должны смотреть глубже и немного научнее.

Примечание 0:
Разъяснение терминологии наследования и инстанции.
Сначала позвольте мне назвать область разработки для жизненного цикла разработки, когда мы моделируем и программируем нашу систему и область выполнения, иногда наша система работает.

У нас есть классы и моделирование и их разработка в области разработки. И объекты в области выполнения. В области разработки нет объекта.

И в Object Oriented определение экземпляра: Создание объекта из класса.

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

Итак, с этим вступлением, я хочу уточнить Наследование:
Наследование - это взаимосвязь между классами, а не объектами.
Наследование может существовать в области разработки, а не в области выполнения. В области выполнения не существует Наследования.

После запуска нашего проекта нет отношения между родителем и дочерним элементом (если есть только Наследование между дочерним классом и родительским классом). Итак, возникает вопрос: что такое super.invokeMethod1() или super.attribute1?, Это не отношения между дочерним и родительским. Все атрибуты и методы родителя передаются дочернему элементу, и это всего лишь обозначение для доступа к частям, переданным от родителя.

Кроме того, в области разработки нет объектов. Таким образом, нет никаких экземпляров в области разработки. Это просто отношения Is-A и Has-A.

Поэтому, когда мы сказали:

Я бы сказал, что FileNotFoundException является экземпляром IOException

Мы должны уточнить нашу область (разработка и время выполнения).
Например, если FileNotFoundException является экземпляром IOException, то какова связь между конкретным исключением FileNotFoundException во время выполнения (Object) и FileNotFoundException. Это экземпляр экземпляра?

Примечание 1:
Почему мы использовали Наследование? Цель наследования заключается в расширении функциональных возможностей родительского класса (на основе одного и того же типа).

  • Это расширение может произойти путем добавления новых атрибутов или новых методов.
  • Или переопределение существующих методов.
  • Кроме того, расширяя родительский класс, мы также можем достичь возможности повторного использования.
  • Мы не можем ограничивать функциональность родительского класса (принцип Лискова)
  • Мы должны иметь возможность заменить ребенка как родителя в системе (принцип Лискова)
  • и так далее.

Примечание 2:
Ширина и глубина иерархий наследования
Ширина и глубина наследования могут быть связаны со многими факторами:

  • Проект: сложность проекта (Type Complexity), его архитектура и дизайн. Размер проекта, количество классов и т.д.
  • Команда: опыт команды в управлении сложностью проекта.
  • и так далее.

Однако у нас есть эвристика. (Объектно-ориентированная эвристика дизайна, Артур Дж. Риель)

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

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

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

Примечание 3:
Когда мы используем неправильное наследование?
Основано на:

  • Примечание 1: цель наследования (расширение функций родительского класса)
  • Примечание 2: ширина и глубина наследования

В этих условиях мы используем неправильное наследование:

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

  2. У нас есть много классов в иерархии наследования. Он вызывает чрезмерную специализацию. Это может привести к следующим причинам:

    • Возможно, мы не рассматривали примечание 1 (расширение родительских функций)
    • Возможно, наша модуляция (упаковка) неверна. И мы поставили много случаев использования системы в одном пакете, и мы должны сделать Рефакторинг дизайна.
  3. Они - другие причины, но не совсем связаны с этим ответом.

Примечание 4:
Что нам делать? Когда мы используем неправильное наследование?

Решение 1. Мы должны выполнить Рефакторинг Проекта, чтобы проверить значение классов, чтобы Расширить родительскую Функциональность. В этом рефакторинге, возможно, многие классы системы удалены.

Решение 2. Мы должны выполнить Реконструкцию Проекта для модуляции. В этом рефакторинге, возможно, некоторые классы нашего пакета передаются в другие пакеты.

Решение 3: Использование композиции над наследованием.
Мы можем использовать этот метод по многим причинам. Динамическая иерархия - одна из популярных причин, по которой мы предпочитаем композицию вместо наследования.
см Тим Бодро (Солнца) отмечает здесь:

Иерархии объектов не масштабируются

Решение 4: использование экземпляров по подклассам

Этот вопрос касается этой техники. Позвольте мне назвать его экземплярами по подклассам.

Когда мы сможем его использовать:

  1. (Совет 1): Рассмотрим примечание 1, когда мы точно не расширяем функциональные возможности родительского класса. Или расширения не являются разумными и достаточными.

  2. (Совет 2 :) Рассмотрим примечание 2: если у нас много подклассов (полу или одинаковых классов), которые немного расширяют родительский класс, и мы можем управлять этим расширением без наследования. Обратите внимание, что это нелегко сказать. Мы должны доказать, что это не противоречит другим объектно-ориентированным принципам, таким как принцип открытого закрытия.

Что нам делать?
Мартин Фаулер рекомендует (книга 1 стр. 232 и книга 2 стр. 251):

Замените подкласс полями, измените методы на поля суперкласса и исключите подклассы.

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

Ответ 2

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

Исключения - это просто сложные значения. Их поведение тривиально: предоставлять сообщение, обеспечивать причину и т.д. И они, естественно, иерархичны. Там Throwable наверху, и другие исключения повторно специализируются на нем. Иерархия упрощает обработку исключений, предоставляя механизм естественного фильтра: когда вы говорите catch (IOException..., вы знаете, что вы получите все плохое, что произошло в отношении ввода-вывода. Не может быть намного яснее этого. Тестирование, которое может быть уродливым для иерархии больших объектов, не является проблемой для исключений: мало или ничего не нужно тестировать в значении.

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

Ваш второй пример, похоже, касается объектов с более сложным поведением. Они имеют два аспекта:

  • Поведение должно быть проверено.
  • Объекты со сложным поведением часто меняют свои отношения друг с другом по мере развития систем.

Вот причины для часто услышанной мантры "состав над наследованием".. С середины 90-х годов было хорошо понятно, что большие композиции небольших объектов обычно легче тестировать, поддерживать и измените, чем большие иерархии наследования обязательно больших объектов.

Сказав это, выбор, который вы предложили для реализации, не имеет смысла. Вопрос, на который вам нужно ответить, - "Каково поведение собак, которых я интересую?" Затем описать их с помощью интерфейса и запрограммировать интерфейс.

interface Dog {
  Breed getBreed();
  Set<Dog> getFavoritePlaymates(DayOfWeek dayOfWeek);
  void emitBarkingSound(double volume);
  Food getFavoriteFood(Instant asOfTime);
}

Когда вы понимаете поведение, решения по внедрению становятся намного понятнее.

Тогда эмпирическое правило для реализации заключается в том, чтобы поставить простые, общие поведения в абстрактном базовом классе:

abstract class AbstractDog implements Dog {
  private Breed breed;
  Dog(Breed breed) { this.breed = breed; }
  @Override Breed getBreed() { return breed; }
}

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

Иерархии реализации, подобные этой, могут быть полезны для уменьшения шаблона, но более 2-х глубин - это запах кода. Если вам требуется 3 или более уровней, очень вероятно, что вы можете и должны обернуть куски обычного поведения из классов низкого уровня в вспомогательных классах, которые будут легче тестироваться и доступны для композиции во всей системе. Например, вместо того, чтобы предлагать метод protected void emitSound(Mp3Stream sound); в базовом классе для использования наследниками, было бы гораздо предпочтительнее создать новый class SoundEmitter {} и добавить член final с этим типом в Dog.

Затем создайте конкретные классы, заполнив остальную часть поведения:

class Poodle extends AbstractDog {
  Poodle() { super(Breed.POODLE); }
  Set<Dog> getFavoritePlaymates(DayOfWeek dayOfWeek) { ... }
  Food getFavoriteFood(Instant asOfTime) { ... }
}

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

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

Ответ 3

Следующие комментарии относятся к условию, что подклассы фактически не расширяют функциональность своего суперкласса.

Из документа Oracle doc:

Signals that an I/O exception of some sort has occurred. This class is the general class of exceptions produced by failed or interrupted I/O operations.

В нем указано, что IOException является общим исключением. Если у нас есть причина перечисления:

enum cause{
    FileNotFound, CharacterCoding, ...;
}

Мы не сможем выполнить исключение IOException, если причина в нашем пользовательском коде не включена в перечисление. Другими словами, это делает IOException более конкретным вместо общего.

Предполагая, что мы не программируем библиотеку, а функциональность класса Dog ниже специфична для нашего бизнес-требования:

enum DogBreed {
    Bulldog, Poodle;
}

class Dog {
    DogBreed dogBreed;
    public Dog(DogBreed dogBreed) {
       this.dogBreed = dogBreed;
    }
}

Лично я думаю, что полезно использовать перечисление, потому что оно упрощает структуру класса (меньше классов).

Ответ 4

Вариант 1 должен перечислять все известные причины во время объявления.

Вариант 2 можно расширить, создав новые классы, не касаясь оригинальной декларации.

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

Это в основном O в SOLID, открытый для расширения, закрытый для модификации.

Но это также почему, например, во многих средах существует тип перечислений DayOfWeek. Крайне маловероятно, что западный мир внезапно просыпается в один прекрасный день и решает отправиться на 14 уникальных дней, или 8, или 6. Таким образом, занятия для них, вероятно, будут излишними. Эти вещи более закреплены в камне (стук на лесу).

Ответ 5

Первый код, который вы цитируете, включает исключения.

Наследование естественным образом подходит для типов исключений, потому что конструкция, предоставляемая языком, для дифференциации исключений, представляющих интерес в заявлении try-catch, заключается в использовании системы типов. Это означает, что мы можем легко выбрать только более специфический тип (FileNotFound) или более общий тип (IOException).

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

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

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

Ваш вариант 1 является скорее примером композиции, тогда как ваш вариант 2 явно является примером наследования.

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

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

Класс, который не предлагает никаких переопределений, никакого нового состояния, никаких новых методов, не отличается от его базы. Таким образом, в объявлении такого класса мало смысла, если мы не попытаемся выполнить экземпляр тестов на нем (например, конструкция языка обработки исключений выполняется под обложками). Мы не можем действительно сказать из этого примера, который был изобретен для целей запроса, есть ли добавленная ценность в этих подклассах, но это не так.

Чтобы быть ясным, есть много худшего примера наследования, например, когда (pre) занятие, такое как Учитель или Студент, наследуется от Лица. Это означает, что Учитель не может быть Студентом одновременно, если мы не включим еще больше классов, например. TeacherStudent, возможно, используя множественное наследование..

Мы можем назвать этот класс взрывом, так как иногда нам нужна матрица классов из-за неулокальных отношений is-a. (Добавьте один новый класс, и вам понадобится целая новая строка или столбец взорванных классов.)

Работа с дизайном, который страдает от классового взрыва, на самом деле создает больше работы для клиентов, потребляющих эти абстракции, так что это незакрепленная ситуация. Здесь речь идет о нашем доверии к естественному языку, потому что, когда мы говорим, что кто-то есть - Студент, это не с логической точки зрения тот же постоянный "is-a" /instance -of relationship (subclassing), а скорее потенциально-временную роль, которую играет Человек: одна из многих возможных ролей, которую Человек может играть одновременно. В этих случаях состав явно превосходит наследование.

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

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

Если вы управляете перечислением и можете перекомпилировать код как единицу по мере необходимости для обработки новых типов собак, вам не нужна эта расширяемость.

Ответ 6

Два представленных вами варианта фактически не выражают то, что я думаю, что вы пытаетесь достичь. То, что вы пытаетесь отличить, это состав и наследование.

Композиция работает следующим образом:

class Poodle {
    Legs legs;
    Tail tail;
}

class Bulldog {
    Legs legs;
    Tail tail;
}

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

Java выбрала наследование вместо композиции для IOException и FileNotFoundException.

То есть, FileNotFoundException является своего рода (т.е. extends) IOException и разрешает обработку только на основе идентичности суперкласса (хотя вы можете указать специальную обработку, если вы решите).

Аргументы для выбора композиции над наследованием хорошо репетируются другими и могут быть легко найдены путем поиска "композиции против наследования".

Ответ 7

Я считаю, что вы не понимаете силу наследования, каждый дочерний класс автоматически имеет функциональность суперкласса плюс все, что определено в дочернем классе. В случае FileNotFoundException; каждый дочерний класс автоматически является экземпляром суперкласса, поэтому FileNotFoundException является экземпляром IOException. Вот почему вы могли поймать FileNotFoundException в блоке catch IOException. Когда вызывается дочерний класс, суперкласс также вызывается и сохраняется в памяти с соответствующими свойствами и методами. Кроме того, FileNotFoundException является экземпляром java.lang.Exception. поэтому при создании FileNotFoundException вы также вызываете конструктор каждого класса, из которого вы наследуете. поэтому java.lang.Object > java.lang.Throwable > java.lang.Exception > java.io.IOException > java.io.FileNotFoundException, который показывает, что FileNotFoundException является экземпляром java.lang.Object. Таким образом, FileNotFoundException имеет все методы и свойства, доступные для каждого класса, который унаследован. С точки зрения дизайна она сводится к желаемой функциональности реализации класса, следует ли использовать наследование или нет, но использование наследования обычно включает в себя упрощение рефакторинга, тестирования и отладки кода. Если вы хотите изменить поведение FileNotFoundException, вы можете просто перейти к определению класса и изменить или переопределить реализации FileNotFoundException, не беспокоясь обо всем коде в суперклассах. Код суперкласса IOException работает без каких-либо знаний или зависимости от того, что содержится в FileNotFoundException. Он просто автоматически создается с любыми свойствами и методами всякий раз, когда вызывается FileNotFoundException. Поэтому, когда вы рассматриваете передовые методы развязки, абстракции и написания многоразового наследования кода, имеет смысл. Если вы хотите добавить скажут породу мопса и породу ирландского сеттера и хотели, чтобы эти классы вели себя по-другому, тогда наследование имело бы смысл. Если вы просто устанавливаете свойство с породой, и поведение все равно, то использование перечислимого значения может иметь больше смысла.