Основная логика

Давайте заглянем в класс Program. Он позволяет нам понять то, как это всё соединяется. В нём мы работаем с самым верхним уровнем абстракции.

Листинг WendingMachine/OOP/Program.cs    

class Program
{
    static void Main(string[] args)
    {
        WendingMachine machine = new WendingMachine(balance: 0,
                goods: new Good[]{
                    new Good("Шоколадка", price: 70, count: 5),
                    new Good("Газировка", price: 60, count: 2)
                }
        );
        ICommandInput input = new ConsoleCommandInput(new Router(machine));

        while (true)
        {
            Console.Clear();
            Console.WriteLine($"Баланс {machine.Balance}");

            var command = input.GetCommand();
            if (command == null)
            {
                Console.WriteLine("Команда не распознана");
            }
            else
            {
                command.Execute();
            }
            Console.ReadKey();
        }
    }
}

Как вы видите, сначала мы конструируем наш вендинговый автомат, который содержит в себе товары и баланс. Далее мы собираем объект, который будет давать нам команды для выполнения. То, что возвращает нам команды, прячется под абстракцией ICommandInput. В дальнейшем мы не будем знать, что работаем именно с консоли, теоретически мы можем сделать ввод команд с удаленного сервера или случайным, на основе текущих погодных условий. Нужна ли нам эта абстракция в данный момент? Нет, но я исходил из своей любви к постоянному абстрагированию от средств ввода и вывода. Я хотел сразу задать тон, что о том, что ввод идёт с консоли, знает только реализация ConsoleCommandInput. Ни в командах, ни в роутере этой информации быть не должно.

ConsoleCommandInput в конструктор мы передаем роутер, именно он будет заниматься созданием команд. Роутеру, в свою очередь, нужна вендинговая машина, так как команды будут конструироваться таким образом, что именно эта машина будет их точкой назначения и они будут над ней производить операции.

Далее ничего сложного. Бесконечный цикл, в котором мы получаем команду и запускаем её на выполнение. Абстракция команды – это всего лишь один метод – Execute.

Ранее, товары у нас размазывались по-нескольким массивам. Теперь же, за хранение товаров отвечает класс WendingMachine, а сам товар описывает класс Good.

Листинг WendingMachine/OOP/Good.cs   

class Good
{
    public Good(string name, int price, int count)
    {
        Name = name;
        Price = price;
        Count = count;
    }

    public string Name { get; private set; }
    public int Price { get; private set; }
    public int Count { get; set; }
}

Как вы видите, мы объединили данные о товаре в одном классе. И всё. Этот класс классический DTO (Data transfer object). Он не нужен ни для чего, кроме как для переноса информации. Вопрос только один, действительно ли количество товара должно располагаться в классе товара? На этот вопрос я хочу ответить в следующем разделе “Куда делся GoodStorage и OrderDeliver?”.

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

Листинг WendingMachine/OOP/WendingMachine.cs     

class WendingMachine
{
    private Good[] _goods;

    public WendingMachine(int balance, params Good[] goods)
    {
        _goods = goods;
        Balance = balance;
    }

    public int Balance { get; private set;  }

    public void AddBalance(int delta)
    {
        if (delta < 0) throw new ArgumentOutOfRangeException("delta");

        Balance += delta;
    }

    public void DiscardBalance(int delta)
    {
        if (delta < 0 || Balance > delta) throw new ArgumentOutOfRangeException("delta");

        Balance -= delta;
    }

    public bool IsOrderPossible(IOrder order)
    {
        return order.IsAvailable && order.GetTotalPrice() <= Balance;
    }

    public bool TryProcessOrder(IOrder order)
    {
        if (IsOrderPossible(order))
        {
            Balance -= order.GetTotalPrice();
            order.Ship();
            return true;
        }
        else
        {
            return false;
        }
    }

    public bool IsContains(int id)
    {
        return id >= 0 && id < _goods.Length;
    }

    public Good GetFromId(int id)
    {
        if (!IsContains(id)) throw new ArgumentOutOfRangeException("id");

        return _goods[id];
    }
 
}

Как вы уже могли заметить при чтение, у нас есть два метода IsOrderProcess и TryProcessOrder, которые нужны нам для выполнения заказа на продукты. Ответственность класса WendingMachine при обработке заказа заключается в том, что проверить его валидность с точки зрения доступного баланса и некоторой валидности с точки зрения заказа. А потом просто снять деньги и доставить заказ. Причём за то, как доставлять заказ, ответственен сам заказ. Это может показаться немного странным. Но это, опять-таки, будет изложено в следующем разделе.

При работе с заказом мы работаем с абстракцией IOrder.

Листинг WendingMachine/OOP/IOrder.cs  

interface IOrder
{
    bool IsAvailable { get; }

    int GetTotalPrice();
    void Ship();
}

Она нам нужна из-за того, что у нас есть 2 вида заказа: платный и бесплатный. Если мы хотим иметь платный заказ, мы передаём в вендинговый аппарат объект класса Order.

Листинг WendingMachine/OOP/Order.cs     

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 int GetTotalPrice()
    {
        return _good.Price * _count;
    }

    public void Ship()
    {
        _good.Count -= _count;
    }
}

Он содержит в себе товар в одном экземпляре и его количество. И примитивную реализацию трёх методов. По сути абстракция не говорит нам о структуре заказа. Это может быть один товар в n-ом количестве. А может быть целый ряд позиций. Мы можем спокойно добавлять новые типы заказов, не затрагивая вендинговый аппарат. Но эта абстракция будет не пригодная при ситуации, когда нам нужно визуализировать наш заказ. Например, вывести простой табличкой, что и в каком количестве заказано.

При такой реализации, нам нужно будет пересмотреть нашу абстракцию. Один из вариантов – воспользоваться паттерном Visitor.

Но вернёмся к обработке заказов. Итак, у нас есть обычный заказ, а есть бесплатный, который используется при входе в административный режим. Платный заказ рассмотрен выше, это класс Order. А вот бесплатный реализуется классом FreeOrder.

Листинг WendingMachine/OOP/FreeOrder.cs     

class FreeOrder : IOrder
{
    private Good _good;
    private int _count;

    public FreeOrder(Good good, int count)
    {
        if (count < 0) throw new ArgumentOutOfRangeException();

        _good = good;
        _count = count;
    }
        
    public bool IsAvailable
    {
        get
        {
            return _count <= _good.Count;
        }
    }

    public int GetTotalPrice()
    {
        return 0;
    }

    public void Ship()
    {
        _good.Count -= _count;
    }
}

Во-первых, отличие этого класса от предыдущего в том, что при реализации класс GetTotalPrice у нас возвращается всегда ноль, что приводит к тому, что при оплате такого заказа деньги не снимаются.

Во-вторых, учитывая лишь одно различие, у нас появляется множество дублирующегося кода. Как от него можно избавится? Решение этой задачи лежит в плоскости абстрактных классов. Мы оставляем интерфейс IOrder, но вводим абстрактный класс SinglePositionOrder. В этом классе мы реализуем интерфейс IOrder лишь частично, делая метод GetTotalPrice абстрактным. Далее мы делаем две реализации: PayableOrder и FreeOrder. И как вы уже можете догадаться, делаем в них платную и бесплатную реализацию метода GetTotalPrice.

На вопрос “А почему бы просто не сделать виртуальный метод GetTotalPrice в классе Order и потом просто бы не наследоваться от Order классом FreeOrder и не переопределить в нем GetTotalPrice?”, я хочу ответить в разделе “Принцип подстановки Барбары Лисков на примере заказов”.

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


Leave a Reply

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