У меня была беседа с коллегой, и в итоге у нас возникли противоречивые представления о цели создания подклассов. Моя интуиция заключается в том, что если основная функция подкласса должна выражать ограниченный диапазон возможных значений его родителя, то, вероятно, это не должен быть подкласс. Он высказался за противоположную интуицию: то, что подклассы представляют объект более «специфично», и, следовательно, отношение подкласса является более подходящим.
Чтобы выразить свою интуицию более конкретно, я думаю, что если у меня есть подкласс, который расширяет родительский класс, но единственный код, который переопределяет подкласс, - это конструктор (да, я знаю, что конструкторы обычно не "переопределяют", терпите меня), тогда что было действительно необходимо, так это вспомогательный метод.
Например, рассмотрим этот несколько реальный класс:
public class DataHelperBuilder
{
public string DatabaseEngine { get; set; }
public string ConnectionString { get; set; }
public DataHelperBuilder(string databaseEngine, string connectionString)
{
DatabaseEngine = databaseEngine;
ConnectionString = connectionString;
}
// Other optional "DataHelper" configuration settings omitted
public DataHelper CreateDataHelper()
{
Type dataHelperType = DatabaseEngineTypeHelper.GetType(DatabaseEngine);
DataHelper dh = (DataHelper)Activator.CreateInstance(dataHelperType);
dh.SetConnectionString(ConnectionString);
// Omitted some code that applies decorators to the returned object
// based on omitted configuration settings
return dh;
}
}
Он утверждает, что было бы вполне уместно иметь такой подкласс:
public class SystemDataHelperBuilder
{
public SystemDataHelperBuilder()
: base(Configuration.GetSystemDatabaseEngine(),
Configuration.GetSystemConnectionString())
{
}
}
Итак, вопрос:
- Среди людей, которые говорят о шаблонах проектирования, какая из этих интуиций верна? Является ли подкласс, как описано выше, антишаблоном?
- Если это анти-паттерн, как его зовут?
Я прошу прощения, если это оказалось легко гуглируемым ответом; мои поиски в Google в основном возвращали информацию об анти-шаблоне телескопического конструктора, а не то, что я искал.
источник
Ответы:
Если все, что вы хотите сделать, - это создать класс X с определенными аргументами, то использование подклассов является странным способом выражения этого намерения, поскольку вы не используете какие-либо функции, которые предоставляют вам классы и наследование. На самом деле это не анти-паттерн, это просто странно и немного бессмысленно (если у вас нет других причин для этого). Более естественным способом выражения этого намерения был бы Фабричный метод , который в данном случае является причудливым названием для вашего «вспомогательного метода».
Что касается общей интуиции, то и «более конкретный», и «ограниченный диапазон» являются потенциально вредным способом мышления о подклассах, поскольку они оба подразумевают, что сделать Square подклассом Rectangle - хорошая идея. Не полагаясь на что-то формальное, такое как LSP, я бы сказал, что лучшая интуиция заключается в том, что подкласс либо обеспечивает реализацию базового интерфейса, либо расширяет интерфейс для добавления некоторых новых функций.
источник
SystemDataHelperBuilder
конкретно.Ваш коллега прав (если принять стандартные системы типов).
Подумайте об этом, классы представляют возможные юридические ценности. Если у класса
A
есть одно байтовое полеF
, вы можете подумать, что оноA
имеет 256 допустимых значений, но эта интуиция неверна.A
ограничивает "каждую перестановку значений когда-либо" до "должно иметь полеF
быть 0-255".Если вы расширяете
A
с помощьюB
которого есть другое поле байтаFF
, это добавляет ограничения . Вместо «значение, гдеF
байт», у вас есть «значение, гдеF
байт &&FF
также является байтом». Поскольку вы придерживаетесь старых правил, все, что работало для базового типа, все еще работает для вашего подтипа. Но так как вы добавляете правила, вы еще больше специализируетесь на том, что «может быть что угодно».Или думать об этом по- другому, предположим , что
A
была куча подтипов:B
,C
, иD
. Переменная типаA
может быть любым из этих подтипов, но переменная типаB
является более конкретной. Это (в большинстве типов систем) не может бытьC
илиD
.Enh? Наличие специализированных объектов полезно и не наносит явного вреда коду. Достижение этого результата с помощью подтипирования, возможно, немного сомнительно, но зависит от того, какие инструменты у вас есть в вашем распоряжении. Даже с лучшими инструментами эта реализация проста, понятна и надежна.
Я бы не стал рассматривать это как антипаттерн, поскольку он не является явно неправильным и плохим в любой ситуации.
источник
byte and byte
определенно имеет больше значений, чем просто типbyte
; 256 раз больше. Кроме того, я не думаю, что вы можете сопоставить правила с количеством элементов (или количеством элементов, если хотите быть точным). Он ломается, когда вы рассматриваете бесконечные множества. Чётных чисел столько же, сколько и целых, но знание того, что число четное, всё ещё является ограничением / дает мне больше информации, чем просто знание, что это целое число! То же самое можно сказать и о натуральных числах.n -> 2n
). Это не всегда возможно; Вы не можете сопоставить целые числа с реалами, поэтому набор реалов "больше", чем целые числа. Но вы можете отобразить (реальный) интервал [0, 1] на множество всех реалов, так что они будут иметь одинаковый «размер». Подмножества определяются как «каждый элементA
также является элементомB
» именно потому, что как только ваши множества становятся бесконечными, это может быть в том случае, если они имеют одинаковую мощность, но имеют разные элементы.Нет, это не анти-паттерн. Я могу придумать несколько примеров практического использования для этого:
MySQLDao
иSqliteDao
в вашей системе, но по какой-то причине вы хотите убедиться, что коллекция содержит данные только из одного источника, если вы используете подклассы, как вы описываете, вы можете заставить компилятор проверить эту конкретную корректность. С другой стороны, если вы сохраните источник данных в виде поля, это станет проверкой во время выполнения.AlgorithmConfiguration
+ProductClass
иAlgorithmInstance
. Другими словами, если вы настраиваетеFooAlgorithm
дляProductClass
в файле свойств, вы можете получить только одинFooAlgorithm
для этого класса продукта. Единственный способ получить два значенияFooAlgorithm
для заданногоProductClass
- создать подклассFooBarAlgorithm
. Это нормально, потому что это делает файл свойств простым в использовании (нет необходимости в синтаксисе для многих различных конфигураций в одном разделе в файле свойств), и очень редко будет более одного экземпляра для данного класса продукта.Итог: в этом нет никакого вреда. Анти-паттерн определяется как:
Здесь нет риска быть крайне контрпродуктивным, так что это не анти-паттерн.
источник
Если что-то является шаблоном или антипаттерном, существенно зависит от того, на каком языке и в какой среде вы пишете. Например,
for
циклы - это шаблон в ассемблере, C и аналогичных языках и анти-шаблон в lisp.Позвольте мне рассказать вам историю некоторого кода, который я написал давно ...
Он был на языке LPC и реализовал основу для наложения заклинаний в игре. У вас был суперкласс заклинаний, у которого был подкласс боевых заклинаний, которые обрабатывали некоторые проверки, а затем подкласс для заклинаний прямого урона для одной цели, которые затем были разделены на отдельные заклинания - магическая ракета, заряд молнии и тому подобное.
Характер работы LPC заключался в том, что у вас был главный объект, который был Singleton. до того, как вы пошли 'eww Singleton' - это было и не было вообще. Никто не делал копии заклинаний, а скорее вызывал (эффективно) статические методы в заклинаниях. Код для магической ракеты выглядел так:
И ты видишь это? Это только класс конструктора. Он установил некоторые значения, которые были в его родительском абстрактном классе (нет, он не должен использовать сеттеры - его неизменяемый), и это было все. Это сработало правильно . Это было легко кодировать, легко расширять и легко понимать.
Этот тип шаблона транслируется в другие ситуации, когда у вас есть подкласс, который расширяет абстрактный класс и устанавливает несколько собственных значений, но все функции находятся в абстрактном классе, который он наследует. Посмотрите на StringBuilder в Java для примера этого - не совсем конструктор, но я вынужден найти много логики в самих методах (все в AbstractStringBuilder).
Это не плохо, и, конечно, это не анти-паттерн (а в некоторых языках это может быть сам паттерн).
источник
Наиболее распространенное использование таких подклассов, о которых я могу думать, - это иерархии исключений, хотя это вырожденный случай, когда мы, как правило, вообще ничего не определяем в классе, если язык позволяет нам наследовать конструкторы. Мы используем наследование, чтобы выразить, что
ReadError
это особый случайIOError
, и так далее, ноReadError
не нужно переопределять какие-либо методыIOError
.Мы делаем это потому, что исключения обнаруживаются путем проверки их типа. Таким образом, нам нужно закодировать специализацию в типе, чтобы кто-то мог поймать только,
ReadError
не поймав всеIOError
, если они захотят.Как правило, проверка типов вещей ужасно плохая форма, и мы стараемся избегать этого. Если нам удастся избежать этого, то нет необходимости выражать специализации в системе типов.
Тем не менее, это может быть полезно, например, если имя типа отображается в форме записанной строки объекта: это зависит от языка и, возможно, от структуры. Вы могли бы в принципе заменить имя типа в любой такой системе с помощью вызова метода класса, в этом случае мы бы преимущественным больше , чем просто конструктор в
SystemDataHelperBuilder
, мы также переопределитьloggingname()
. Поэтому, если в вашей системе есть такая регистрация, вы можете себе представить, что, хотя ваш класс буквально переопределяет конструктор, «морально» он также переопределяет имя класса, и поэтому для практических целей это не только подкласс конструктора. Желательно другое поведение,Итак, я бы сказал, что его код может быть хорошей идеей, если в другом месте есть какой-то код, который каким-то образом проверяет наличие экземпляров
SystemDataHelperBuilder
, либо явно с ветвью (например, перехват исключений, основанный на типе), либо, возможно, потому что есть некоторый общий код, который в итоге закончится используя имяSystemDataHelperBuilder
полезным способом (даже если это просто логирование).Однако использование типов в особых случаях приводит к некоторой путанице, поскольку если
ImmutableRectangle(1,1)
иImmutableSquare(1)
ведут себя иначе , то в конечном итоге кто-то задумывается, почему и, возможно, пожелает этого. В отличие от иерархий исключений, вы проверяете, является ли прямоугольник квадратом, проверяяheight == width
, не с помощьюinstanceof
. В вашем примере есть разница между aSystemDataHelperBuilder
и a,DataHelperBuilder
созданным с точно такими же аргументами, и это может вызвать проблемы. Следовательно, функция для возврата объекта, созданного с «правильными» аргументами, обычно кажется мне правильной: подкласс приводит к двум различным представлениям «одного и того же», и это помогает, только если вы делаете что-то обычно старался бы не делать.Обратите внимание, что я не говорю о шаблонах проектирования и анти-шаблонах, потому что я не верю, что можно предоставить таксономию всех хороших и плохих идей, о которых когда-либо думал программист. Таким образом, не каждая идея является шаблоном или анти-шаблоном, а становится только тем, что когда кто-то идентифицирует, что это происходит неоднократно в разных контекстах, и называет его ;-) Я не знаю ни одного конкретного имени для этого вида подклассов.
источник
ImmutableSquareMatrix:ImmutableMatrix
, при условии, что был эффективный способ попросить,ImmutableMatrix
чтобы представить его содержание какImmutableSquareMatrix
возможный. Даже если бы вImmutableSquareMatrix
основном былImmutableMatrix
конструктор, чей конструктор требовал совпадения высоты и ширины, и чейAsSquareMatrix
метод просто возвращал бы себя (а не, напримерImmutableSquareMatrix
, создавал объект, который оборачивает один и тот же базовый массив), такой дизайн позволял бы проверять параметры во время компиляции для методов, которые требуют квадрат матрицыSystemDataHelperBuilder
, а не только те, которыеDataHelperBuilder
будут выполнять, то имеет смысл определить тип для этого (и интерфейс, если вы обрабатываете DI таким образом) , В вашем примере есть некоторые операции с квадратной матрицей, которых не было бы у неквадрата, например, определитель, но ваша точка зрения хороша тем, что даже если системе не нужны те, которые вы все равно хотите проверить по типу ,height == width
, не с помощьюinstanceof
". Вы можете предпочесть что-то, что вы можете утверждать статически.foo=new ImmutableMatrix(someReadableMatrix)
более понятен, чем типsomeReadableMatrix.AsImmutable()
или дажеImmutableMatrix.Create(someReadableMatrix)
, но последние формы предлагают полезные семантические возможности, которых у первого нет; если конструктор может сказать, что матрица является квадратной, то он может отражать свой тип, который может быть чище, чем требующийся код ниже по потоку, которому нужна квадратная матрица для создания нового экземпляра объекта.new
оператора. Класс - это просто вызываемый объект, который при вызове возвращает (по умолчанию) экземпляр самого себя. Поэтому, если вы хотите сохранить целостность интерфейса, вполне возможно написать фабричную функцию, имя которой начинается с заглавной буквы, поэтому она выглядит как вызов конструктора. Не то, чтобы я делал это обычно с заводскими функциями, но это там, где это нужно.Самое строгое объектно-ориентированное определение отношения подкласса известно как «is-a». Используя традиционный пример квадрата и прямоугольника, квадрат представляет собой прямоугольник «is-a».
Таким образом, вы должны использовать подклассы для любой ситуации, когда вы чувствуете, что «is-a» является значимым отношением для вашего кода.
В вашем конкретном случае подкласса, в котором нет ничего, кроме конструктора, вы должны проанализировать его с точки зрения «is-a». Понятно, что SystemDataHelperBuilder «is-a» DataHelperBuilder, поэтому первый проход предполагает, что это допустимое использование.
Вопрос, на который вы должны ответить, заключается в том, использует ли какой-либо другой код это отношение «есть». Любой другой код пытается отличить SystemDataHelperBuilders от общего населения DataHelperBuilders? Если это так, отношения вполне разумны, и вы должны сохранить подкласс.
Однако, если никакой другой код не делает этого, то то, что у вас есть, это Relationshp, который может быть отношением «есть» или может быть реализован с помощью фабрики. В этом случае вы могли бы пойти любым путем, но я бы порекомендовал Фабрику, а не отношения «есть». При анализе объектно-ориентированного кода, написанного другим человеком, иерархия классов является основным источником информации. Он обеспечивает отношения «is-a», которые им понадобятся для понимания кода. Соответственно, помещение этого поведения в подкласс продвигает поведение от «чего-то, что кто-то найдет, если они будут копаться в методах суперкласса», до «чего-то, что все будут просматривать, прежде чем они даже начнут погружаться в код». Цитируя Гвидо ван Россума, «код читается чаще, чем пишется» так что снижение читабельности вашего кода для будущих разработчиков - это жесткая цена. Я бы предпочел не платить, если бы вместо этого мог использовать фабрику.
источник
Подтип никогда не бывает «неправильным», если вы всегда можете заменить экземпляр его супертипа на экземпляр подтипа, и все будет работать правильно. Это проверяется до тех пор, пока подкласс никогда не пытается ослабить какие-либо гарантии, которые дает супертип. Это может дать более сильные (более конкретные) гарантии, поэтому в этом смысле интуиция вашего коллеги верна.
Учитывая это, подкласс, который вы создали, вероятно, не является неправильным (поскольку я не знаю точно, что они делают, я не могу сказать наверняка.) Вы должны опасаться проблемы хрупкого базового класса, и вы также должны подумайте,
interface
принесет ли вам пользу, но это верно для всех видов наследования.источник
Я думаю, что ключом к ответу на этот вопрос является рассмотрение этого конкретного сценария использования.
Наследование используется для реализации параметров конфигурации базы данных, например строки подключения.
Это анти-паттерн, потому что класс нарушает принцип единой ответственности - он конфигурирует себя с конкретным источником этой конфигурации, а не «делает что-то и делает это хорошо». Конфигурация класса не должна быть включена в сам класс. Что касается настройки строк подключения к базе данных, в большинстве случаев я видел, как это происходит на уровне приложения во время какого-либо события, происходящего при запуске приложения.
источник
Я использую только подклассы конструктора довольно регулярно, чтобы выразить различные понятия.
Например, рассмотрим следующий класс:
Затем мы можем создать подкласс:
Затем вы можете использовать CapitalizeAndPrintOutputter где угодно в вашем коде, и вы точно знаете, какой это тип выходного устройства. Кроме того, этот шаблон на самом деле позволяет очень просто использовать DI-контейнер для создания этого класса. Если контейнер DI знает о PrintOutputMethod и Capitalizer, он может даже автоматически создать для вас CapitalizeAndPrintOutputter.
Использование этого шаблона действительно довольно гибко и полезно. Да, вы можете использовать фабричные методы, чтобы сделать то же самое, но тогда (по крайней мере, в C #) вы потеряете часть мощности системы типов и, конечно, использование DI-контейнеров станет более громоздким.
источник
Позвольте мне сначала заметить, что при написании кода подкласс на самом деле не более конкретен, чем родительский, поскольку оба инициализированных поля устанавливаются клиентским кодом.
Экземпляр подкласса можно различить только с помощью понижения.
Теперь, если у вас есть проект , в котором
DatabaseEngine
иConnectionString
свойства переопределена подкласса, который доступConfiguration
сделать это, то у вас есть ограничение , которое имеет смысл иметь подкласс.Сказав это, «анти-шаблон» слишком силен для моего вкуса; здесь нет ничего плохого.
источник
В приведенном вами примере ваш партнер прав. Два помощника по созданию данных являются стратегиями. Один более специфичен, чем другой, но оба они взаимозаменяемы после инициализации.
По сути, если бы клиент datahelperbuilder должен был получить systemdatahelperbuilder, ничего бы не сломалось. Другим вариантом было бы сделать systemdatahelperbuilder статическим экземпляром класса datahelperbuilder.
источник