Задача такова: Есть магазин с товарами. У товаров может быть несколько характеристик - цена, категория, скидка. Кроме этого товар подлежит определенной ценовой политике. Например, пользователь меняет цену продукта. Если цена ниже пороговой, то перевести продукт в категорию обычных. Если категория становится Обычный продукт, то убрать на продукт скидку. Должна быть возможность оперативно менять ценовую политику.
Определим интерфейс продукта:Так как к каждому товару должна применяться ценовая политика, пусть каждый товар будет знать ее. Ценовая политика будет применяться к товару и переустанавливать цену, категорию или скидку. Условимся, что в первом приближении это будет выглядеть так:
- namespace Store
- {
- public enum CategoryType
- {
- Children,
- Grows
- }
- public interface IProduct
- {
- int Price { get; set; }
- CategoryType Category { get; set; }
- int Discount { get; set; }
- }
- }
* This source code was highlighted with Source Code Highlighter.
Сразу же встает вопрос, когда применять политику к товару? Если сделать это при вызове сеттеров характеристик, то можно впасть в глубокую рекурсию. Ведь политика в свою очередь тоже будет назначать новые значения цены или скидки. А там опять же сеттеры.
- namespace Store
- {
- public interface IPricePolicy
- {
- void Apply(IProduct productBase);
- }
- public interface IProduct
- {
- int Price { get; set; }
- CategoryType Category { get; set; }
- int Discount { get; set; }
- IPricePolicy Policy { get; set; }
- }
- }
* This source code was highlighted with Source Code Highlighter.
Давайте сделаем так, чтобы политика не применялась к товару на прямую, а рекомендовала новые значения характеристик. То есть, продукт должен иметь возможность передать политике свои характеристики и заменить их, как только применится эта политика.
Нужно, чтобы товар инкапсулировал какую-то сущность, содержащую в себе его характеристики. Пусть это будет IProductBase. Соответственно интерфейс товара тоже нужно исправить.
Отлично, теперь мы может передавать ценовым политикам IProductBase.
- namespace Store
- {
- public interface IProductBase
- {
- int Price { get; set; }
- CategoryType Category { get; set; }
- int Discount { get; set; }
- }
- public interface IProduct
- {
- IPricePolicy Policy { get; set; }
- }
- public class Product : IProduct
- {
- public Product(IProductBase productBase, IPricePolicy policy)
- {
- if (productBase == null)
- throw new ArgumentNullException("ProductBase must not be null");
- if (policy == null)
- throw new ArgumentNullException("Policy must not be null");
- _policy = policy;
- _productBase = productBase;
- }
- #region IProduct Members
- public IPricePolicy Policy
- {
- get
- {
- return _policy;
- }
- set
- {
- if (value == null)
- throw new ArgumentNullException("Policy must not be null");
- _policy = value;
- }
- }
- #endregion
- private IProductBase _productBase;
- private IPricePolicy _policy;
- }
- }
* This source code was highlighted with Source Code Highlighter.
Итак, в интерфейсе товара не оказалось нужных для внешнего использования характеристик. Представим товар в качестве декоратора для IProductBase. То есть товар расширяет возможности своего внутреннего состояния применением к нему (состоянию) политик.
- namespace Store
- {
- public interface IPricePolicy
- {
- IProductBase Apply(IProductBase productBase);
- }
- }
* This source code was highlighted with Source Code Highlighter.
Политика применяется к товару при изменении цены, категории или скидки, а также при смене политики.
- namespace Store
- {
- public interface IProduct : IProductBase
- {
- IPricePolicy Policy { get; set; }
- }
- public class Product : IProduct
- {
- public Product(IProductBase productBase, IPricePolicy policy)
- {
- if (productBase == null)
- throw new ArgumentNullException("ProductBase must not be null");
- if (policy == null)
- throw new ArgumentNullException("Policy must not be null");
- _policy = policy;
- _productBase = productBase;
- }
- #region IProduct Members
- public IPricePolicy Policy
- {
- get
- {
- return _policy;
- }
- set
- {
- if (value == null)
- throw new ArgumentNullException("Policy must not be null");
- _policy = value;
- ApplyPolicy();
- }
- }
- public int Price
- {
- get
- {
- return _productBase.Price;
- }
- set
- {
- if (_productBase.Price == value) return;
- _productBase.Price = value;
- ApplyPolicy();
- }
- }
- public CategoryType Category
- {
- get
- {
- return _productBase.Category;
- }
- set
- {
- if (_productBase.Category == value) return;
- _productBase.Category = value;
- ApplyPolicy();
- }
- }
- public int Discount
- {
- get
- {
- return _productBase.Discount;
- }
- set
- {
- if (_productBase.Discount == value) return;
- _productBase.Discount = value;
- ApplyPolicy();
- }
- }
- #endregion
- private void ApplyPolicy()
- {
- _productBase = _policy.Apply(_productBase);
- }
- public override string ToString()
- {
- var sb =
- new StringBuilder("Price = ").Append(_productBase.Price)
- .Append("; Category = ").Append(_productBase.Category)
- .Append("; Discount = ").Append(_productBase.Discount);
- return sb.ToString();
- }
- private IProductBase _productBase;
- private IPricePolicy _policy;
- }
- }
* This source code was highlighted with Source Code Highlighter.
Как можно было заметить IProductBase имеет публичные сеттеры для характеристик товара. закрыть доступ модификаторами private или protected к ним нельзя, так как товар не сможет модифицировать свой _productBase. Но и открытыми их оставлять нельзя, так как политика сможет безпрепятственно модифицировать то, что в нее передал товар. А это нарушение инкапсуляции Product. К сожалению, в C# нельзя поставить модификатор const при передачи параметра.
Выйти из положения можно, если товар будет передавать не свой экземпляр _productBase, а его копию. Введем в интерфейс IProductBase метод Clone.
Можно, конечно, просто наследовать IProductBase от IClonable, но я так не делаю. IClonable.Clone возвращает тип object, а это не типобезопасно.
- namespace Store
- {
- public interface IProductBase
- {
- int Price { get; set; }
- CategoryType Category { get; set; }
- int Discount { get; set; }
- IProductBase Clone();
- }
- }
* This source code was highlighted with Source Code Highlighter.
Теперь модифицируем класс Product в соответствии с последними изменениями.
Теперь несмотря на то, что класс политики сможет менять выдаваемый ему IProductBase как хочет, это не приведет к некорректному состоянию товара.
- using System;
- using System.Text;
- namespace Store
- {
- public class Product: IProduct
- {
- ....
- private void ApplyPolicy()
- {
- _productBase = _policy.Apply(_productBase.Clone());
- }
- public IProductBase Clone()
- {
- return _productBase.Clone();
- }
- ....
- }
- }
* This source code was highlighted with Source Code Highlighter.
Итак, товар закрепляет за собой цену, скидку и категорию и дает возможность менять эти свойства извне. При смене, например, цены товар автоматически применяет установленную для него ценовую политику. В любой момент ценовую политику можно сменить.
А зачем столько сложностей? Почему не вызывать в политику на событие записи свойств товара в базу?
ОтветитьУдалитьВозможно, продукт будет использоваться после изменения цены и до записи в базу.
ОтветитьУдалитьДа ну? Хоть один реалистичный сценарий такого использования? Да и что мешает в таком сценарии тоже вызывать нужный метод расчета?
ОтветитьУдалитьВсе зависит от того в какой бизнес-транзакции будут использоваться объекты класса Product. Реалистичный сценарий, к сожалению, представить не могу - я не экономист. Не могу ни подтвердить ни опровергнуть такое использование, потому и написал - "Возможно".
ОтветитьУдалитьТак как я не одну торговую систему разработал, то могу с уверенностью сказать что свойства товара меняются почти всегда при записи в БД. Поэтому выполнение бизнес-правил "всегда" я бы не стал рассматривать как основной сценарий.
ОтветитьУдалитьХоть я и не считаю такое решение правильным, рад, что оно у вас работает.
ОтветитьУдалить