суббота, 10 декабря 2011 г.

Продукты, цены и ценовая политика

Задача такова: Есть магазин с товарами. У товаров может быть несколько характеристик - цена, категория, скидка. Кроме этого товар подлежит определенной ценовой политике. Например, пользователь меняет цену продукта. Если цена ниже пороговой, то перевести продукт в категорию обычных. Если категория становится Обычный продукт, то убрать на продукт скидку. Должна быть возможность оперативно менять ценовую политику.

Определим интерфейс продукта:

  1. namespace Store
  2. {
  3.   public enum CategoryType
  4.   {
  5.     Children,
  6.     Grows
  7.   }
  8.  
  9.   public interface IProduct
  10.   {
  11.     int Price { get; set; }
  12.     CategoryType Category { get; set; }
  13.     int Discount { get; set; }
  14.   }
  15. }
* This source code was highlighted with Source Code Highlighter.
Так как к каждому товару должна применяться ценовая политика, пусть каждый товар будет знать ее. Ценовая политика будет применяться к товару и переустанавливать цену, категорию или скидку. Условимся, что в первом приближении это будет выглядеть так:

  1. namespace Store
  2. {
  3.   public interface IPricePolicy
  4.   {
  5.     void Apply(IProduct productBase);
  6.   }
  7.  
  8.   public interface IProduct
  9.   {
  10.     int Price { get; set; }
  11.     CategoryType Category { get; set; }
  12.     int Discount { get; set; }
  13.     IPricePolicy Policy { get; set; }
  14.   }
  15. }
* This source code was highlighted with Source Code Highlighter.
Сразу же встает вопрос, когда применять политику к товару? Если сделать это при вызове сеттеров характеристик, то можно впасть в глубокую рекурсию. Ведь политика в свою очередь тоже будет назначать новые значения цены или скидки. А там опять же сеттеры.
Давайте сделаем так, чтобы политика не применялась к товару на прямую, а рекомендовала новые значения характеристик. То есть, продукт должен иметь возможность передать политике свои характеристики и заменить их, как только применится эта политика.
Нужно, чтобы товар инкапсулировал какую-то сущность, содержащую в себе его характеристики. Пусть это будет IProductBase. Соответственно интерфейс товара тоже нужно исправить.

  1. namespace Store
  2. {
  3.   public interface IProductBase
  4.   {
  5.     int Price { get; set; }
  6.     CategoryType Category { get; set; }
  7.     int Discount { get; set; }
  8.   }
  9.  
  10.   public interface IProduct
  11.   {
  12.     IPricePolicy Policy { get; set; }
  13.   }
  14.  
  15.   public class Product : IProduct
  16.   {
  17.     public Product(IProductBase productBase, IPricePolicy policy)
  18.     {
  19.       if (productBase == null)
  20.         throw new ArgumentNullException("ProductBase must not be null");
  21.  
  22.       if (policy == null)
  23.         throw new ArgumentNullException("Policy must not be null");
  24.  
  25.       _policy = policy;
  26.       _productBase = productBase;
  27.     }
  28.  
  29.     #region IProduct Members
  30.  
  31.     public IPricePolicy Policy
  32.     {
  33.       get
  34.       {
  35.         return _policy;
  36.       }
  37.       set
  38.       {
  39.         if (value == null)
  40.           throw new ArgumentNullException("Policy must not be null");
  41.  
  42.         _policy = value;
  43.       }
  44.     }
  45.  
  46.     #endregion
  47.  
  48.     private IProductBase _productBase;
  49.     private IPricePolicy _policy;
  50.   }
  51. }
* This source code was highlighted with Source Code Highlighter.
Отлично, теперь мы может передавать ценовым политикам IProductBase.

  1. namespace Store
  2. {
  3.   public interface IPricePolicy
  4.   {
  5.     IProductBase Apply(IProductBase productBase);
  6.   }
  7. }
* This source code was highlighted with Source Code Highlighter.
Итак, в интерфейсе товара не оказалось нужных для внешнего использования характеристик. Представим товар в качестве декоратора для IProductBase. То есть товар расширяет возможности своего внутреннего состояния применением к нему (состоянию) политик.

  1. namespace Store
  2. {
  3.   public interface IProduct : IProductBase
  4.   {
  5.     IPricePolicy Policy { get; set; }
  6.   }
  7.  
  8.   public class Product : IProduct
  9.   {
  10.     public Product(IProductBase productBase, IPricePolicy policy)
  11.     {
  12.       if (productBase == null)
  13.         throw new ArgumentNullException("ProductBase must not be null");
  14.  
  15.       if (policy == null)
  16.         throw new ArgumentNullException("Policy must not be null");
  17.  
  18.       _policy = policy;
  19.       _productBase = productBase;
  20.     }
  21.  
  22.     #region IProduct Members
  23.  
  24.     public IPricePolicy Policy
  25.     {
  26.       get
  27.       {
  28.         return _policy;
  29.       }
  30.       set
  31.       {
  32.         if (value == null)
  33.           throw new ArgumentNullException("Policy must not be null");
  34.  
  35.         _policy = value;
  36.         ApplyPolicy();
  37.       }
  38.     }
  39.  
  40.     public int Price
  41.     {
  42.       get
  43.       {
  44.         return _productBase.Price;
  45.       }
  46.       set
  47.       {
  48.         if (_productBase.Price == value) return;
  49.         _productBase.Price = value;
  50.         ApplyPolicy();
  51.       }
  52.     }
  53.  
  54.     public CategoryType Category
  55.     {
  56.       get
  57.       {
  58.         return _productBase.Category;
  59.       }
  60.       set
  61.       {
  62.         if (_productBase.Category == value) return;
  63.         _productBase.Category = value;
  64.         ApplyPolicy();
  65.       }
  66.     }
  67.  
  68.     public int Discount
  69.     {
  70.       get
  71.       {
  72.         return _productBase.Discount;
  73.       }
  74.       set
  75.       {
  76.         if (_productBase.Discount == value) return;
  77.         _productBase.Discount = value;
  78.         ApplyPolicy();
  79.       }
  80.     }
  81.  
  82.     #endregion
  83.  
  84.     private void ApplyPolicy()
  85.     {
  86.       _productBase = _policy.Apply(_productBase);
  87.     }
  88.  
  89.     public override string ToString()
  90.     {
  91.       var sb =
  92.         new StringBuilder("Price = ").Append(_productBase.Price)
  93.             .Append("; Category = ").Append(_productBase.Category)
  94.             .Append("; Discount = ").Append(_productBase.Discount);
  95.       return sb.ToString();
  96.     }
  97.  
  98.     private IProductBase _productBase;
  99.     private IPricePolicy _policy;
  100.   }
  101. }
* This source code was highlighted with Source Code Highlighter.
Политика применяется к товару при изменении цены, категории или скидки, а также при смене политики.
Как можно было заметить IProductBase имеет публичные сеттеры для характеристик товара. закрыть доступ модификаторами private или protected к ним нельзя, так как товар не сможет модифицировать свой _productBase. Но и открытыми их оставлять нельзя, так как политика сможет безпрепятственно модифицировать то, что в нее передал товар. А это нарушение инкапсуляции Product. К сожалению, в C# нельзя поставить модификатор const при передачи параметра.
Выйти из положения можно, если товар будет передавать не свой экземпляр _productBase, а его копию. Введем в интерфейс IProductBase метод Clone.

  1. namespace Store
  2. {
  3.   public interface IProductBase
  4.   {
  5.     int Price { get; set; }
  6.     CategoryType Category { get; set; }
  7.     int Discount { get; set; }
  8.     IProductBase Clone();
  9.   }
  10. }
* This source code was highlighted with Source Code Highlighter.
Можно, конечно, просто наследовать IProductBase от IClonable, но я так не делаю. IClonable.Clone  возвращает тип object, а это не типобезопасно.
Теперь модифицируем класс Product в соответствии с последними изменениями.

  1. using System;
  2. using System.Text;
  3.  
  4. namespace Store
  5. {
  6.   public class Product: IProduct
  7.   {
  8.     ....
  9.  
  10.     private void ApplyPolicy()
  11.     {
  12.       _productBase = _policy.Apply(_productBase.Clone());
  13.     }
  14.  
  15.     public IProductBase Clone()
  16.     {
  17.       return _productBase.Clone();
  18.     }
  19.  
  20.     ....
  21.   }
  22. }
* This source code was highlighted with Source Code Highlighter.
Теперь несмотря на то, что класс политики сможет менять выдаваемый ему IProductBase как хочет, это не приведет к некорректному состоянию товара.
Итак, товар закрепляет за собой цену, скидку и категорию и дает возможность менять эти свойства извне. При смене, например, цены товар автоматически применяет установленную для него ценовую политику. В любой момент ценовую политику можно сменить.

6 комментариев:

  1. А зачем столько сложностей? Почему не вызывать в политику на событие записи свойств товара в базу?

    ОтветитьУдалить
  2. Возможно, продукт будет использоваться после изменения цены и до записи в базу.

    ОтветитьУдалить
  3. Да ну? Хоть один реалистичный сценарий такого использования? Да и что мешает в таком сценарии тоже вызывать нужный метод расчета?

    ОтветитьУдалить
  4. Все зависит от того в какой бизнес-транзакции будут использоваться объекты класса Product. Реалистичный сценарий, к сожалению, представить не могу - я не экономист. Не могу ни подтвердить ни опровергнуть такое использование, потому и написал - "Возможно".

    ОтветитьУдалить
  5. Так как я не одну торговую систему разработал, то могу с уверенностью сказать что свойства товара меняются почти всегда при записи в БД. Поэтому выполнение бизнес-правил "всегда" я бы не стал рассматривать как основной сценарий.

    ОтветитьУдалить
  6. Хоть я и не считаю такое решение правильным, рад, что оно у вас работает.

    ОтветитьУдалить