Учитывая табун лошадей, как мне определить среднюю длину рога всех единорогов?

30

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

Я могу вспомнить хотя бы один метод .NET Framework, предназначенный для решения этой проблемы, например Enumerable.OfType<T>метод. Но тот факт, что вы в конечном итоге опрашиваете тип объекта во время выполнения, не подходит мне.

Помимо того, чтобы спрашивать каждую лошадь "Вы единорог?" Следующие подходы также приходят на ум:

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

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

Мне также было бы интересно узнать, существует ли эта проблема в функциональном коде (или, возможно, она существует только в функциональных языках, которые поддерживают изменчивость?)

Это было помечено как возможный дубликат следующего вопроса: Как избежать удушения?

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

В отсутствие a HornMeasurerподход принятого ответа отражает исключительный подход, перечисленный выше.

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

moarboilerplate
источник
22
У лошадей нет рогов, поэтому среднее значение не определено (0/0).
Скотт Уитлок
3
@moarboilerplate В любом месте от 10 до бесконечности.
няня
4
@StephenP: Это не будет работать математически для этого случая; все эти 0 будут искажать среднее.
Мейсон Уилер
3
Если на ваш вопрос лучше ответить без ответа, то он не принадлежит сайту вопросов и ответов; Reddit, Quora или другие сайты, основанные на обсуждении, созданы для вещей, не относящихся к типу ответов ... при этом, я думаю, что это может быть явно ответственно, если вы ищете код, который дал @MasonWheeler, если нет, я думаю, что понятия не имею что ты пытаешься спросить ..
Джимми Хоффа
3
@JimmyHoffa «вы делаете это неправильно» оказывается приемлемым «не ответом», и часто лучше, чем «ну, вот один из способов, которым вы могли бы это сделать» - расширенного обсуждения не требуется.
moarboilerplate

Ответы:

11

Предполагая, что вы хотите относиться к Unicornкак к особому виду Horse, вы можете смоделировать его двумя способами. Более традиционный способ - отношения подкласса. Вы можете избежать проверки типа и понижения рейтинга, просто реорганизовав свой код, чтобы всегда держать списки раздельными в тех контекстах, где это имеет значение, и объединять их только в тех контекстах, где вы никогда не заботитесь о Unicornчертах. Другими словами, вы организуете это так, что вы никогда не попадете в ситуацию, когда вам нужно сначала извлечь единорогов из табуна лошадей. Поначалу это кажется трудным, но возможно в 99,99% случаев и обычно делает ваш код намного чище.

Другой способ, которым вы можете смоделировать единорога, это просто дать всем лошадям дополнительную длину рога. Затем вы можете проверить, является ли это единорог, проверив, имеет ли он длину рога, и найти среднюю длину рога всех единорогов по (в Scala):

case class Horse(val hornLength: Option[Double])

val horse = Horse(None)
val unicorn = Horse(Some(12.0))
val anotherUnicorn = Horse(Some(6.0))

val herd = List(horse, unicorn, anotherUnicorn)
val hornLengths = herd flatMap {_.hornLength}
val averageLength = hornLengths.sum / hornLengths.size

Преимущество этого метода в том, что он более простой, с одним классом, но недостатком является то, что он гораздо менее расширяем и имеет своего рода обходной способ проверки на «единорог». Хитрость, с которой вы столкнетесь с этим решением, состоит в том, чтобы распознавать, когда вы начинаете часто его расширять, что вам нужно перейти к более гибкой архитектуре. Такое решение гораздо более популярно в функциональных языках, где у вас есть простые и мощные функции, такие как простая flatMapфильтрация Noneэлементов.

Карл Билефельдт
источник
7
Конечно, это предполагает, что единственная разница между обычной лошадью и единорогом - это рог. Если это не так, то все усложняется очень быстро.
Мейсон Уилер
@MasonWheeler только во втором представленном методе.
moarboilerplate
1
Обратите внимание на комментарии о том, что не-единороги и единороги никогда не должны сочиняться вместе в сценарии наследования, пока вы не окажетесь в ситуации, когда вас не волнуют единороги. Конечно, .OfType () может решить проблему и заставить ее работать, но это решает проблему, которой вообще не должно быть. Что касается второго подхода, он работает, потому что варианты намного лучше, чем полагаться на нуль, чтобы подразумевать что-то. Я думаю, что второй подход может быть достигнут в ОО с компромиссом, если вы инкапсулируете черты единорога в отдельном свойстве и крайне бдительны.
moarboilerplate
1
компромисс, если вы заключаете черты единорога в отдельную собственность и крайне бдительны - зачем создавать себе тяжелую жизнь. Используйте typeof напрямую и сохраните массу будущих проблем.
gbjbaanb
@gbjbaanb Я считаю, что этот подход действительно подходит только для сценариев, в которых анемика Horseимеет IsUnicornсвойство и какое-то UnicornStuffсвойство с длиной рога (при масштабировании для райдера / блеска, упомянутого в вашем вопросе).
moarboilerplate
38

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

Лично я бы просто пошел с horses.OfType<Unicorn>().Average(u => u.HornLength). Он очень четко выражает намерение кода, что часто является наиболее важной вещью, поскольку кому-то в конечном итоге придется его поддерживать позже.

Мейсон Уилер
источник
Пожалуйста, прости меня, если мой лямбда-синтаксис неправильный; Я не очень хорошо разбираюсь в C # и никогда не смогу точно описать тайные детали. Хотя должно быть понятно, что я имею в виду.
Мейсон Уилер
1
Не беспокойтесь, проблема в значительной степени решена, если список в Unicornлюбом случае содержит только s (для записи, которую вы можете опустить return).
moarboilerplate
4
Это ответ, на который я бы пошел, если бы я хотел быстро решить проблему. Но не ответ, если я хотел бы сделать код более правдоподобным.
Энди
6
Это определенно ответ, если вам не нужен абсурдный уровень оптимизации. Ясность и удобочитаемость этого материала делают практически все остальное спорным.
Дэвид говорит восстановить Монику
1
@DavidGrinberg, что, если написание этого чистого, читабельного метода означало, что вам сначала нужно будет реализовать структуру наследования, которой раньше не существовало?
moarboilerplate
9

В .NET нет ничего плохого в:

var unicorn = animal as Unicorn;
if(unicorn != null)
{
    sum += unicorn.HornLength;
    count++;
}

Использование эквивалента Linq тоже хорошо:

var averageUnicornHornLength = animals
    .OfType<Unicorn>()
    .Select(x => x.HornLength)
    .Average();

Основываясь на вопросе, который вы задали в заголовке, я хотел бы найти этот код. Если бы в вопросе говорилось что-то вроде «что такое среднее у животных с рогами», это было бы иначе:

var averageHornedAnimalHornLength = animals
    .OfType<IHornedAnimal>()
    .Select(x => x.HornLength)
    .Average();

Обратите внимание, что при использовании Linq AverageMinи Max) вызовет исключение, если перечислимое значение пустое, а тип T не имеет значения NULL. Это потому, что среднее значение действительно не определено (0/0). Так что на самом деле вам нужно что-то вроде этого:

var hornedAnimals = animals
    .OfType<IHornedAnimal>()
    .ToList();
if(hornedAnimals.Count > 0)
{
    var averageHornLengthOfHornedAnimals = hornedAnimals
        .Average(x => x.HornLength);
}
else
{
    // deal with it in your own way...
}

редактировать

Я просто думаю, что это нужно добавить ... одна из причин, по которой такой вопрос не подходит для объектно-ориентированных программистов, заключается в том, что он предполагает, что мы используем классы и объекты для моделирования структуры данных. Первоначальная объектно-ориентированная идея Smalltalk-esque состояла в том, чтобы структурировать вашу программу из модулей, которые были созданы как объекты и выполняли для вас услуги, когда вы отправляли им сообщение. Тот факт, что мы также можем использовать классы и объекты для моделирования структуры данных, является (полезным) побочным эффектом, но это две разные вещи. Я даже не думаю, что последнее должно рассматриваться как объектно-ориентированное программирование, так как вы могли бы сделать то же самое с a struct, но это было бы не так красиво.

Если вы используете объектно-ориентированное программирование для создания сервисов, которые делают что-то для вас, то вопрос о том, является ли этот сервис каким-то другим сервисом или конкретной реализацией, обычно не одобряется по уважительным причинам. Вам дали интерфейс (обычно через внедрение зависимостей), и вы должны написать код для этого интерфейса / контракта.

С другой стороны, если вы (неправильно) используете идеи класса / объекта / интерфейса для создания структуры данных или модели данных, то лично я не вижу проблемы с использованием идеи is-a в полной мере. Если вы определили, что единороги являются подтипом лошадей, и это вполне имеет смысл в вашем домене, тогда просто продолжайте и спросите лошадей в вашем стаде, чтобы найти единорогов. В конце концов, в таком случае мы обычно пытаемся создать предметно-ориентированный язык, чтобы лучше выразить решения проблем, которые мы должны решить. В этом смысле нет ничего плохого с .OfType<Unicorn>()т. Д.

В конечном счете, сбор коллекции элементов и их фильтрация по типу - это на самом деле просто функциональное программирование, а не объектно-ориентированное программирование. К счастью, такие языки, как C #, теперь удобны для обеих парадигм.

Скотт Уитлок
источник
7
Вы уже знаете, что animal это Unicorn ; просто приведите, а не используйте as, или, возможно, даже лучше используйте, as а затем проверьте на ноль.
Филипп Кендалл
3

Но тот факт, что вы в конечном итоге опрашиваете тип объекта во время выполнения, не подходит мне.

Проблема с этим утверждением состоит в том, что независимо от того, какой механизм вы используете, вы всегда будете опрашивать объект, чтобы определить его тип. Это может быть RTTI или объединение или простая структура данных, по вашему запросу if horn > 0. Точные особенности меняются незначительно, но цель та же - вы спрашиваете объект о себе каким-то образом, чтобы узнать, стоит ли его допрашивать дальше.

Учитывая это, имеет смысл использовать поддержку вашего языка для этого. typeofНапример, в .NET вы бы использовали .

Причина, по которой это делается, не ограничивается использованием вашего языка. Если у вас есть объект, похожий на другой, но с небольшими изменениями, есть вероятность, что со временем вы обнаружите больше различий. В вашем примере с единорогами / лошадями вы можете сказать, что есть только длина рога ... но завтра вы будете проверять, является ли потенциальный всадник девственницей, или если корма блестит. (Классический пример из реальной жизни - это графические виджеты, основанные на общей базе, и вы должны искать флажки и списки по-разному. Количество различий было бы слишком велико, чтобы просто создать один суперобъект, содержащий все возможные перестановки данных. ).

Если проверка типа объектов во время выполнения не выполняется должным образом, то альтернативой является разделение различных объектов с самого начала - вместо хранения одного стада единорогов / лошадей у ​​вас есть 2 коллекции - одна для лошадей, одна для единорогов , Это может работать очень хорошо, даже если вы храните их в специализированном контейнере (например, в мультикарте, где ключом является тип объекта ... но тогда, даже если мы храним их в 2 группах, мы сразу же возвращаемся к опросу типа объекта !)

Конечно, подход, основанный на исключениях, неверен. Использование исключений в качестве нормального потока программы - это запах кода (если у вас было стадо единорогов и осел с приклеенной к голове ракушкой, я бы сказал, что подход на основе исключений - это нормально, но если у вас есть стадо единорогов и лошади, тогда проверяющие каждого на единорог, не являются неожиданными. Исключения составляют исключительные обстоятельства, а не сложное ifутверждение). В любом случае, использование исключений для этой проблемы - это просто опрос типа объекта во время выполнения, только здесь вы неправильно используете функцию языка для проверки объектов, не являющихся единорогами. Вы могли бы также код вif horn > 0 и, по крайней мере, обработайте вашу коллекцию быстро, четко, используя меньше строк кода и избегая любых проблем, возникающих после появления других исключений (например, пустой коллекции или попытки измерить раковину осла)

gbjbaanb
источник
В унаследованном контексте if horn > 0именно так и решается эта проблема. Тогда проблемы, которые обычно возникают, это когда вы хотите проверить гонщиков и блеск, и они horn > 0зарыты повсюду в несвязанном коде (также код страдает от загадочных ошибок из-за отсутствия проверок, когда рог имеет значение 0). Кроме того, создание подкласса лошади по факту, как правило, является самым дорогим предложением, поэтому я обычно не склонен делать это, если они все еще объединены в конце рефакторинга. Так что, безусловно, становится «насколько уродливы альтернативы»
moarboilerplate
@moarboilerplate Вы говорите это сами, используйте дешевое и простое решение, и оно превратится в беспорядок. Вот почему ОО языки были изобретены, как решение этой проблемы. Поначалу подклассовая лошадь может показаться дорогой, но скоро окупится. Продолжая с простым, но грязным, решение стоит все больше и больше с течением времени.
gbjbaanb
3

Поскольку у вопроса есть functional-programmingтег, мы могли бы использовать тип суммы, чтобы отразить две разновидности лошадей, и сопоставление с образцом, чтобы устранить неоднозначность между ними. Например, в F #:

type Equine =
| Horse
| Unicorn of hornLength: float

module equines =

  let averageHornLength (equines : Equine list) =
    equines 
    |> List.choose (fun x -> 
      match x with
      | Unicorn u -> Some(u)
      | _ -> None)
    |> List.average

let herd = [ Horse ; Horse ; Unicorn(35.0) ; Horse ; Unicorn(50.0) ]

printfn "Average horn length in herd : %f" (equines.averageHornLength herd) // prints 42.5

По сравнению с ООП, FP обладает преимуществом разделения данных / функций, что, возможно, избавляет вас от (неоправданной?) «Нечистой совести» от нарушения уровня абстракции при переходе к определенным подтипам из списка объектов супертипа.

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

guillaume31
источник
2

Короткая форма того же ответа в конце требует чтения книги или веб-статьи.

Шаблон посетителя

В проблеме есть смесь лошадей и единорогов. (Нарушение принципа подстановки Лискова является распространенной проблемой в устаревших кодовых базах.)

Добавить метод в лошадь и все подклассы

Horse.visit(EquineVisitor v)

Интерфейс Equine Visitor выглядит примерно так в java / c #

interface EquineVisitor {
  void visitHorse(Horse z);
  void visitUnicorn(Unicorn z);
}

Unicorn.visit(EquineVisitor v){
   v.visitUnicorn(this);
}

Horse.visit(EquineVisitor v){
   v.visitHorse(this);
}

Для измерения рогов мы сейчас напишем ....

class HornMeasurer implements EquineVistor {
    void visitHorse(Horse h){} // ignore horses
    void visitUnicorn(Unicorn u){
         double len = u.getHornLength();
         totalLength+=len;
         unicornCount++;
    }

    double getAverageLength(){
          return totalLength/unicornCount;
    }

    double totalLength=0;
    int unicornCount=0;
}

Шаблон посетителей подвергается критике за усложнение рефакторинга и роста.

Краткий ответ: используйте шаблон проектирования Visitor для двойной отправки.

см. также https://en.wikipedia.org/wiki/Visitor_pattern

см. также http://c2.com/cgi/wiki?VisitorPattern для обсуждения посетителей.

см. также Design Patterns by Gamma et al.

Тим Виллискрофт
источник
Я собирался ответить с шаблоном посетителя самостоятельно. Пришлось прокрутить удивительный способ найти, если кто-то уже упомянул об этом!
Бен Терли
0

Предполагая, что в вашей архитектуре единороги являются подвидом лошади, и вы встречаете места, где вы можете получить коллекцию, Horseгде могут быть некоторые из них Unicorn, я лично выбрал бы первый метод ( .OfType<Unicorn>()...), потому что это самый простой способ выразить ваше намерение. , Для любого, кто придет позже (включая вас через 3 месяца), сразу становится очевидным, что вы пытаетесь выполнить с помощью этого кода: выберите единорогов среди лошадей.

Другие методы, которые вы перечислили, выглядят как еще один способ задать вопрос «Вы единорог?». Например, если вы используете какой-то метод измерения рожков на основе исключений, у вас может быть код, который будет выглядеть примерно так:

foreach (var horse in horses)
{
    try
    {
        var length = horse.MeasureHorn();
        //...
    }
    catch (NoHornException e)
    {
        continue;
    }
}

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

Допустим, вы идете волшебным путем для проверки рогов на лошадях. Итак, теперь ваши классы выглядят примерно так:

class Horse
{
    public double MeasureHorn() { return -1; }
    //...
}

class Unicorn : Horse
{
    public override double MeasureHorn { return _hornLength; }
    //...
}

Теперь ваш Horseкласс должен знать о Unicornклассе и иметь дополнительные методы для работы с вещами, которые ему не нужны. Теперь представьте, что у вас также есть Pegasuss и Zebras, которые наследуются от Horse. Теперь Horseнужен Flyметод, а такжеMeasureWings , CountStripesи т.д. И тогда Unicornкласс получает эти методы тоже. Теперь все ваши классы должны знать друг о друге, и вы загрязнили классы кучей методов, которых не должно быть там, чтобы не задавать вопрос системе типов "Является ли это единорогом?"

Так что по поводу добавления чего-то к Horses, чтобы сказать, если что-то есть Unicornи обрабатывать все измерения рога? Что ж, теперь вам нужно проверить существование этого объекта, чтобы узнать, является ли что-то единорогом (который просто заменяет одну проверку другой). Это также немного мутит воду, так как теперь у вас может бытьList<Horse> unicorns что действительно содержит всех единорогов, но система типов и отладчик не могут легко сказать вам это. «Но я знаю, что это все единороги, - говорите вы, - само название так говорит». Ну а что, если что-то было плохо названо? Или, скажем, вы написали что-то с предположением, что это действительно будут единороги, но затем требования изменились, и теперь это может также смешать пегасы? (Потому что ничего подобного не происходит, особенно в устаревшем программном обеспечении / сарказме.) Теперь система типов с радостью поместит ваш пегас в единорогов. Если бы ваша переменная была объявлена ​​как List<Unicorn>компилятор (или среда выполнения), то она подойдет, если вы попытаетесь смешать в pegasi или horse.

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

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

Becuzz
источник
Безмолвное исключение определенно плохо - мое предложение было проверкой, if(horse.IsUnicorn) horse.MeasureHorn();и исключения не будут обнаружены - они будут срабатывать, когда !horse.IsUnicornвы находитесь в контексте измерения единорога, или внутри MeasureHornне единорога. Таким образом, когда выдается исключение, вы не маскируете ошибки, оно полностью взрывается и является признаком того, что что-то нужно исправить. Очевидно, что это подходит только для определенных сценариев, но это реализация, которая не использует исключение для определения пути выполнения.
moarboilerplate
0

Похоже, ваш семантический домен имеет отношение IS-A, но вы немного опасаетесь использовать подтипы / наследование для моделирования этого - особенно из-за отражения типа времени выполнения. Однако я думаю, что вы боитесь не того, что подтипирование действительно сопряжено с опасностями, но тот факт, что вы запрашиваете объект во время выполнения, не является проблемой. Вы поймете, что я имею в виду.

Объектно-ориентированное программирование довольно сильно опирается на понятие отношений IS-A, возможно, оно слишком сильно опирается на него, что приводит к двум известным критическим концепциям:

Но я думаю, что есть еще один, более функционально-ориентированный способ взглянуть на отношения IS-A, который, возможно, не имеет таких трудностей. Во-первых, мы хотим смоделировать лошадей и единорогов в нашей программе, поэтому у нас будет тип Horseи Unicornтип. Каковы значения этих типов? Ну, я бы сказал это:

  1. Значения этих типов являются представлениями или описаниями лошадей и единорогов (соответственно);
  2. Это схематические представления или описания - они не в свободной форме, они построены в соответствии с очень строгими правилами.

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

  1. Существует общая функция, которая преобразует любое Circle(описание схемы круга) в Ellipse(описание другого типа), которое описывает те же круги;
  2. Существует частичная функция, которая принимает Ellipseи, если описывает круг, возвращает соответствующую Circle.

Итак, в терминах функционального программирования ваш Unicornтип вообще не должен быть подтипом Horse, вам просто нужны такие операции:

-- Convert any unicorn-description of into a horse-description that
-- describes the same unicorns.
toHorse :: Unicorn -> Horse

-- If the horse described by the given horse-description is a unicorn,
-- then return a unicorn-description of that unicorn, otherwise return
-- nothing.
toUnicorn :: Horse -> Maybe Unicorn

И toUnicornдолжен быть правым обратным к toHorse:

toUnicorn (toHorse x) = Just x

MaybeТип Haskell - это то, что другие языки называют типом «option». Например, Optional<Unicorn>тип Java 8 - это Unicornили ничего. Обратите внимание, что две из ваших альтернатив - выбрасывание исключения или возвращение «значения по умолчанию или магического значения» - очень похожи на типы опций.

Поэтому в основном то, что я здесь сделал, это реконструкция концепции IS-A с точки зрения типов и функций без использования подтипов или наследования. Что бы я от этого отнял:

  1. Ваша модель должна иметь Horseтип;
  2. В Horseпотребности типа для кодирования достаточно информации , чтобы однозначно определить , описывает ли какое - либо значение единорога;
  3. Некоторые операции этого Horseтипа должны предоставлять эту информацию, чтобы клиенты этого типа могли наблюдать, является ли данный Horseтекст единорогом;
  4. Клиенты этого Horseтипа должны будут использовать эти последние операции во время выполнения, чтобы различать единорогов и лошадей.

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

В объектно-ориентированном программировании знакомый способ сделать это заключается в следующем:

  • Иметь Horseтип;
  • Иметь Unicornв качестве подтипа Horse;
  • Используйте отражение типа среды выполнения в качестве доступной для клиента операции, которая определяет, является ли данный Horseобъект типом Unicorn.

Это имеет большую слабость, когда вы смотрите на это с позиции «вещь против описания», которую я представил выше:

  • Что если у вас есть Horseэкземпляр, который описывает единорога, но не является Unicornэкземпляром?

Возвращаясь к началу, это то, что я считаю действительно страшной частью использования подтипов и даункастов для моделирования этих отношений IS-A - не факт, что вы должны выполнять проверку во время выполнения. Немного злоупотребляя типографикой, спрашивать, является Horseли это Unicornэкземпляром, не является синонимом вопроса о Horseтом, является ли это единорогом (является ли это Horseописанием лошади, которая также является единорогом). Нет, если ваша программа не пошла на многое, чтобы инкапсулировать код, Horsesкоторый создает, так что каждый раз, когда клиент пытается создать объект Horse, описывающий единорога, создается Unicornэкземпляр класса. По моему опыту, программисты редко делают это осторожно.

Так что я бы пошел с подходом, где есть явная, не пониженная операция, которая преобразует Horses в Unicorns. Это может быть метод Horseтипа:

interface Horse {
    // ...
    Optional<Unicorn> toUnicorn();
}

... или это может быть внешний объект (ваш "отдельный объект на лошади, который говорит вам, является ли лошадь единорогом или нет"):

class HorseToUnicornCoercion {
    Optional<Unicorn> convert(Horse horse) {
       // ...
    }
}

Выбор между ними зависит от того, как организована ваша программа - в обоих случаях у вас есть эквивалент моей Horse -> Maybe Unicornоперации сверху, вы просто упаковываете ее по-разному (что, по общему признанию, будет иметь волновой эффект на то, какие операции Horseнужны типу) выставлять своим клиентам).

sacundim
источник
-1

Комментарий ОП в другом ответе прояснил вопрос, подумал я

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

Если сформулировать это так, я думаю, что нам нужно больше информации. Ответ, вероятно, зависит от ряда вещей:

  • Наши языковые возможности. Например, я бы, вероятно, подошел к этому по-разному в ruby, javascript и Java.
  • Сами понятия: что такое лошадь и что такое единорог? Какие данные связаны с каждым? Они точно такие же, кроме рога, или есть другие различия?
  • Как еще мы их используем, кроме средних значений длины рога? А как насчет стада? Может быть, мы тоже должны их смоделировать? Мы используем их в другом месте? herd.averageHornLength()кажется, соответствует нашей концептуальной модели.
  • Как создаются объекты лошади и единорога? Изменяется ли этот код в рамках нашего рефакторинга?

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

OP, дайте мне знать, если я все еще не понимаю ...

Ион
источник
1
Ярмарка очков. Чтобы проблема не стала еще более абстрактной, мы должны сделать несколько разумных допущений: 1) язык со строгой типизацией 2) стадо ограничивает лошадей одним типом, вероятно, из-за набора 3) методов, таких как типирование утки, вероятно, следует избегать , Что касается того, что можно изменить, то здесь не обязательно какие-либо ограничения, но каждый тип изменений имеет свои уникальные последствия ...
moarboilerplate
Если стадо ограничивает лошадей одним типом, это не единственное наше наследование выбора (не нравится этот вариант) или объект-оболочка (скажем HerdMember), который мы инициализируем либо лошадью, либо единорогом (освобождая лошадь и единорога от необходимости отношения подтипа) ). HerdMemberзатем может быть свободно реализовано, isUnicorn()однако, по своему усмотрению, и предложенное мной решение для фильтрации следует.
Иона
В некоторых языках hornLength () можно смешивать, и если это так, то это может быть правильным решением. Тем не менее, в языках, где типизация менее гибкая, вы должны прибегнуть к каким-то хакерским методам, чтобы сделать то же самое, или вы должны сделать что-то вроде надписи на лошади, что может привести к путанице в коде, потому что лошадь не ' Концептуально есть какие-либо рога. Кроме того, если вы выполняете математические вычисления, включая значения по умолчанию, это может исказить результаты (см. Комментарии под оригинальным вопросом)
moarboilerplate
Тем не менее, миксины, если они не выполняются, являются средой выполнения, просто наследуются под другим именем. Ваш комментарий «у лошади концептуально нет рогов» связан с моим комментарием о необходимости узнать больше о том, что они из себя представляют, если в нашем ответе нужно указать, как мы моделируем лошадей и единорогов и как они связаны друг с другом. Любое решение, которое включает в себя значения по умолчанию, вышло из-под контроля неправильно.
Иона
Вы правы в том, что для того, чтобы получить точное решение для конкретного проявления этой проблемы, вам нужно иметь много контекста. Чтобы ответить на ваш вопрос о лошади с рогом и привязать ее к миксинам, я подумал о сценарии, в котором hornLength, смешанный с лошадью, которая не является единорогом, является ошибкой. Рассмотрим черту Scala, которая имеет реализацию по умолчанию для hornLength, которая выдает исключение. Тип единорога может переопределить эту реализацию, и если лошадь когда-либо превращает это в контекст, где оценивается hornLength, это исключение.
moarboilerplate
-2

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

Мартин Маат
источник
Я согласен с этим. У Мэйсона Уилера также есть хорошее решение в его ответе, но если вам нужно выделить единорогов по разным причинам в разных местах, ваш код будет иметь много horses.ofType<Unicorn>...конструкций. Наличие GetUnicornsфункции было бы однострочным, но было бы еще более устойчивым к изменениям в отношениях лошадь / единорог с точки зрения вызывающего.
Шаз
@Ryan Если вы возвращаете IEnumerable<Horse>, хотя ваши критерии единорога находятся в одном месте, он инкапсулирован, поэтому ваши абоненты должны делать предположения о том, почему им нужны единороги (я могу получить похлебку из моллюсков, заказав суп дня сегодня, но это не так) Я имею в виду, я получу это завтра, сделав то же самое). Кроме того, вы должны выставить значение по умолчанию для клаксона на Horse. Если Unicornэто собственный тип, вы должны создать новый тип и поддерживать сопоставления типов, что может привести к дополнительным расходам.
moarboilerplate
1
@moarboilerplate: Мы считаем, что все это поддерживает решение. Прелесть в том, что он не зависит от деталей реализации единорога. Независимо от того, проводите ли вы дискриминацию в зависимости от члена данных, класса или времени дня (эти лошади могут превратиться в единорогов в полночь, если луна подходит всем, насколько я знаю), решение остается в силе, интерфейс остается прежним.
Мартин Маат