Приведенный выше вопрос является абстрактным примером распространенной проблемы, с которой я сталкиваюсь в унаследованном коде, или, точнее, проблем, возникших в результате предыдущих попыток решить эту проблему.
Я могу вспомнить хотя бы один метод .NET Framework, предназначенный для решения этой проблемы, например Enumerable.OfType<T>
метод. Но тот факт, что вы в конечном итоге опрашиваете тип объекта во время выполнения, не подходит мне.
Помимо того, чтобы спрашивать каждую лошадь "Вы единорог?" Следующие подходы также приходят на ум:
- Бросьте исключение, когда сделана попытка определить длину рога не единорога (предоставляет функциональность, не подходящую для каждой лошади)
- Возвращает значение по умолчанию или магическое значение для длины рога не единорога (требуются проверки по умолчанию, распространяемые по всему коду, который хочет обработать статистику рога для группы лошадей, которые могут быть не единорогами)
- Избавьтесь от наследования и создайте на лошади отдельный объект, который скажет вам, является ли лошадь единорогом или нет (что потенциально может привести к возникновению той же проблемы в слое)
У меня есть ощущение, что на этот вопрос лучше всего ответить «не ответ». Но как вы подходите к этой проблеме и, если она зависит, каков контекст вашего решения?
Мне также было бы интересно узнать, существует ли эта проблема в функциональном коде (или, возможно, она существует только в функциональных языках, которые поддерживают изменчивость?)
Это было помечено как возможный дубликат следующего вопроса: Как избежать удушения?
Ответ на этот вопрос предполагает, что у человека есть HornMeasurer
все, с помощью которого должны быть выполнены все измерения рога. Но это навязывание кодовой базы, которая была сформирована по эгалитарному принципу, согласно которому каждый может свободно измерять рог лошади.
В отсутствие a HornMeasurer
подход принятого ответа отражает исключительный подход, перечисленный выше.
Также было некоторое замешательство в комментариях о том, являются ли лошади и единороги лошадьми, или единорог - волшебный подвид лошади. Обе возможности должны быть рассмотрены - возможно, одна предпочтительнее другой?
источник
Ответы:
Предполагая, что вы хотите относиться к
Unicorn
как к особому видуHorse
, вы можете смоделировать его двумя способами. Более традиционный способ - отношения подкласса. Вы можете избежать проверки типа и понижения рейтинга, просто реорганизовав свой код, чтобы всегда держать списки раздельными в тех контекстах, где это имеет значение, и объединять их только в тех контекстах, где вы никогда не заботитесь оUnicorn
чертах. Другими словами, вы организуете это так, что вы никогда не попадете в ситуацию, когда вам нужно сначала извлечь единорогов из табуна лошадей. Поначалу это кажется трудным, но возможно в 99,99% случаев и обычно делает ваш код намного чище.Другой способ, которым вы можете смоделировать единорога, это просто дать всем лошадям дополнительную длину рога. Затем вы можете проверить, является ли это единорог, проверив, имеет ли он длину рога, и найти среднюю длину рога всех единорогов по (в Scala):
Преимущество этого метода в том, что он более простой, с одним классом, но недостатком является то, что он гораздо менее расширяем и имеет своего рода обходной способ проверки на «единорог». Хитрость, с которой вы столкнетесь с этим решением, состоит в том, чтобы распознавать, когда вы начинаете часто его расширять, что вам нужно перейти к более гибкой архитектуре. Такое решение гораздо более популярно в функциональных языках, где у вас есть простые и мощные функции, такие как простая
flatMap
фильтрацияNone
элементов.источник
Horse
имеетIsUnicorn
свойство и какое-тоUnicornStuff
свойство с длиной рога (при масштабировании для райдера / блеска, упомянутого в вашем вопросе).Вы в значительной степени рассмотрели все варианты. Если у вас есть поведение, которое зависит от определенного подтипа и смешано с другими типами, ваш код должен знать об этом подтипе; это простые логические рассуждения.
Лично я бы просто пошел с
horses.OfType<Unicorn>().Average(u => u.HornLength)
. Он очень четко выражает намерение кода, что часто является наиболее важной вещью, поскольку кому-то в конечном итоге придется его поддерживать позже.источник
Unicorn
любом случае содержит только s (для записи, которую вы можете опуститьreturn
).В .NET нет ничего плохого в:
Использование эквивалента Linq тоже хорошо:
Основываясь на вопросе, который вы задали в заголовке, я хотел бы найти этот код. Если бы в вопросе говорилось что-то вроде «что такое среднее у животных с рогами», это было бы иначе:
Обратите внимание, что при использовании Linq
Average
(иMin
иMax
) вызовет исключение, если перечислимое значение пустое, а тип T не имеет значения NULL. Это потому, что среднее значение действительно не определено (0/0). Так что на самом деле вам нужно что-то вроде этого:редактировать
Я просто думаю, что это нужно добавить ... одна из причин, по которой такой вопрос не подходит для объектно-ориентированных программистов, заключается в том, что он предполагает, что мы используем классы и объекты для моделирования структуры данных. Первоначальная объектно-ориентированная идея Smalltalk-esque состояла в том, чтобы структурировать вашу программу из модулей, которые были созданы как объекты и выполняли для вас услуги, когда вы отправляли им сообщение. Тот факт, что мы также можем использовать классы и объекты для моделирования структуры данных, является (полезным) побочным эффектом, но это две разные вещи. Я даже не думаю, что последнее должно рассматриваться как объектно-ориентированное программирование, так как вы могли бы сделать то же самое с a
struct
, но это было бы не так красиво.Если вы используете объектно-ориентированное программирование для создания сервисов, которые делают что-то для вас, то вопрос о том, является ли этот сервис каким-то другим сервисом или конкретной реализацией, обычно не одобряется по уважительным причинам. Вам дали интерфейс (обычно через внедрение зависимостей), и вы должны написать код для этого интерфейса / контракта.
С другой стороны, если вы (неправильно) используете идеи класса / объекта / интерфейса для создания структуры данных или модели данных, то лично я не вижу проблемы с использованием идеи is-a в полной мере. Если вы определили, что единороги являются подтипом лошадей, и это вполне имеет смысл в вашем домене, тогда просто продолжайте и спросите лошадей в вашем стаде, чтобы найти единорогов. В конце концов, в таком случае мы обычно пытаемся создать предметно-ориентированный язык, чтобы лучше выразить решения проблем, которые мы должны решить. В этом смысле нет ничего плохого с
.OfType<Unicorn>()
т. Д.В конечном счете, сбор коллекции элементов и их фильтрация по типу - это на самом деле просто функциональное программирование, а не объектно-ориентированное программирование. К счастью, такие языки, как C #, теперь удобны для обеих парадигм.
источник
animal
этоUnicorn
; просто приведите, а не используйтеas
, или, возможно, даже лучше используйте,as
а затем проверьте на ноль.Проблема с этим утверждением состоит в том, что независимо от того, какой механизм вы используете, вы всегда будете опрашивать объект, чтобы определить его тип. Это может быть RTTI или объединение или простая структура данных, по вашему запросу
if horn > 0
. Точные особенности меняются незначительно, но цель та же - вы спрашиваете объект о себе каким-то образом, чтобы узнать, стоит ли его допрашивать дальше.Учитывая это, имеет смысл использовать поддержку вашего языка для этого.
typeof
Например, в .NET вы бы использовали .Причина, по которой это делается, не ограничивается использованием вашего языка. Если у вас есть объект, похожий на другой, но с небольшими изменениями, есть вероятность, что со временем вы обнаружите больше различий. В вашем примере с единорогами / лошадями вы можете сказать, что есть только длина рога ... но завтра вы будете проверять, является ли потенциальный всадник девственницей, или если корма блестит. (Классический пример из реальной жизни - это графические виджеты, основанные на общей базе, и вы должны искать флажки и списки по-разному. Количество различий было бы слишком велико, чтобы просто создать один суперобъект, содержащий все возможные перестановки данных. ).
Если проверка типа объектов во время выполнения не выполняется должным образом, то альтернативой является разделение различных объектов с самого начала - вместо хранения одного стада единорогов / лошадей у вас есть 2 коллекции - одна для лошадей, одна для единорогов , Это может работать очень хорошо, даже если вы храните их в специализированном контейнере (например, в мультикарте, где ключом является тип объекта ... но тогда, даже если мы храним их в 2 группах, мы сразу же возвращаемся к опросу типа объекта !)
Конечно, подход, основанный на исключениях, неверен. Использование исключений в качестве нормального потока программы - это запах кода (если у вас было стадо единорогов и осел с приклеенной к голове ракушкой, я бы сказал, что подход на основе исключений - это нормально, но если у вас есть стадо единорогов и лошади, тогда проверяющие каждого на единорог, не являются неожиданными. Исключения составляют исключительные обстоятельства, а не сложное
if
утверждение). В любом случае, использование исключений для этой проблемы - это просто опрос типа объекта во время выполнения, только здесь вы неправильно используете функцию языка для проверки объектов, не являющихся единорогами. Вы могли бы также код вif horn > 0
и, по крайней мере, обработайте вашу коллекцию быстро, четко, используя меньше строк кода и избегая любых проблем, возникающих после появления других исключений (например, пустой коллекции или попытки измерить раковину осла)источник
if horn > 0
именно так и решается эта проблема. Тогда проблемы, которые обычно возникают, это когда вы хотите проверить гонщиков и блеск, и ониhorn > 0
зарыты повсюду в несвязанном коде (также код страдает от загадочных ошибок из-за отсутствия проверок, когда рог имеет значение 0). Кроме того, создание подкласса лошади по факту, как правило, является самым дорогим предложением, поэтому я обычно не склонен делать это, если они все еще объединены в конце рефакторинга. Так что, безусловно, становится «насколько уродливы альтернативы»Поскольку у вопроса есть
functional-programming
тег, мы могли бы использовать тип суммы, чтобы отразить две разновидности лошадей, и сопоставление с образцом, чтобы устранить неоднозначность между ними. Например, в F #:По сравнению с ООП, FP обладает преимуществом разделения данных / функций, что, возможно, избавляет вас от (неоправданной?) «Нечистой совести» от нарушения уровня абстракции при переходе к определенным подтипам из списка объектов супертипа.
В отличие от ОО-решений, предложенных в других ответах, сопоставление с образцом также обеспечивает более легкую точку расширения, если другой Рогатый вид
Equine
обнаружится однажды.источник
Короткая форма того же ответа в конце требует чтения книги или веб-статьи.
Шаблон посетителя
В проблеме есть смесь лошадей и единорогов. (Нарушение принципа подстановки Лискова является распространенной проблемой в устаревших кодовых базах.)
Добавить метод в лошадь и все подклассы
Интерфейс Equine Visitor выглядит примерно так в java / c #
Для измерения рогов мы сейчас напишем ....
Шаблон посетителей подвергается критике за усложнение рефакторинга и роста.
Краткий ответ: используйте шаблон проектирования Visitor для двойной отправки.
см. также https://en.wikipedia.org/wiki/Visitor_pattern
см. также http://c2.com/cgi/wiki?VisitorPattern для обсуждения посетителей.
см. также Design Patterns by Gamma et al.
источник
Предполагая, что в вашей архитектуре единороги являются подвидом лошади, и вы встречаете места, где вы можете получить коллекцию,
Horse
где могут быть некоторые из нихUnicorn
, я лично выбрал бы первый метод (.OfType<Unicorn>()...
), потому что это самый простой способ выразить ваше намерение. , Для любого, кто придет позже (включая вас через 3 месяца), сразу становится очевидным, что вы пытаетесь выполнить с помощью этого кода: выберите единорогов среди лошадей.Другие методы, которые вы перечислили, выглядят как еще один способ задать вопрос «Вы единорог?». Например, если вы используете какой-то метод измерения рожков на основе исключений, у вас может быть код, который будет выглядеть примерно так:
Так что теперь исключением становится показатель того, что что-то не является единорогом. И теперь это не совсем исключительная ситуация, а часть нормального программного потока. И использование исключения вместо
if
кажется еще более грязным, чем просто проверка типа.Допустим, вы идете волшебным путем для проверки рогов на лошадях. Итак, теперь ваши классы выглядят примерно так:
Теперь ваш
Horse
класс должен знать оUnicorn
классе и иметь дополнительные методы для работы с вещами, которые ему не нужны. Теперь представьте, что у вас также естьPegasus
s иZebra
s, которые наследуются отHorse
. ТеперьHorse
нуженFly
метод, а такжеMeasureWings
,CountStripes
и т.д. И тогдаUnicorn
класс получает эти методы тоже. Теперь все ваши классы должны знать друг о друге, и вы загрязнили классы кучей методов, которых не должно быть там, чтобы не задавать вопрос системе типов "Является ли это единорогом?"Так что по поводу добавления чего-то к
Horse
s, чтобы сказать, если что-то естьUnicorn
и обрабатывать все измерения рога? Что ж, теперь вам нужно проверить существование этого объекта, чтобы узнать, является ли что-то единорогом (который просто заменяет одну проверку другой). Это также немного мутит воду, так как теперь у вас может бытьList<Horse> unicorns
что действительно содержит всех единорогов, но система типов и отладчик не могут легко сказать вам это. «Но я знаю, что это все единороги, - говорите вы, - само название так говорит». Ну а что, если что-то было плохо названо? Или, скажем, вы написали что-то с предположением, что это действительно будут единороги, но затем требования изменились, и теперь это может также смешать пегасы? (Потому что ничего подобного не происходит, особенно в устаревшем программном обеспечении / сарказме.) Теперь система типов с радостью поместит ваш пегас в единорогов. Если бы ваша переменная была объявлена какList<Unicorn>
компилятор (или среда выполнения), то она подойдет, если вы попытаетесь смешать в pegasi или horse.Наконец, все эти методы являются просто заменой для проверки системы типов. Лично я бы не стал изобретать велосипед здесь и надеялся, что мой код работает так же хорошо, как то, что встроено и проверено тысячами других кодировщиков тысячи раз.
В конечном счете, код должен быть понятен для вас . Компьютер поймет это независимо от того, как ты это пишешь. Вы тот, кто должен его отладить и уметь рассуждать об этом. Сделайте выбор, который облегчит вашу работу. Если по какой-либо причине один из этих других методов дает вам преимущество, заключающееся в том, что он перевешивает более понятный код в тех местах, где он будет отображаться, воспользуйтесь им. Но это зависит от вашей кодовой базы.
источник
if(horse.IsUnicorn) horse.MeasureHorn();
и исключения не будут обнаружены - они будут срабатывать, когда!horse.IsUnicorn
вы находитесь в контексте измерения единорога, или внутриMeasureHorn
не единорога. Таким образом, когда выдается исключение, вы не маскируете ошибки, оно полностью взрывается и является признаком того, что что-то нужно исправить. Очевидно, что это подходит только для определенных сценариев, но это реализация, которая не использует исключение для определения пути выполнения.Похоже, ваш семантический домен имеет отношение IS-A, но вы немного опасаетесь использовать подтипы / наследование для моделирования этого - особенно из-за отражения типа времени выполнения. Однако я думаю, что вы боитесь не того, что подтипирование действительно сопряжено с опасностями, но тот факт, что вы запрашиваете объект во время выполнения, не является проблемой. Вы поймете, что я имею в виду.
Объектно-ориентированное программирование довольно сильно опирается на понятие отношений IS-A, возможно, оно слишком сильно опирается на него, что приводит к двум известным критическим концепциям:
Но я думаю, что есть еще один, более функционально-ориентированный способ взглянуть на отношения IS-A, который, возможно, не имеет таких трудностей. Во-первых, мы хотим смоделировать лошадей и единорогов в нашей программе, поэтому у нас будет тип
Horse
иUnicorn
тип. Каковы значения этих типов? Ну, я бы сказал это:Это может показаться очевидным, но я думаю, что один из способов, с помощью которого люди начинают сталкиваться с такими проблемами, как проблема кругового эллипса, заключается в том, что они недостаточно внимательно следят за этими вопросами. Каждый круг является эллипсом, но это не означает, что каждое схематичное описание круга автоматически представляет собой схематическое описание эллипса в соответствии с другой схемой. Другими словами, только потому , что круг является эллипсом , не означает , что
Circle
этоEllipse
, так сказать. Но это значит, что:Circle
(описание схемы круга) вEllipse
(описание другого типа), которое описывает те же круги;Ellipse
и, если описывает круг, возвращает соответствующуюCircle
.Итак, в терминах функционального программирования ваш
Unicorn
тип вообще не должен быть подтипомHorse
, вам просто нужны такие операции:И
toUnicorn
должен быть правым обратным кtoHorse
:Maybe
Тип Haskell - это то, что другие языки называют типом «option». Например,Optional<Unicorn>
тип Java 8 - этоUnicorn
или ничего. Обратите внимание, что две из ваших альтернатив - выбрасывание исключения или возвращение «значения по умолчанию или магического значения» - очень похожи на типы опций.Поэтому в основном то, что я здесь сделал, это реконструкция концепции IS-A с точки зрения типов и функций без использования подтипов или наследования. Что бы я от этого отнял:
Horse
тип;Horse
потребности типа для кодирования достаточно информации , чтобы однозначно определить , описывает ли какое - либо значение единорога;Horse
типа должны предоставлять эту информацию, чтобы клиенты этого типа могли наблюдать, является ли данныйHorse
текст единорогом;Horse
типа должны будут использовать эти последние операции во время выполнения, чтобы различать единорогов и лошадей.Так что это принципиально модель «спроси каждого
Horse
, единорог». Вы настороженно относитесь к этой модели, но я так думаю. Если я дам вам списокHorse
s, все, что гарантирует тип, это то, что вещи, которые описывают элементы в списке, являются лошадьми - так что вам неизбежно понадобится что-то сделать во время выполнения, чтобы сказать, кто из них является единорогом. Так что я думаю, что обойти это невозможно - вам нужно реализовать операции, которые сделают это за вас.В объектно-ориентированном программировании знакомый способ сделать это заключается в следующем:
Horse
тип;Unicorn
в качестве подтипаHorse
;Horse
объект типомUnicorn
.Это имеет большую слабость, когда вы смотрите на это с позиции «вещь против описания», которую я представил выше:
Horse
экземпляр, который описывает единорога, но не являетсяUnicorn
экземпляром?Возвращаясь к началу, это то, что я считаю действительно страшной частью использования подтипов и даункастов для моделирования этих отношений IS-A - не факт, что вы должны выполнять проверку во время выполнения. Немного злоупотребляя типографикой, спрашивать, является
Horse
ли этоUnicorn
экземпляром, не является синонимом вопроса оHorse
том, является ли это единорогом (является ли этоHorse
описанием лошади, которая также является единорогом). Нет, если ваша программа не пошла на многое, чтобы инкапсулировать код,Horses
который создает, так что каждый раз, когда клиент пытается создать объектHorse
, описывающий единорога, создаетсяUnicorn
экземпляр класса. По моему опыту, программисты редко делают это осторожно.Так что я бы пошел с подходом, где есть явная, не пониженная операция, которая преобразует
Horse
s вUnicorn
s. Это может быть методHorse
типа:... или это может быть внешний объект (ваш "отдельный объект на лошади, который говорит вам, является ли лошадь единорогом или нет"):
Выбор между ними зависит от того, как организована ваша программа - в обоих случаях у вас есть эквивалент моей
Horse -> Maybe Unicorn
операции сверху, вы просто упаковываете ее по-разному (что, по общему признанию, будет иметь волновой эффект на то, какие операцииHorse
нужны типу) выставлять своим клиентам).источник
Комментарий ОП в другом ответе прояснил вопрос, подумал я
Если сформулировать это так, я думаю, что нам нужно больше информации. Ответ, вероятно, зависит от ряда вещей:
herd.averageHornLength()
кажется, соответствует нашей концептуальной модели.В общем, я бы даже не думал о наследовании и подтипах здесь. У вас есть список объектов. Некоторые из этих объектов могут быть идентифицированы как единороги, возможно, потому что у них есть
hornLength()
метод. Отфильтруйте список на основе этого уникального уникального свойства. Теперь проблема сводится к усреднению длины рога в списке единорогов.OP, дайте мне знать, если я все еще не понимаю ...
источник
HerdMember
), который мы инициализируем либо лошадью, либо единорогом (освобождая лошадь и единорога от необходимости отношения подтипа) ).HerdMember
затем может быть свободно реализовано,isUnicorn()
однако, по своему усмотрению, и предложенное мной решение для фильтрации следует.Метод GetUnicorns (), который возвращает IEnumerable, кажется мне наиболее элегантным, гибким и универсальным решением. Таким образом, вы можете иметь дело с любыми (комбинацией) черт, которые определяют, будет ли лошадь считаться единорогом, а не просто типом класса или значением определенного свойства.
источник
horses.ofType<Unicorn>...
конструкций. НаличиеGetUnicorns
функции было бы однострочным, но было бы еще более устойчивым к изменениям в отношениях лошадь / единорог с точки зрения вызывающего.IEnumerable<Horse>
, хотя ваши критерии единорога находятся в одном месте, он инкапсулирован, поэтому ваши абоненты должны делать предположения о том, почему им нужны единороги (я могу получить похлебку из моллюсков, заказав суп дня сегодня, но это не так) Я имею в виду, я получу это завтра, сделав то же самое). Кроме того, вы должны выставить значение по умолчанию для клаксона наHorse
. ЕслиUnicorn
это собственный тип, вы должны создать новый тип и поддерживать сопоставления типов, что может привести к дополнительным расходам.