Подклассы только для конструктора: это анти-шаблон?


37

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

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

Например, рассмотрим этот несколько реальный класс:

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())
    {
    }
 }

Итак, вопрос:

  1. Среди людей, которые говорят о шаблонах проектирования, какая из этих интуиций верна? Является ли подкласс, как описано выше, антишаблоном?
  2. Если это анти-паттерн, как его зовут?

Я прошу прощения, если это оказалось легко гуглируемым ответом; мои поиски в Google в основном возвращали информацию об анти-шаблоне телескопического конструктора, а не то, что я искал.


2
Я считаю слово «Помощник» в названии антипаттерном. Это все равно, что читать эссе с постоянными ссылками на вещи и прочее. Скажи, что это такое . Это фабрика? Конструктор? Для меня это отдает ленивым мыслительным процессом и поощряет нарушение принципа единой ответственности, поскольку правильное семантическое имя будет направлять его. Я согласен с другими избирателями, и вы должны принять ответ @ lxrec. Создание подкласса для фабричного метода совершенно бессмысленно, если только вы не можете доверять пользователям передачу правильных аргументов, как указано в документации. В этом случае, получить новых пользователей.
Аарон Холл

Я абсолютно согласен с ответом @ Ixrec. В конце концов, его ответ говорит, что я был прав все время, а мой коллега был неправ. ;-) А если серьезно, именно поэтому я чувствовал себя странно, принимая это; Я думал, что меня могут обвинить в предвзятости. Но я новичок в программировании StackExchange; Я был бы готов принять это, если бы я был уверен, что это не будет нарушением этикета.
трудом узнаешь

1
Нет, ожидается, что вы примете ответ, который считаете наиболее ценным. То, что сообщество считает наиболее ценным на сегодняшний день, - это Ixrec на более чем 3 к 1. Таким образом, они ожидают, что вы примете это. В этом сообществе очень мало разочарований, выраженных спрашивающим, принимающим неправильный ответ, но есть большое разочарование, выраженное среди спрашивающих, которые никогда не принимают ответ. Обратите внимание, что никто не жалуется на принятый здесь ответ, вместо этого они жалуются на голосование, которое, кажется, исправляет себя: stackoverflow.com/q/2052390/541136
Аарон Холл

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

Ответы:


54

Если все, что вы хотите сделать, - это создать класс X с определенными аргументами, то использование подклассов является странным способом выражения этого намерения, поскольку вы не используете какие-либо функции, которые предоставляют вам классы и наследование. На самом деле это не анти-паттерн, это просто странно и немного бессмысленно (если у вас нет других причин для этого). Более естественным способом выражения этого намерения был бы Фабричный метод , который в данном случае является причудливым названием для вашего «вспомогательного метода».

Что касается общей интуиции, то и «более конкретный», и «ограниченный диапазон» являются потенциально вредным способом мышления о подклассах, поскольку они оба подразумевают, что сделать Square подклассом Rectangle - хорошая идея. Не полагаясь на что-то формальное, такое как LSP, я бы сказал, что лучшая интуиция заключается в том, что подкласс либо обеспечивает реализацию базового интерфейса, либо расширяет интерфейс для добавления некоторых новых функций.


2
Фабричные методы, тем не менее, не позволяют вам принудительно применять определенные поведения (управляемые аргументами) в вашей кодовой базе. И иногда полезно явно аннотировать, что этот класс / метод / поле занимает SystemDataHelperBuilderконкретно.
Теластин

3
Ах, я думаю, что аргумент квадрат против прямоугольника был именно тем, что я искал. Благодарность!
Едва ли знаю

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

10
Суть проблемы «квадрат / прямоугольник» в том, что геометрические фигуры неизменны. Вы всегда можете использовать квадрат везде, где имеет смысл прямоугольник, но изменение размеров - это не то, что делают квадраты или прямоугольники.
Доваль

3
@nha LSP = Принцип замещения Лискова
Ixrec

16

Какая из этих интуиций верна?

Ваш коллега прав (если принять стандартные системы типов).

Подумайте об этом, классы представляют возможные юридические ценности. Если у класса Aесть одно байтовое поле F, вы можете подумать, что оно Aимеет 256 допустимых значений, но эта интуиция неверна. Aограничивает "каждую перестановку значений когда-либо" до "должно иметь поле Fбыть 0-255".

Если вы расширяете Aс помощью Bкоторого есть другое поле байта FF, это добавляет ограничения . Вместо «значение, где Fбайт», у вас есть «значение, где Fбайт && FFтакже является байтом». Поскольку вы придерживаетесь старых правил, все, что работало для базового типа, все еще работает для вашего подтипа. Но так как вы добавляете правила, вы еще больше специализируетесь на том, что «может быть что угодно».

Или думать об этом по- другому, предположим , что Aбыла куча подтипов: B, C, и D. Переменная типа Aможет быть любым из этих подтипов, но переменная типа Bявляется более конкретной. Это (в большинстве типов систем) не может быть Cили D.

Это анти-паттерн?

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

Я бы не стал рассматривать это как антипаттерн, поскольку он не является явно неправильным и плохим в любой ситуации.


5
«Больше правил означает меньше возможных значений, значит, более конкретные». Я действительно не следую этому. Тип byte and byteопределенно имеет больше значений, чем просто тип byte; 256 раз больше. Кроме того, я не думаю, что вы можете сопоставить правила с количеством элементов (или количеством элементов, если хотите быть точным). Он ломается, когда вы рассматриваете бесконечные множества. Чётных чисел столько же, сколько и целых, но знание того, что число четное, всё ещё является ограничением / дает мне больше информации, чем просто знание, что это целое число! То же самое можно сказать и о натуральных числах.
Доваль

6
@Telastyn Мы получаем не по теме, но можно доказать, что количество элементов (четных, «размер») набора четных целых чисел совпадает с набором всех целых чисел или даже всех рациональных чисел. Это действительно классные вещи. Видеть Math.grinnell.edu/~miletijo/museum/infinite.html
HardlyKnowEm

2
Теория множеств довольно неинтуитивна. Вы можете поместить целые и четные числа в соответствие один к одному (например, используя функцию n -> 2n). Это не всегда возможно; Вы не можете сопоставить целые числа с реалами, поэтому набор реалов "больше", чем целые числа. Но вы можете отобразить (реальный) интервал [0, 1] на множество всех реалов, так что они будут иметь одинаковый «размер». Подмножества определяются как «каждый элемент Aтакже является элементом B» именно потому, что как только ваши множества становятся бесконечными, это может быть в том случае, если они имеют одинаковую мощность, но имеют разные элементы.
Доваль

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

1
@Doval: существует более одного возможного значения «меньше» - то есть, более одного порядка упорядочения на множествах. Отношение «является подмножеством» дает четко определенный (частичный) порядок на множествах. Более того, «больше правил» может означать, что «все элементы множества удовлетворяют большему количеству (т. Е. Надмножеству) предложений (из определенного множества)». С этими определениями «больше правил» действительно означает «меньше ценностей». Это, грубо говоря, подход, принятый рядом семантических моделей компьютерных программ, например доменов Скотта.
psmears

12

Нет, это не анти-паттерн. Я могу придумать несколько примеров практического использования для этого:

  1. Если вы хотите использовать проверку времени компиляции, чтобы убедиться, что коллекции объектов соответствуют только одному конкретному подклассу. Например, если у вас есть MySQLDaoи SqliteDaoв вашей системе, но по какой-то причине вы хотите убедиться, что коллекция содержит данные только из одного источника, если вы используете подклассы, как вы описываете, вы можете заставить компилятор проверить эту конкретную корректность. С другой стороны, если вы сохраните источник данных в виде поля, это станет проверкой во время выполнения.
  2. Мое текущее приложение имеет отношение один к одному между AlgorithmConfiguration+ ProductClassи AlgorithmInstance. Другими словами, если вы настраиваете FooAlgorithmдляProductClass в файле свойств, вы можете получить только один FooAlgorithmдля этого класса продукта. Единственный способ получить два значения FooAlgorithmдля заданного ProductClass- создать подкласс FooBarAlgorithm. Это нормально, потому что это делает файл свойств простым в использовании (нет необходимости в синтаксисе для многих различных конфигураций в одном разделе в файле свойств), и очень редко будет более одного экземпляра для данного класса продукта.
  3. @ Ответ Теластина дает еще один хороший пример.

Итог: в этом нет никакого вреда. Анти-паттерн определяется как:

Анти-паттерн (или антипаттерн) является распространенным ответом на повторяющуюся проблему, которая обычно неэффективна и рискует быть крайне контрпродуктивной.

Здесь нет риска быть крайне контрпродуктивным, так что это не анти-паттерн.


2
Да, я думаю, что если бы мне пришлось перефразировать вопрос снова, я бы сказал: «Запах кода». Это не так уж и плохо, чтобы предупредить следующее поколение молодых разработчиков, чтобы избежать этого, но это то, что, если вы видите это, это может указывать на наличие проблемы в всеобъемлющем дизайне, но в конечном итоге также может быть правильным.
HardlyKnowEm

Точка 1 точно в цель. Я не совсем понял пункт 2, но я уверен, что это так же хорошо ... :)
Фил

9

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

Позвольте мне рассказать вам историю некоторого кода, который я написал давно ...

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

Характер работы LPC заключался в том, что у вас был главный объект, который был Singleton. до того, как вы пошли 'eww Singleton' - это было и не было вообще. Никто не делал копии заклинаний, а скорее вызывал (эффективно) статические методы в заклинаниях. Код для магической ракеты выглядел так:

inherit "spells/direct";

void init() {
  ::init();
  damage = 10;
  cost = 3;
  delay = 1.0;
  caster_message = "You cast a magic missile at ${target}";
  target_message = "You are hit with a magic missile cast by ${caster}";
}

И ты видишь это? Это только класс конструктора. Он установил некоторые значения, которые были в его родительском абстрактном классе (нет, он не должен использовать сеттеры - его неизменяемый), и это было все. Это сработало правильно . Это было легко кодировать, легко расширять и легко понимать.

Этот тип шаблона транслируется в другие ситуации, когда у вас есть подкласс, который расширяет абстрактный класс и устанавливает несколько собственных значений, но все функции находятся в абстрактном классе, который он наследует. Посмотрите на StringBuilder в Java для примера этого - не совсем конструктор, но я вынужден найти много логики в самих методах (все в AbstractStringBuilder).

Это не плохо, и, конечно, это не анти-паттерн (а в некоторых языках это может быть сам паттерн).


2

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

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

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

Тем не менее, это может быть полезно, например, если имя типа отображается в форме записанной строки объекта: это зависит от языка и, возможно, от структуры. Вы могли бы в принципе заменить имя типа в любой такой системе с помощью вызова метода класса, в этом случае мы бы преимущественным больше , чем просто конструктор в SystemDataHelperBuilder, мы также переопределить loggingname(). Поэтому, если в вашей системе есть такая регистрация, вы можете себе представить, что, хотя ваш класс буквально переопределяет конструктор, «морально» он также переопределяет имя класса, и поэтому для практических целей это не только подкласс конструктора. Желательно другое поведение,

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

Однако использование типов в особых случаях приводит к некоторой путанице, поскольку если ImmutableRectangle(1,1)и ImmutableSquare(1)ведут себя иначе , то в конечном итоге кто-то задумывается, почему и, возможно, пожелает этого. В отличие от иерархий исключений, вы проверяете, является ли прямоугольник квадратом, проверяя height == width, не с помощью instanceof. В вашем примере есть разница между a SystemDataHelperBuilderи a, DataHelperBuilderсозданным с точно такими же аргументами, и это может вызвать проблемы. Следовательно, функция для возврата объекта, созданного с «правильными» аргументами, обычно кажется мне правильной: подкласс приводит к двум различным представлениям «одного и того же», и это помогает, только если вы делаете что-то обычно старался бы не делать.

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


Я мог видеть использование для ImmutableSquareMatrix:ImmutableMatrix, при условии, что был эффективный способ попросить, ImmutableMatrixчтобы представить его содержание как ImmutableSquareMatrixвозможный. Даже если бы в ImmutableSquareMatrixосновном был ImmutableMatrixконструктор, чей конструктор требовал совпадения высоты и ширины, и чей AsSquareMatrixметод просто возвращал бы себя (а не, например ImmutableSquareMatrix, создавал объект, который оборачивает один и тот же базовый массив), такой дизайн позволял бы проверять параметры во время компиляции для методов, которые требуют квадрат матрицы
суперкат

@supercat: хорошая точка зрения, и в примере спрашивающего, если есть функции, которые должны быть переданы SystemDataHelperBuilder, а не только те, которые DataHelperBuilderбудут выполнять, то имеет смысл определить тип для этого (и интерфейс, если вы обрабатываете DI таким образом) , В вашем примере есть некоторые операции с квадратной матрицей, которых не было бы у неквадрата, например, определитель, но ваша точка зрения хороша тем, что даже если системе не нужны те, которые вы все равно хотите проверить по типу ,
Стив Джессоп

... так что я полагаю, что не совсем верно то, что я сказал, что "вы проверяете, является ли прямоугольник квадратом, проверяя height == width, не с помощью instanceof". Вы можете предпочесть что-то, что вы можете утверждать статически.
Стив Джессоп

Хотелось бы, чтобы был механизм, который позволял бы что-то вроде синтаксиса конструктора вызывать статические фабричные методы. Тип выражения foo=new ImmutableMatrix(someReadableMatrix)более понятен, чем тип someReadableMatrix.AsImmutable()или даже ImmutableMatrix.Create(someReadableMatrix), но последние формы предлагают полезные семантические возможности, которых у первого нет; если конструктор может сказать, что матрица является квадратной, то он может отражать свой тип, который может быть чище, чем требующийся код ниже по потоку, которому нужна квадратная матрица для создания нового экземпляра объекта.
суперкат

@supercat: Одна маленькая вещь в Python, которая мне нравится, это отсутствие newоператора. Класс - это просто вызываемый объект, который при вызове возвращает (по умолчанию) экземпляр самого себя. Поэтому, если вы хотите сохранить целостность интерфейса, вполне возможно написать фабричную функцию, имя которой начинается с заглавной буквы, поэтому она выглядит как вызов конструктора. Не то, чтобы я делал это обычно с заводскими функциями, но это там, где это нужно.
Стив Джессоп

2

Самое строгое объектно-ориентированное определение отношения подкласса известно как «is-a». Используя традиционный пример квадрата и прямоугольника, квадрат представляет собой прямоугольник «is-a».

Таким образом, вы должны использовать подклассы для любой ситуации, когда вы чувствуете, что «is-a» является значимым отношением для вашего кода.

В вашем конкретном случае подкласса, в котором нет ничего, кроме конструктора, вы должны проанализировать его с точки зрения «is-a». Понятно, что SystemDataHelperBuilder «is-a» DataHelperBuilder, поэтому первый проход предполагает, что это допустимое использование.

Вопрос, на который вы должны ответить, заключается в том, использует ли какой-либо другой код это отношение «есть». Любой другой код пытается отличить SystemDataHelperBuilders от общего населения DataHelperBuilders? Если это так, отношения вполне разумны, и вы должны сохранить подкласс.

Однако, если никакой другой код не делает этого, то то, что у вас есть, это Relationshp, который может быть отношением «есть» или может быть реализован с помощью фабрики. В этом случае вы могли бы пойти любым путем, но я бы порекомендовал Фабрику, а не отношения «есть». При анализе объектно-ориентированного кода, написанного другим человеком, иерархия классов является основным источником информации. Он обеспечивает отношения «is-a», которые им понадобятся для понимания кода. Соответственно, помещение этого поведения в подкласс продвигает поведение от «чего-то, что кто-то найдет, если они будут копаться в методах суперкласса», до «чего-то, что все будут просматривать, прежде чем они даже начнут погружаться в код». Цитируя Гвидо ван Россума, «код читается чаще, чем пишется» так что снижение читабельности вашего кода для будущих разработчиков - это жесткая цена. Я бы предпочел не платить, если бы вместо этого мог использовать фабрику.


1

Подтип никогда не бывает «неправильным», если вы всегда можете заменить экземпляр его супертипа на экземпляр подтипа, и все будет работать правильно. Это проверяется до тех пор, пока подкласс никогда не пытается ослабить какие-либо гарантии, которые дает супертип. Это может дать более сильные (более конкретные) гарантии, поэтому в этом смысле интуиция вашего коллеги верна.

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


1

Я думаю, что ключом к ответу на этот вопрос является рассмотрение этого конкретного сценария использования.

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

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


1

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

Например, рассмотрим следующий класс:

public class Outputter
{
    private readonly IOutputMethod outputMethod;
    private readonly IDataMassager dataMassager;

    public Outputter(IOutputMethod outputMethod, IDataMassager dataMassager)
    {
        this.outputMethod = outputMethod;
        this.dataMassager = dataMassager;
    }

    public MassageDataAndOutput(string data)
    {
        this.outputMethod.Output(this.dataMassager.Massage(data));
    }
}

Затем мы можем создать подкласс:

public class CapitalizeAndPrintOutputter : Outputter
{
    public CapitalizeAndPrintOutputter()
        : base(new PrintOutputMethod(), new Capitalizer())
    {
    }
}

Затем вы можете использовать CapitalizeAndPrintOutputter где угодно в вашем коде, и вы точно знаете, какой это тип выходного устройства. Кроме того, этот шаблон на самом деле позволяет очень просто использовать DI-контейнер для создания этого класса. Если контейнер DI знает о PrintOutputMethod и Capitalizer, он может даже автоматически создать для вас CapitalizeAndPrintOutputter.

Использование этого шаблона действительно довольно гибко и полезно. Да, вы можете использовать фабричные методы, чтобы сделать то же самое, но тогда (по крайней мере, в C #) вы потеряете часть мощности системы типов и, конечно, использование DI-контейнеров станет более громоздким.


1

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

Экземпляр подкласса можно различить только с помощью понижения.

Теперь, если у вас есть проект , в котором DatabaseEngineи ConnectionStringсвойства переопределена подкласса, который доступ Configurationсделать это, то у вас есть ограничение , которое имеет смысл иметь подкласс.

Сказав это, «анти-шаблон» слишком силен для моего вкуса; здесь нет ничего плохого.


0

В приведенном вами примере ваш партнер прав. Два помощника по созданию данных являются стратегиями. Один более специфичен, чем другой, но оба они взаимозаменяемы после инициализации.

По сути, если бы клиент datahelperbuilder должен был получить systemdatahelperbuilder, ничего бы не сломалось. Другим вариантом было бы сделать systemdatahelperbuilder статическим экземпляром класса datahelperbuilder.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.