Сокрыть структуру данных и сделать программу понятней

Сокрыв структуру данных я сделаю программу понятнее и безопаснее в обращении. Что я имею виду под структурой данных? Смотрите, у нас есть товар и хранилище товаров. Всё это организовано как ряд массивов, которые связанны друг с другом. Каждый массив хранит в себе данные определённого свойства товара. Массив обслуживает все товары.

Например, массив цен хранит цены всех товаров. А массив имён – имена. Ячейки под одним индексом в этих массивах относятся к одному товару. Так, например, вторая ячейка массива цен хранит цену второго товара. А вторая ячейка, массива имен, хранит имя второго товара.

namespricesavailableQuantity
0Шоколадка705
1Газировка602


Работаем мы с такой структурой следующим образом. В этом примере я вывел всю информацию о первом товаре.

Фрагмент 2.23

Console.WriteLine($"Имя - {names[0]}");
Console.WriteLine($"Цена - {prices[0]}");
Console.WriteLine($"Остаток - {availableQuantity[0]}");

Что нам нужно сделать с этим? Гораздо лучше здесь будут смотреться следующие функции: GetName, GetPrice, GetAvailableQuantity которым мы будем передавать Id товара в ответ будет получать нужное нам свойство.

Эти функции будут работать с глобальными переменными, которые будут представлены статическими полями класса Program.

Фрагмент 2.24   

class Program
{
    private static int balance = 0;
    private static int[] coinsQuantity = { 0, 0, 0, 0 }; //1, 2, 5, 10
    private static int[] coinsValues = { 1, 2, 5, 10 };
    private static string[] names = { "Шоколадка", "Газировка" };
    private static int[] prices = { 70, 60 };
    private static int[] availableQuantity = { 5, 2 };
    private static PaymentType payment = PaymentType.Card;

    static void Main(string[] args)
    {
...

Я поднял все переменные из начала метода Main в поля класса Program. Это не совсем правильно делать. Дело в том, что есть концепция чистых функций, она очень полезная. Суть её в том, что поведение функции должно быть детерминировано и не иметь побочных эффектов. Также при одних и тех же входных значения, она должна возвращать одни и те же значения.  Имеется ввиду, что она возвращает не то же самое, что ей прислали, а то что выходное значение строго привязано ко входному.

Здесь же мы собираемся сделать метод (функцию) которая будет работать с глобальными значениями. Это означает, что если я ей передам условный 0, то она может вернуть 1 а может 2. Это зависит от глобальных значений.

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

И так, у нас есть структура данных и нам нужно сделать к ней фасад для комфортной работы. Представлен он будет следующим набором функций.

Фрагмент 2.25

private static string GetName(int id)
{
    if(id < 0 || id > names.Length) throw new ArgumentOutOfRangeException("id");

    return names[id];
}

private static int GetPrice(int id)
{
    if(id < 0 || id > names.Length) throw new ArgumentOutOfRangeException("id");

    return prices[id];
}

private static int GetAvailableQuantity(int id)
{
    if(id < 0 || id > names.Length) throw new ArgumentOutOfRangeException("id");

    return availableQuantity[id];
}

private static bool IsAvailableInQuantity(int id, int count)
{
    return count < 0 || count > GetAvailableQuantity(id);
}

Ничего не заметили? Правильно, дублирующийся код. Попробуй исправить этот момент самостоятельно, не подлядывая на следующий исправленный фрагмент.

Фрагмент 2.26

private static bool Exist(int id)
{
    return id > 0 && id < names.Length;
}

private static void ValidateId(int id)
{
    if (!Exist(id))
    {
        throw new ArgumentOutOfRangeException("id");
    }
}

private static string GetName(int id)
{
    ValidateId(id);
    return names[id];
}

private static int GetPrice(int id)
{
    ValidateId(id);
    return prices[id];
}

private static int GetAvailableQuantity(int id)
{
    ValidateId(id);
    return availableQuantity[id];
}

У нас добавился метод Exist который позволяет проверить существует ли определённый товар. И вспомогательный метод ValidateId который выбрасывает исключение если товара нет. Он нужен в методах которые возвращают свойство товара. В этих методах также можно избавиться от постоянного вызова ValidateId, но вызовы уйдут сами как только мы перейдем к использованию классов и объектов. Но в самом конце этой главы мы попадем в сходную ситуацию, и поборим сложность с помощью аспектно ориентированного программирования.

Весь сок проделанных действий будет виден когда мы начнём получившимися методами пользоваться. Давайте посмотрим на команду BuyGood в текущем состояние.

Фрагмент 2.27

else if (command.StartsWith("BuyGood"))
{
    //Разбиение строки на единицы данных
    string[] rawData = command.Substring("BuyGood ".Length).Split(' ');

    //Сопоставление этих данных с переменными (и их типами) 
    if (rawData.Length != 2)
    {
        Console.WriteLine("Неправильные аргументы команды");
        break;
    }

    int id = 0;
    if(!MapParameter(rawData, out id, BuyGoodParameter.Id))
    {
        break;
    }

    int count = 0;
    if (!MapParameter(rawData, out count, BuyGoodParameter.Count))
    {
        break;
    }

    //Проверка корректности этих данных на основе текущего состояния модели.
    if (id < 0 || id >= names.Length)
    {
        Console.WriteLine("Такого товара нет");
        break;
    }

    if (count < 0 || count > availableQuantity[id])
    {
        Console.WriteLine("Нет такого количества");
        break;
    }

    //Выполнение
    int totalPrice = GetTotalPrice(prices[id], count);

    if (balance >= totalPrice)
    {
        balance -= totalPrice;
        availableQuantity[id] -= count;
    }
}

Пора препарировать этот код. Воспользовавшись нашими методами мы получим следующее.

Фрагмент 2.28

else if (command.StartsWith("BuyGood"))
{
    //Разбиение строки на единицы данных
    string[] rawData = command.Substring("BuyGood ".Length).Split(' ');

    //Сопоставление этих данных с переменными (и их типами) 
    if (rawData.Length != 2)
    {
        Console.WriteLine("Неправильные аргументы команды");
        break;
    }

    int id = 0;
    if(!MapParameter(rawData, out id, BuyGoodParameter.Id))
    {
        break;
    }

    int count = 0;
    if (!MapParameter(rawData, out count, BuyGoodParameter.Count))
    {
        break;
    }

    //Проверка корректности этих данных на основе текущего состояния модели.
    if (Exist(id))
    {
        Console.WriteLine("Такого товара нет");
        break;
    }

    if (IsAvailableInQuantity(id, count))
    {
        Console.WriteLine("Нет такого количества");
        break;
    }

    //Выполнение
    int totalPrice = GetTotalPrice(GetPrice(id), count);

    if (balance >= totalPrice)
    {
        balance -= totalPrice;
        availableQuantity[id] -= count;
    }
}

Вам может показаться, что поменялось мало что. Но на самом деле мы проделали важные изменения, которые  станут более очевидными после перехода на объектно ориентированный дизайн.

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

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

Давайте разобьем основной метод Main чтобы можно было быстро окинуть его взглядом и понять что происходит.

Я считаю, его можно разбить на следующие методы:

Фрагмент 2.29

private static string ReadCommand()
{
    Console.WriteLine("Введите команду:");
    return Console.ReadLine();
}

private static void ExecuteCommand(string command)
{
    if (command == "AddMoney")
    {
        ...
    }
    else if (command == "GetChange")
    {
        ...
    }
    else if (command.StartsWith("BuyGood"))
    {
        ...
    }
    else
    {
        Console.WriteLine("Команда не определена");
    }

}

В результате метод Main превратится в следующее:

Фрагмент 2.30  

static void Main(string[] args)
{
    Console.WriteLine($"Имя - {names[0]}");
    Console.WriteLine($"Цена - {prices[0]}");
    Console.WriteLine($"Остаток - {availableQuantity[0]}");

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

        string command = ReadCommand();
        ExecuteCommand(command);

        Console.ReadKey();
    }
}

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

Но основываясь на своём опыте, я бы предпочёл работать с итоговым вариантом.

Итог.

WendingMachine/FunctionalComposition/Program.cs

using System;

namespace FunctionalComposition
{
    class Program
    {
        private static int balance = 0;
        private static int[] coinsQuantity = { 0, 0, 0, 0 }; //1, 2, 5, 10
        private static int[] coinsValues = { 1, 2, 5, 10 };
        private static string[] names = { "Шоколадка", "Газировка" };
        private static int[] prices = { 70, 60 };
        private static int[] availableQuantity = { 5, 2 };
        private static PaymentType payment = PaymentType.Card;

        static void Main(string[] args)
        {
            while (true)
            {
                Console.Clear();
                Console.WriteLine($"Баланс {balance}");

                string command = ReadCommand();
                ExecuteCommand(command);

                Console.ReadKey();
            }
        }

        private static string ReadCommand()
        {
            Console.WriteLine("Введите команду:");
            return Console.ReadLine();
        }

        private static void ExecuteCommand(string command)
        {
            if (command == "AddMoney")
            {
                switch (payment)
                {
                    case PaymentType.Coins:
                        for (int i = 0; i < coinsValues.Length; i++)
                        {
                            Console.WriteLine($"Сколько монет номиналом {coinsValues[i]} вы хотите внести?");
                            int count = ReadInt();
                            coinsQuantity[i] += count;
                            balance += count * coinsValues[i];
                        }
                        break;
                    case PaymentType.Card:
                        Console.WriteLine("Сколько снять с вашей карты?");
                        int balanceDelta = ReadInt();
                        balance += balanceDelta;
                        Console.WriteLine("Баланс успешно пополнен");
                        break;
                    default:
                        break;
                }
            }
            else if (command == "GetChange")
            {
                balance = 0;
            }
            else if (command.StartsWith("BuyGood"))
            {
                //Разбиение строки на единицы данных
                string[] rawData = command.Substring("BuyGood ".Length).Split(' ');

                //Сопоставление этих данных с переменными (и их типами) 
                if (rawData.Length != 2)
                {
                    Console.WriteLine("Неправильные аргументы команды");
                    return;
                }

                int id = 0;
                if (!MapParameter(rawData, out id, BuyGoodParameter.Id))
                {
                    return;
                }

                int count = 0;
                if (!MapParameter(rawData, out count, BuyGoodParameter.Count))
                {
                    return;
                }

                //Проверка корректности этих данных на основе текущего состояния модели.
                if (Exist(id))
                {
                    Console.WriteLine("Такого товара нет");
                    return;
                }

                if (IsAvailableInQuantity(id, count))
                {
                    Console.WriteLine("Нет такого количества");
                    return;
                }

                //Выполнение
                int totalPrice = GetTotalPrice(GetPrice(id), count);

                if (balance >= totalPrice)
                {
                    balance -= totalPrice;
                    availableQuantity[id] -= count;
                }
            }
            else
            {
                Console.WriteLine("Команда не определена");
            }

        }

        private static bool Exist(int id)
        {
            return id > 0 && id < names.Length;
        }

        private static void ValidateId(int id)
        {
            if (!Exist(id))
            {
                throw new ArgumentOutOfRangeException("id");
            }
        }

        private static string GetName(int id)
        {
            ValidateId(id);
            return names[id];
        }

        private static int GetPrice(int id)
        {
            ValidateId(id);
            return prices[id];
        }

        private static int GetAvailableQuantity(int id)
        {
            ValidateId(id);
            return availableQuantity[id];
        }

        private static bool IsAvailableInQuantity(int id, int count)
        {
            return count < 0 || count > GetAvailableQuantity(id);
        }

        private static int GetTotalPrice(int price, int count)
        {
            return price * count;
        }

        private static int ReadInt()
        {
            int result = 0;

            while (!int.TryParse(Console.ReadLine(),
                        out result))
            {
                Console.WriteLine("Вы ввели не число!");
            }

            return result;
        }

        private static bool MapParameter(string[] rawParams, out int containter, BuyGoodParameter parameter)
        {
            int index = (int)parameter;
            string name = Enum.GetName(typeof(BuyGoodParameter), parameter);

            if (!int.TryParse(rawParams[index], out containter))
            {
                Console.WriteLine($"Ошибка в параметре {name}, он должен быть числом");
                return false;
            }

            return true;
        }

    }

    enum BuyGoodParameter
    {
        Id = 0, 
        Count = 1
    }

    enum PaymentType
    {
        Coins,
        Card
    }
}

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


Leave a Reply

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