Должен ли я включить список сборов / скидок в класс заказа или они должны быть пунктами - PullRequest
6 голосов
/ 28 апреля 2009

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

Проще показать, чем описать, но приложение по сути напоминает приложение для торговых точек, состоящее из 3 основных частей: Предметы, Элементы заказа и Заказ.

Класс элемента - это данные, полученные из хранилища данных.

public class Item 
    : IComparable<OrderItem>, IEquatable<OrderItem>
{
    public Int32 ID { get; set; }
    public String Description { get; set; }
    public decimal Cost { get; set; }

    public Item(Int32 id, String description, decimal cost) 
    { 
        ID = id; 
        Description = description; 
        Cost = cost;
    }
    // Extraneous Detail Omitted
}

Класс позиции заказа - это строка позиции заказа.

public class OrderItem 
    : Item, IBillableItem, IComparable<OrderItem>, IEquatable<OrderItem>
{
    // IBillableItem members
    public Boolean IsTaxed { get; set; } 
    public decimal ExtendedCost { get { return Cost * Quantity; } } 

    public Int32 Quantity { get; set; }

    public OrderItem (Item i, Int32 quantity) 
        : base(i.ID, i.Description, i.Cost) 
    {
        Quantity = quantity;

        IsTaxed = false;
    }
    // Extraneous Detail Omitted
}

В настоящее время, когда вы добавляете сборы или скидки к заказу, это так просто:

Order order = new Order();
// Fee
order.Add(new OrderItem(new Item("Admin Fee", 20), 1));

// Discount
order.Add(new OrderItem(new Item("Today's Special", -5), 1));

Мне это нравится, имеет смысл, и базовый класс, который Order наследует от итераций по элементам в списке, рассчитывает соответствующие налоги и позволяет другим документам типа Order (которых их 2) наследовать от базы класс, который вычисляет все это, не повторяя ничего. Если в документе типа заказа нет скидок, это так же просто, как просто не добавлять - $ value OrderItem.

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

МОЯ МЫСЛЬ

Имеют 2 интерфейса: IHasFees, IHasDiscounts и имеют Order для их реализации; оба из которых будут иметь одного члена списка. Таким образом, я мог получить доступ только к пунктам продажи, только к платам и только к скидкам (и связать их с элементами управления, если это будет необходимо).

Что мне не нравится в этом: - Теперь у меня есть 3 различных метода добавления / удаления для класса (AddItem / AddFee / AddDiscount / Remove ...) - Я дублирую (трижды дублирую) функциональность, поскольку все они являются просто списками элементов одного типа, просто каждый список имеет разное значение.

Я на правильном пути? Я подозреваю, что это решаемая проблема для большинства людей (учитывая, что этот тип программного обеспечения очень распространен).

Ответы [ 4 ]

3 голосов
/ 28 апреля 2009

Я укажу вам замечание Роба Коннери на подкасте ALT.net, которое я слушал не так давно (я не сторонник ALT.net, но рассуждения казались обоснованными):

Что имеет смысл для «бизнес-пользователя» (если он у вас есть).

Как программист, вы захотите учитывать Item, Fee, Discount и т. Д., Потому что они имеют схожие атрибуты и поведение.

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

DRY не означает ограничение вашей модели, и вы должны помнить об этом при анализе поведения с помощью наследования или чего-либо подобного.

Конкретным примером, который использовался в этом случае, был пример корзины покупок. Естественная идея программиста состояла в том, чтобы использовать заказ в незапятнанном состоянии. И это имеет смысл, потому что они выглядят точно так же. За исключением того, что они не. Это не имеет смысла для клиента, потому что это две разные концепции, и это просто делает дизайн менее понятным.

Это вопрос практики, вкуса и мнения, поэтому не следует слепо следовать советам, размещенным на веб-сайте:)

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

Полагаю, причина в том, что за этими понятиями Предметы являются конкретными примерами инвентаризованных предметов, они влияют на объемы запасов, их можно перечислить и измерить количественно.

Сборы нет. Они не разделяют большинство атрибутов.

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

3 голосов
/ 28 апреля 2009

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

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

Я бы посмотрел на это так; отнесите комиссии и скидки к категории «Особые», а затем создайте интерфейс «ISpecial», от которого наследуются комиссии и скидки. Извлеките любую общую функциональность в интерфейс ISpecial (например, «Проверка»). Затем сделайте так, чтобы ваш Заказ реализовал интерфейс ISpecial (или любой другой).

Таким образом, вы можете определить конкретное поведение Fee.Validate () и поведение Discount.Validate и правильно работать с ним благодаря магии полиморфизма (foreach of m_specialCollection .validate them). Таким образом, вы также можете легко расширить интерфейс Special для всего, что может понадобиться (скажем, Taxes).

2 голосов
/ 28 апреля 2009

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

Учитывая то, что вы описываете, вот как я бы попытался реализовать это:

Создайте класс Order, который реализует общедоступные свойства для каждого однозначного элемента данных, который вы хотите отобразить для привязки данных: номер заказа, дата, клиент, общие сборы, общие скидки и т. Д. Похоже, что вы необходимо отобразить конкретные сборы / скидки в виде отдельных значений; если так, реализуйте общедоступные свойства для них.

Создайте абстрактный класс OrderItem, который реализует общедоступные свойства для каждого элемента данных, с которым вы хотите связать сетку, и для каждого элемента данных, с которым вы хотите отсортировать элементы. (Вы также можете сделать это интерфейсом IOrderItem; это действительно зависит от того, будут ли методы, общие для всех позиций заказа.)

Создание подклассов OrderItem (или классов, реализующих IOrderItem) для определенных типов позиций, которые могут появляться в заказе: ProductOrderItem, FeeOrderItem, DiscountOrderItem и т. Д.

В вашей реализации ProductItem реализуйте свойство типа Item - это будет выглядеть примерно так:

public class ProductItem : OrderItem
{
   public Item Item { get; set; }
   public string Description { get { return Item.Description; } }
   public int Quantity { get; set; }
   public decimal Amount { get { return Item.Price * Quantity; } }
}

Реализовать свойство типа IEnumerable<OrderItem> в Order для хранения всех позиций. Реализуйте метод AddItem для добавления OrderItems, например ::

public void AddItem(OrderItem item)
{
   _Items.Add(item); // note that backing field is a List<OrderItem>
}

, который вы можете назвать довольно просто:

Order o = new Order();
o.AddItem(new ProductOrderItem { Item = GetItem(1), Quantity = 2 });
o.AddItem(new FeeItem { Description = "Special Fee", Amount = 100 });
o.AddItem(new DiscountItem { DiscountAmount = .05 });

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

public decimal TotalFees
{
   get
   {
      return (from OrderItem item in Items
              where item is FeeItem
              select item.Amount).Sum();
   }
}

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

Обратите внимание, что вы также можете ограничить AddItem добавлением ProductItem s и использовать другие методы в Order для добавления других типов элементов. Например, если заказ может иметь только одну сумму скидки:

public void SetDiscountAmount(decimal discountAmount)
{
   DiscountOrderItem item = _Items
      .Where(x => x is DiscountOrderItem)
      .SingleOrDefault();
   if (item == null)
   {
       item = new DiscountOrderItem();
       _Items.Add(item);
   }
   item.DiscountAmount = discountAmount;
}

Вы бы использовали этот подход, если вы хотите отобразить сумму скидки в соответствующем месте в таблице позиций заказа, но также хотите, чтобы сумма скидки для заказа составляла одно значение. (Можно утверждать, что вы можете захотеть сделать DiscountAmount свойством Order, создать DiscountOrderItem в его установщике и получить DiscountOrderItem его Amount от Order.DiscountAmount. Я думаю, что оба подхода имеют свои плюсы и минусы.)

1 голос
/ 28 апреля 2009

Один из вариантов - добавить атрибут ItemType в OrderItem

.
enum ItemType
{
   Item,
   Fee,
   Discount
}

Теперь вы можете в своем классе заказа иметь:

public IList<OrderItem> Fees
{
   get
   {
      return _items.Find(i=>i.ItemType==ItemType.Fee);
   }
}

Теперь вы все еще можете хранить свой единый список и избегать дополнительных интерфейсов. Вы могли бы даже иметь метод, подобный IList GetItems (тип ItemType).

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

Скидки могут даже стать больше правил, если я закажу 10 пунктов, скидка 5%.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...