Я напомню вам, что у нас есть дублирующийся код. Он находится в листинге класса “FreeOrder” и “Order”. По сути оба класса являются копиями друг друга с небольшими изменениями.
Это интересная ситуация, для которой я предложил следующее решение:
“Решение этой задачи лежит в плоскости абстрактных классов. Мы оставляем интерфейс IOrder, но вводим абстрактный класс SinglePositionOrder. В этом классе мы реализуем интерфейс IOrder лишь частично, делая метод GetTotalPrice абстрактным. Далее мы делаем две реализации: PayableOrder и FreeOrder. И как вы уже можете догадаться, делаем в них платную и бесплатную реализацию метода GetTotalPrice.”
Я чуть ранее
Прочитав это вы могли задаться вопросом: “А почему бы просто не сделать виртуальный метод GetTotalPrice в классе Order, а потом просто бы не унаследоваться от Order классом FreeOrder и не переопределить в нём GetTotalPrice?”.
Я пообещал дать ответ в этом разделе. И настало время ответить за свои слова. Основание моего решения находятся в одном из принципов “S.O.L.I.D.” А именно в принципе LSP – это принцип подстановки Барбары Лисков.
Его можно определить несколькими способами. Но в этот раз я хочу воспользоваться более эмпирическим описанием: “Если есть некий тип A и производный тип B, то мы должны иметь возможность использовать B вместо типа A, при этом поведение программы не должно меняться”. Грубо говоря, тест, который проходит тип A, должен проходить и тип B.
Этот принцип, как и все остальные, не являются обязательными для соблюдения. Даже больше, слепое и фанатичное следование им может привести вас к более худшим результатам, чем вообще отказ от них.
В данной ситуации мы решили последовать ему. И для этого нам нужно будет сделать два решения, и пустить их через призму этого принципа. Взглянем на решение на основе виртуальных методов и переопределения поведения типа Order.
Решение 1
class Order : IOrder
{
private Good _good;
private int _count;
public Order(Good good, int count)
{
if (count < 0) throw new ArgumentOutOfRangeException();
_good = good;
_count = count;
}
public bool IsAvailable {
get
{
return _count <= _good.Count;
}
}
public virtual int GetTotalPrice()
{
return _good.Price * _count;
}
public void Ship()
{
_good.Count -= _count;
}
}
class FreeOrder : Order
{
public FreeOrder(Good good, int count) : base(good, count)
{
}
public override int GetTotalPrice()
{
return 0;
}
}
Выглядит лаконично! Но это простота может быть обманчивой. Как вы заметили, я не хочу тут ничего чётко утверждать. Такое решение действительно простое и в некоторых ситуациях подходящее. Но мы должны с вами понимать, что FreeOrder – это костыль, который мы подсовываем системе и она начинает работать так, как мы хотим. Это не такой явный костыль, как если бы, вместо команды добавления денег, использовали команду покупки с типом, который имеет отрицательную цену. И всё же мы ситуативно и несогласованно изменяем поведение системы.
Более корректное решение будет проведено через абстрактный класс, который содержит такую спецификацию, что заказ имеет один товар определенного количества. Но в нём нет определения платный ли это товар или бесплатный.
Решение 2
abstract class Order : IOrder
{
protected readonly Good Good;
protected readonly int Count;
public Order(Good good, int count)
{
if (count < 0) throw new ArgumentOutOfRangeException();
Good = good;
Count = count;
}
public bool IsAvailable {
get
{
return Count<= Good.Count;
}
}
public abstract int GetTotalPrice();
public void Ship()
{
Good.Count -= Count;
}
}
class PayableOrder : Order
{
public PayableOrder(Good good, int count) : base(good, count)
{
}
public override int GetTotalPrice()
{
return Good.Price * Count;
}
}
class FreeOrder : Order
{
public FreeOrder(Good good, int count) : base(good, count)
{
}
public override int GetTotalPrice()
{
return 0;
}
}
Разница в том, что мы не можем ставить бесплатный заказ там, где ожидается платный, так же как и обратное. Так что система будет работать корректно и нам сложней её сломать.
Вопрос с тестами тоже закрывается просто – мы не можем тестировать абстрактный класс без создания конкретной реализации. Мы не имеем конкретного поведения в точке расчета цены, поэтому мы не можем как-то контролировать результат.
Технически мы можем попробовать тестами описать то, что число должно быть всегда больше или равно нулю. Но в таком тесте уже не описать, что при создание заказа с двумя шоколадками, его цена должна быть 60. Именно то, что метод абстрактный, говорит о том, что стратегия формирования цены спрятана от нас. Если в некой подсистеме нам достаточно только того, что число положительное, то мы работаем именно с типом Order. Если же мы хотим иметь товар, цена которого имеет прямую зависимость только от того, какой товар и в каком количестве мы добавили, мы используем тип PayableOrder.
Таким разделением типов мы позволяем нам точней декларировать, например, функциональные члены. Точно задавая то, что мы хотим без догадок и предположений, которые ни на чём не основываются.