Сокрыв структуру данных я сделаю программу понятнее и безопаснее в обращении. Что я имею виду под структурой данных? Смотрите, у нас есть товар и хранилище товаров. Всё это организовано как ряд массивов, которые связанны друг с другом. Каждый массив хранит в себе данные определённого свойства товара. Массив обслуживает все товары.
Например, массив цен хранит цены всех товаров. А массив имён – имена. Ячейки под одним индексом в этих массивах относятся к одному товару. Так, например, вторая ячейка массива цен хранит цену второго товара. А вторая ячейка, массива имен, хранит имя второго товара.
names | prices | availableQuantity | |
0 | Шоколадка | 70 | 5 |
1 | Газировка | 60 | 2 |
Работаем мы с такой структурой следующим образом. В этом примере я вывел всю информацию о первом товаре.
Фрагмент 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
}
}