В последнее время меня беспокоит использование абстрактных классов.
Иногда абстрактный класс создается заранее и работает как шаблон того, как будут работать производные классы. Это означает, более или менее, что они предоставляют некоторые функциональные возможности высокого уровня, но не учитывают некоторые детали, которые должны быть реализованы производными классами. Абстрактный класс определяет потребность в этих деталях, создавая некоторые абстрактные методы. В таких случаях абстрактный класс работает как проект, высокоуровневое описание функциональности или как вы хотите это назвать. Он не может использоваться сам по себе, но должен быть специализированным для определения деталей, которые были исключены из реализации высокого уровня.
Иногда случается, что абстрактный класс создается после создания некоторых «производных» классов (поскольку родительского / абстрактного класса там нет, они еще не были получены, но вы понимаете, что я имею в виду). В этих случаях абстрактный класс обычно используется как место, где вы можете поместить любой тип общего кода, который содержат текущие производные классы.
Сделав вышесказанное, мне интересно, какой из этих двух случаев должен быть правилом. Должны ли какие-либо подробности передаваться абстрактному классу только потому, что они в настоящее время являются общими для всех производных классов? Должен ли существовать общий код, который не является частью высокоуровневой функциональности?
Должен ли быть код, который может не иметь никакого значения для самого абстрактного класса, только потому, что он является общим для производных классов?
Позвольте мне привести пример: у абстрактного класса A есть метод a () и абстрактный метод aq (). Метод aq () в обоих производных классах AB и AC использует метод b (). Должен ли b () переместиться в A? Если да, то если кто-то смотрит только на A (давайте притворимся, что AB и AC не существуют), существование b () не имело бы особого смысла! Это плохо? Должен ли кто-то иметь возможность взглянуть на абстрактный класс и понять, что происходит, не посещая производные классы?
Честно говоря, в момент, когда я спрашивал об этом, я склонен полагать, что написание абстрактного класса, который имеет смысл без необходимости смотреть на производные классы, это вопрос чистого кода и чистой архитектуры. Really на самом деле не нравится, что идея абстрактного класса, который действует как дамп для любого вида кода, часто встречается во всех производных классах.
Что ты думаешь / практикуешь?
источник
Writing an abstract class that makes sense without having to look in the derived classes is a matter of clean code and clean architecture.
-- Почему? Разве не возможно, что в ходе проектирования и разработки приложения я обнаружил, что несколько классов имеют общие функциональные возможности, которые естественно могут быть преобразованы в абстрактный класс? Должен ли я быть достаточно ясновидящим, чтобы всегда предвидеть это, прежде чем писать какой-либо код для производных классов? Если мне не удастся быть таким проницательным, запрещено ли мне проводить такой рефакторинг? Должен ли я выбросить свой код и начать все сначала?Ответы:
Что вы спрашиваете, где разместить
b()
, и, в некотором другом смысле, вопрос в том,A
является ли лучший выбор в качестве непосредственного суперкласса дляAB
иAC
.Кажется, есть три варианта:
b()
в обоихAB
иAC
ABAC-Parent
который наследуетA
и представляетb()
, а затем используется в качестве непосредственного суперкласса дляAB
иAC
b()
вA
(не зная другой класс в будущемAD
захочетb()
или нет)Пока другой класс
AD
, который не хочетb()
представлять себя, (3) кажется правильным выбором.В то время, когда
AD
мы дарим подарки, мы можем изменить подход к (2) - ведь это программное обеспечение!источник
b()
в один класс. Сделайте его бесплатной функцией, которая принимает все свои данные в качестве аргументов и имеет обаAB
иAC
вызывает их. Это означает, что вам не нужно перемещать его или создавать больше классов при добавленииAD
.b()
нуждается ни в каком долговечном состоянии экземпляра.ac
вызовb
вместоac
абстрактного?Абстрактный класс не предназначен для того, чтобы быть местом сброса различных функций или данных, которые добавляются в абстрактный класс, потому что это удобно.
Единственное практическое правило, которое, по-видимому, обеспечивает наиболее надежный и расширяемый объектно-ориентированный подход, - это « Предпочитать композицию над наследованием ». Абстрактный класс, вероятно, лучше всего рассматривать как спецификацию интерфейса, которая не содержит никакого кода.
Если у вас есть какой-то метод, который является своего рода библиотечным методом, который идет вместе с абстрактным классом, метод, который является наиболее вероятным средством выражения некоторой функциональности или поведения, которое обычно требуется классам, производным от абстрактного класса, тогда имеет смысл создать класс между абстрактным классом и другими классами, где этот метод доступен. Этот новый класс обеспечивает конкретную реализацию абстрактного класса, определяющего конкретную альтернативу или путь реализации путем предоставления этого метода.
Идея абстрактного класса состоит в том, чтобы иметь абстрактную модель того, что производные классы, которые на самом деле реализуют абстрактный класс, должны обеспечивать до уровня обслуживания или поведения. Наследование легко использовать, и очень часто самые полезные классы состоят из различных классов, использующих шаблон mixin .
Однако всегда есть вопросы об изменениях, что изменится и как это изменится и почему это изменится.
Наследование может привести к хрупкому и хрупкому телу источника (см. Также Наследование: просто прекратите его использовать! ).
источник
b()
это чистая функция. Это может быть функтоид или шаблон или что-то еще. Я использую фразу «метод библиотеки» как обозначение некоторого компонента, будь то чистая функция, объект COM или что-либо еще, что используется для функциональности, которую оно предоставляет решению.Ваш вопрос предлагает либо подход, либо подход к абстрактным классам. Но я думаю, что вы должны рассматривать их как еще один инструмент в вашем наборе инструментов. И тогда возникает вопрос: для каких работ / задач абстрактные классы являются правильным инструментом?
Одним из отличных вариантов использования является реализация шаблона метода шаблона . Вы помещаете всю инвариантную логику в абстрактный класс и вариантную логику в его подклассы. Обратите внимание, что разделяемая логика сама по себе является неполной и нефункциональной. В большинстве случаев речь идет о реализации алгоритма, в котором несколько шагов всегда одинаковы, но варьируется как минимум один шаг. Поместите этот шаг как абстрактный метод, который вызывается из одной из функций внутри абстрактного класса.
Я думаю, что ваш первый пример - это, по сути, описание шаблона метода шаблона (поправьте меня, если я ошибаюсь), поэтому я считаю это вполне допустимым вариантом использования абстрактных классов.
Для вашего второго примера я думаю, что использование абстрактных классов не является оптимальным выбором, потому что существуют превосходные методы для работы с общей логикой и дублированным кодом. Допустим, у вас есть абстрактный класс
A
, производные классыB
иC
оба производных класса совместно используют некоторую логику в форме методаs()
. Чтобы выбрать правильный подход, чтобы избавиться от дублирования, важно знать, является ли методs()
частью общедоступного интерфейса или нет.Если это не так (как в вашем конкретном примере с методом
b()
), дело довольно простое. Просто создайте из него отдельный класс, также извлекая контекст, необходимый для выполнения операции. Это классический пример композиции над наследованием . Если контекст небольшой или отсутствует, простой вспомогательной функции может быть уже достаточно, как предлагается в некоторых комментариях.Если он
s()
является частью общедоступного интерфейса, он становится немного сложнее. В предположении, чтоs()
это не имеет ничего общегоB
и неC
имеет отношения кA
вам, вы не должны вкладыватьs()
внутрьA
. Так где же это поставить? Я бы поспорил за объявление отдельного интерфейсаI
, который определяетs()
. Опять же, вы должны создать отдельный класс, который содержит логику для реализацииs()
и то и другоеB
иC
зависит от него.Наконец, вот ссылка на отличный ответ на интересный вопрос о SO, который может помочь вам решить, когда переходить на интерфейс, а когда на абстрактный класс:
источник
Мне кажется, вы упускаете точку объектной ориентации, как логически, так и технически. Вы описываете два сценария: группировка общего поведения типов в базовом классе и полиморфизм. Оба они являются законными приложениями абстрактного класса. Но следует ли сделать класс абстрактным или нет, зависит от вашей аналитической модели, а не от технических возможностей.
Вы должны распознать тип, который не имеет воплощений в реальном мире, но закладывает основу для определенных типов, которые существуют. Пример: животное. Там нет такого понятия, как животное. Это всегда собака, кошка или что-то еще, но настоящего животного нет. Все же Животное создает их всех.
Тогда вы говорите об уровнях. Уровни не являются частью сделки, когда дело доходит до наследования. Признание общих данных или общего поведения также не является техническим подходом, который, скорее всего, не поможет. Вы должны распознать стереотип и затем вставить базовый класс. Если такого стереотипа нет, вам лучше использовать пару интерфейсов, реализованных несколькими классами.
Название вашего абстрактного класса вместе с его членами должно иметь смысл. Он должен быть независимым от любого производного класса, как технически, так и логически. Подобно Animal может иметь абстрактный метод Eat (который будет полиморфным) и логические абстрактные свойства IsPet и IsLifestock, которые имеют смысл, не зная о кошках, собаках или свиньях. Любая зависимость (техническая или логическая) должна идти только одним путем: от потомка к основанию. Сами базовые классы также не должны знать о нисходящих классах.
источник
Первый случай , из вашего вопроса:
Второй случай:
Тогда ваш вопрос :
IMO у вас есть два разных сценария, поэтому вы не можете нарисовать одно правило, чтобы решить, какой дизайн должен быть применен.
Для первого случая у нас есть такие вещи, как шаблонный метод , уже упомянутый в других ответах.
Во втором случае вы привели пример абстрактного класса A, получающего метод ab (); IMO метод b () следует перемещать, только если он имеет смысл для ВСЕХ других производных классов. Другими словами, если вы перемещаете его, потому что он используется в двух местах, возможно, это неправильный выбор, потому что завтра может появиться новый конкретный класс Derived, для которого b () вообще не имеет смысла. Кроме того, как вы сказали, если вы посмотрите на класс A отдельно и b () также не имеет смысла в этом контексте, то, вероятно, это намек на то, что это не очень хороший выбор дизайна.
источник
У меня были точно такие же вопросы некоторое время назад!
Я пришел к выводу, что всякий раз, когда я сталкиваюсь с этой проблемой, я обнаруживаю скрытую концепцию, которую я не нашел. И эта концепция, скорее всего, должна быть выражена каким - то ценностным объектом . У вас есть очень абстрактный пример, поэтому я боюсь, что не смогу продемонстрировать, что я имею в виду, используя ваш код. Но вот случай из моей собственной практики. У меня было два класса, которые представляли способ отправить запрос на внешний ресурс и разобрать ответ. Были сходства в том, как были сформированы запросы и как были проанализированы ответы. Вот так выглядела моя иерархия:
Такой код указывает, что я просто неправильно использую наследование. Вот как это может быть изменено:
С тех пор я очень бережно отношусь к наследству, потому что мне много раз причиняли боль.
С тех пор я пришел к другому выводу о наследовании. Я категорически против наследования, основанного на внутренней структуре . Это нарушает инкапсуляцию, это хрупко, это в конце концов процедурно. И когда я правильно разлагаю свой домен , наследование просто не происходит очень часто.
источник