ООП. Выбор объектов

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

Предполагая, что я создаю объект Drink, который имеет такие атрибуты, как volume и temperature, а также методы, подобные pour() и drink(), я изо всех сил пытаюсь понять, где именно появляются типы напитков.

Скажем, что у меня есть типы напитков Tea, Coffee или Juice, мой первый инстинкт относится к подклассу Drink, поскольку они имеют общие атрибуты и методы.

Затем проблема становится как Tea, так и Coffee иметь такие атрибуты, как sugars и milk, но Juice нет, тогда как все три имеют variant (Earl Grey, decaff и orange соответственно).

Аналогично, Tea и Coffee имеют метод addSugar(), тогда как это не имеет смысла для объекта Juice.

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

Но затем я получаю два метода addSugar() в подклассах Tea и Coffee.

Или, учитывая, что я в конечном итоге помещаю все атрибуты и методы в суперкласс, так как большинство разделяет по крайней мере пару типов напитков, интересно, какой смысл в суб-классификации вообще?

Я боюсь, что просто пытаюсь отвлечься слишком много, но не хочу возвращаться в угол, если я хочу добавить новый тип, например Water -with variant еще или сверкающий вниз дорога.

Ответы

Ответ 1

Частью проблемы является то, что объекты реального мира не аккуратно расположены в иерархии. У каждого объекта много разных родителей. Стакан сока, безусловно, напиток, но он также является источником питания, но не каждый напиток. Там не много пищи в стакане воды. Кроме того, существует множество источников питания, которые не являются напитками. Но большинство языков позволит вам наследовать только один класс. Чашка чая технически является источником энергии (она горячая, она содержит тепловую энергию), но также и батарея. Есть ли у них общий базовый класс?

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

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

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

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

В конечном счете, помните, что ваша цель - написать действующую программу и не написать идеальную диаграмму класса ООП. Вопрос не в том, "как я могу представлять разные типы напитков в ООП", но "как я могу включить мою программу для обработки разных видов напитков". Ответ может заключаться в том, чтобы организовать их в иерархии классов с общим базовым классом Drink. Но это может быть и другое. Это может быть "относиться ко всем напиткам одинаково и просто переключать имя", и в этом случае одного класса достаточно. Или это может относиться к напиткам как к другому виду потребляемого или просто к другому виду жидкости, без фактического моделирования их "питьевой" собственности.

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

Никогда не теряйте из виду свою цель. Зачем вам нужно моделировать напитки в вашей программе?

Ответ 2

Объектно-ориентированный дизайн не выполняется изолированно. Вопрос, который вы задаете, - это то, что нужно для моего конкретного приложения (если есть) с классом Drink? Ответ и, следовательно, дизайн, будут отличаться от приложения к приложению. Путь № 1 к неудаче в ОО состоит в том, чтобы попытаться построить совершенные платоновские иерархии классов - такие вещи существуют только в совершенных платоновых мирах и не полезны в реальном мире, в котором мы живем.

Ответ 3

Существует два решения проблемы sugart. Первый заключается в добавлении подкласса, такого как HotDrinks, в котором содержится addSugar и addMilk, например:

                 Drink
                  /  \
          HotDrink    Juice
           /   \
        Tea     Coffee

Проблема в том, что это становится беспорядочным, если их много.

Другим решением является добавление интерфейсов. Каждый класс может реализовать ноль или больше интерфейсов. Таким образом, у вас есть интерфейсы ISweetened и ICowPowerEnabled. Где и чай, и кофе реализуют интерфейс ISweetened и ICowPowerEnabled.

Ответ 4

Чтобы добавить к другим ответам, интерфейсы, как правило, дают вам большую гибкость при развязывании кода.

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

Например, если вы сегодня "добавляете сахар" в напиток, вы можете позже добавить его в блины. И тогда вы можете закончить свой код, попросив напиток во многих местах, используя AddSugar и различные другие несвязанные методы - это может быть сложно реорганизовать.

Если ваш класс знает, как что-то сделать, тогда он может реализовать интерфейс, независимо от того, что класс на самом деле. Например:

interface IAcceptSugar
{
    void AddSugar();
}

public class Tea : IAcceptSugar { ... }
public class Coffee : IAcceptSugar { ... }
public class Pancake : IAcceptSugar { ... }
public class SugarAddict : IAcceptSugar { ... }

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

public void Sweeten(List<IAcceptSugar> items)
{
   if (this.MustGetRidOfAllThisSugar)
   {
       // I want to get rid of my sugar, and
       // frankly, I don't care what you guys
       // do with it

       foreach(IAcceptSugar item in items)
          item.AddSugar();
   }
}

Ваш код также станет легче тестировать. Чем проще интерфейс, тем проще вы будете тестировать потребителей этого интерфейса.

[Изменить]

Возможно, я был не совсем ясен, поэтому я уточню: если AddSugar() делает то же самое для группы производных объектов, вы, конечно, реализуете его в своем базовом классе. Но в этом примере мы сказали, что Drink не всегда должен реализовывать IAcceptSugar.

Итак, у нас может получиться общий класс:

 public class DrinkWithSugar : Drink, IAcceptSugar { ... }

который будет реализовывать AddSugar() одинаково для кучи напитков.

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

Мораль такова: ваша иерархия может измениться, если ваши интерфейсы остаются неизменными.

Ответ 5

Этот pdf файл может вам помочь, и он идеально подходит вашему примеру:

Образец декоратора

Ответ 6

Две вещи, которые могут иметь значение:

1) Нил Баттерворт поднимает очень важное различие между Моделью Реального Мира в некотором контексте против самого Реального Мира. Объектно-ориентированный дизайн иногда также называется объектно-ориентированным моделированием. A model касается только "интересных" аспектов реального мира. Что интересно, зависит от контекста вашего приложения.

2) Наслаждайтесь композицией над наследованием. У напитка есть свойства объема и температуры (которые в вашем контексте моделирования могут быть относительно человеческого восприятия, то есть чрезмерно холодные, холодные, теплые, горячие) и состоят из ингредиентов (вода, чай, кофе, молоко, сахар, ароматизаторы). Обратите внимание, что он не наследуется от ингредиентов, из которых они сделаны. Что касается хороших способов составления ингредиентов в напитках, попробуйте посмотреть рисунок декоратора.

Ответ 7

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

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

Ответ 8

Используйте шаблон Decorator.

Ответ 9

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

Другая ошибка заключается в чрезмерном использовании наследования. Хотя может показаться хорошей идеей начать разработку красивой иерархии наследования, лучше подождать, пока возникнет полиморфная проблема, а затем создаст наследование.

Если у вас возникла проблема создания классов для 4 разных типов напитков, сделайте 4 разных типа без наследования. Если вам задали проблему, которая гласит: "Хорошо, теперь мы хотим передать любой из этих напитков Лицу и распечатать то, что они пили", это идеальное использование полиморфизма. Добавьте интерфейс или абстрактный базовый класс с помощью функции drink(). Push compile, обратите внимание, что все 5 классов говорят, что функция не реализована, реализовать все 5 функций, нажимать компиляцию, и все готово.

Чтобы ответить на ваш вопрос на вопрос о добавлении молока или сахара, проблема, которую я предполагаю, что вы работаете, заключается в том, что вы хотите передать объект Drinkable в Person, но Drinkable не имеет addMilk или addSugar. Эта проблема может быть решена двумя или более способами, один очень плохой способ сделать это.

Плохой путь: передайте Drinkable в Person и выполните проверку типа и наложите объект на объект Tea или Juice.

void prepare (Drinkable drink)
{
  if (drink is Juice)
  {
    Juice juice_drink = (Juick) drink;
    juice_drink.thing();
  }
}

Один из способов: один из способов - пропустить напиток до Person через конкретные классы, а затем передать его пить.

class Person
{
  void prepare_juice (Juice drink)
  {
    drink.thing1();
  }
  void prepare_coffee (Coffee drink)
  {
    drink.addSugar ();
    drink.addCream ();
  }
}

После того, как вы попросили человека приготовить напиток, попросите их выпить его.

Ответ 10

Все зависит от обстоятельств - и это на самом деле означает - начните с простейшего дизайна и реорганизуйте его, когда увидите необходимость. Может быть, это не будет выглядеть очень блестящую ООП - но я хотел бы начать с помощью только одного класса и собственности "CanAddSugar" и "SugarAmount" (позже вы могли бы обобщать его предикативный CanAddIngredient (IngredientName) так, чтобы люди могли добавить виски в свои напитки: )

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