Принцип подстановки Барбары Лисков на примере заказов

Я напомню вам, что у нас есть дублирующийся код. Он находится в листинге класса “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.

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

Если вы нашли ошибку, пожалуйста выделите её и нажмите Ctrl+Enter.


Leave a Reply

Your email address will not be published. Required fields are marked *