Чтобы было яснее
Страница 3. Код, поведение которого определяется данными, и явное наследование


Код, поведение которого определяется данными, и явное наследование

Последний пример, который я предлагаю вашему вниманию, будет еще более громоздким. Рассмотрим систему скидок на заказы, использующую различные правила для вычисления скидки. "Синее" правило предоставляет вам фиксированную скидку в 150 долларов, если вы покупаете товары, поставляемые определенной группой поставщиков, а также если общая сумма вашего заказа превышает определенное пороговое значение. "Красное" правило предоставляет вам скидку в 10% при поставке товара в определенные штаты США.

На рисунке 5 изображен явный код для этой системы скидок. У заказа есть ссылка на вспомогательный объект класса, отвечающего за вычисление скидки. У этого класса есть два подкласса - для "красного" и для "синего" правила. На рисунке 6 показан пример кода, в котором используется один, более общий класс для подсчета скидки. Поведение этого класса определяется данными, задаваемыми при создании заказа.

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

Рисунок 5. Явная логика вычисления скидок (язык C#)

public class Order ...
public Decimal BaseAmount;
public String Supplier;
public String DeliveryState;
public Discounter Discounter;
public virtual Decimal Discount {
get {
return Discounter.Value(this);
}
}
}
abstract public class Discounter {
abstract public Decimal Value (Order order);
}
public class BlueDiscounter :Discounter {
public readonly IList DiscountedSuppliers = new ArrayList();
public Decimal Threshold = 500m;
public void AddDiscountedSupplier(String arg){
DiscountedSuppliers.Add(arg);
}
public override Decimal Value (Order order){
return (DiscountApplies(order))? 150 :0;
}
private Boolean DiscountApplies(Order order){
return DiscountedSuppliers.Contains(order.Supplier)&&
(order.BaseAmount >Threshold);
}
}
public class RedDiscounter :Discounter {
public readonly IList DiscountedStates = new ArrayList();
public void AddDiscountedState (String arg){
DiscountedStates.Add(arg);
}
public override Decimal Value (Order order){
return (DiscountedStates.Contains(order.DeliveryState))?
order.BaseAmount *0.1 : 0;
}
}

// создаем "синий" заказ
BlueDiscounter bluePlan = new BlueDiscounter();
bluePlan.AddDiscountedSupplier(“ieee”);
blue = new Order();
blue.Discounter = bluePlan;
blue.BaseAmount = 500;
blue.Supplier = “ieee”;

Рисунок 6. Логика вычисления скидок, основанная на данных (язык C#)

public class GenericOrder :Order ...
public Discounter Discounter;
public override Decimal Discount {
get {
return Discounter.Value(this);
}
}

public enum DiscountType {constant,proportional};

public class Discounter ...
public DiscountType Type;
public IList DiscountedValues;
public String PropertyNameForInclude;
public String PropertyNameForCompare;
public Decimal CompareThreshold;
public Decimal Amount;

public Decimal Value(GenericOrder order){
if (ShouldApplyDiscount(order)){
if (Type == DiscountType.constant)
return Amount;
if (Type == DiscountType.proportional)
return Amount *order.BaseAmount;
throw new Exception (“Unreachable Code reached ”);
}else return 0;
}

private Boolean ShouldApplyDiscount(Order order){
return PassesContainTest(order)&&
PassesCompareTest(order);
}

private Boolean PassesContainTest(Order order){
return DiscountedValues.Contains
(GetPropertyValue(order,PropertyNameForInclude));
}

private Boolean PassesCompareTest(Order order){
if (PropertyNameForCompare == null)return true;
else {
Decimal compareValue =
(Decimal)GetPropertyValue(order,PropertyNameForCompare);
return compareValue > CompareThreshold;
}
}

private Object GetPropertyValue (Order order,String propertyName){
FieldInfo fi = typeof(Order).GetField(propertyName);
if (fi == null)
throw new Exception(“unable to find field for “+propertyName);
return fi.GetValue(order);
}
}

// создаем "синий" заказ
GenericDiscounter blueDiscounter = new GenericDiscounter();
String []suppliers = {“ieee”};
blueDiscounter.DiscountedValues = suppliers;
blueDiscounter.PropertyNameForInclude = “Supplier”;
blueDiscounter.Amount = 150;
blueDiscounter.PropertyNameForCompare = “BaseAmount”;
blueDiscounter.CompareThreshold = 500m;
blueDiscounter.Type = DiscountType.constant;
blue = new Order();
blue.BaseAmount = 500;
blue.Discounter = blueDiscounter;

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

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

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

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

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

 
« Предыдущая статья   Следующая статья »