Мы имеем основное ядро и дополнительную логику связи всего этого с пользовательским интерфейсом. У нас есть корневые абстракции ICommandInput и ICommand.
Листинг WendingMachine/OOP/ICommandInput.cs
interface ICommandInput
{
ICommand GetCommand();
}
Листинг WendingMachine/OOP/ICommand.cs
interface ICommand
{
void Execute();
}
Эти интерфейсы невероятно просты. По сути, первый позволяет нам абстрагироваться от того как и откуда приходят команды, а второй от реализации команды, которая пришла.
Теперь нам интересна реализация консольного ввода через класс ConsoleCommandInput.
Листинг WendingMachine/OOP/ConsoleCommandInput.cs
class ConsoleCommandInput : ICommandInput
{
private Router _router;
public ConsoleCommandInput(Router router)
{
_router = router;
}
public ICommand GetCommand()
{
string rawCommand = Console.ReadLine();
Request request = ParseString(rawCommand);
return _router.CreateCommand(request);
}
private Request ParseString(string input)
{
string[] terms = input.Split(' ');
int[] values = new int[0];
if (terms.Length > 1)
{
values = new int[terms.Length - 1];
for(int i = 1; i < terms.Length; i++)
{
values[i-1] = Convert.ToInt32(terms[i]);
}
}
return new Request(terms[0], values);
}
}
Здесь сразу в глаза бросается зависимость от некоторого роутера. Вся логика, по созданию команды по вводу, делегируется этой сущностью, которая является некой вариацией паттерна фабрика.
Ответственность именно ConsoleCommandInput состоит из того, чтобы прочитать строку с консоли и разобрать её в объект типа Request. Это происходит в методе ParseString. Сам по себе тип Request представляет следующее.
Листинг WendingMachine/OOP/Request.cs
class Request
{
public Request(string command, int[] values)
{
Command = command;
Values = values;
}
public string Command { get; set; }
public int[] Values { get; set; }
public bool IsIncorectValuesCount(int correct)
{
return correct != Values.Length;
}
}
Запрос у нас состоит из команды и набора аргументов с типом int. А также методом, который помогает нам валидировать аргументы. В рамках типа Request мы можем только проверить соответствует ли текущее количество аргументов необходимому объему. Для этого тут и нужен метод IsIncorectValueCount. Он делает немного обратное, он проверяет некорректное ли значение. Такой стиль выбран не просто так, в классе Router вы увидите причины, которые привели к этому.
Основная стратегия класса ConsoleCommandInput видна в методе GetCommand.
Фрагмент 2.31
public ICommand GetCommand()
{
string rawCommand = Console.ReadLine();
Request request = ParseString(rawCommand);
return _router.CreateCommand(request);
}
- Считать строку;
- Преобразовать её в запрос;
- Создать из запроса команду;
- Вернуть команду;
Третий шаг делегируется типу Router. Посмотрим на него поближе?
Листинг WendingMachine/OOP/Request.cs
class Router
{
private WendingMachine _machine;
private RouterState _state;
public Router(WendingMachine machine)
{
_machine = machine;
_state = new DefaultState(this);
}
public ICommand CreateCommand(Request request)
{
switch (request.Command)
{
case "AddMoney":
if (request.IsIncorectValuesCount(1)) return null;
return new AddMoney(_machine, request.Values[0]);
case "GetChange":
if (request.IsIncorectValuesCount(0)) return null;
return new GetChange(_machine);
case "BuyGood":
if (request.IsIncorectValuesCount(2)) return null;
return new BuyGood(_machine, _state.MakeOrder(request));
case "ShowCommands":
if (request.IsIncorectValuesCount(0)) return null;
return new ShowCommands("AddMoney", "GetChange", "BuyGood", "ShowCommands");
case "Login":
if (request.IsIncorectValuesCount(0)) return null;
return new Login(this);
default:
return null;
}
}
public void Login()
{
_state = new AdminState(this);
}
public void Logout()
{
_state = new DefaultState(this);
}
abstract class RouterState
{
protected readonly Router Router;
public RouterState(Router router)
{
Router = router;
}
public abstract IOrder MakeOrder(Request request);
}
class DefaultState : RouterState
{
public DefaultState(Router router) : base(router)
{
}
public override IOrder MakeOrder(Request request)
{
return new Order(Router._machine.GetFromId(request.Values[0]), request.Values[1]);
}
}
class AdminState : RouterState
{
public AdminState(Router router) : base(router)
{
}
public override IOrder MakeOrder(Request request)
{
return new FreeOrder(Router._machine.GetFromId(request.Values[0]), request.Values[1]);
}
}
}
Этот тип очень сложно описывать. Если что-то в вашей программе сложно описать, то скорей всего это нужно переписать. Я не берусь говорить, что в случае с этим кодом, это не так. Точнее, я это скажу, так как считаю, что сложность именно в идиоматической сложности.
Начнём с основного метода, который создает из запроса команду. Называется он CreateCommand и состоит из огромного свича, который строковое представление преобразует в объект заранее заданного типа. Правильно ли это? Не совсем. У нас есть каверзная команда – вывод всех команд. И, к сожалению, она никак не связана с этим свичом, из-за чего она может лгать. Этот момент как бы раскрывает нам глаза на ту проблему, которую мы вызвали этим свичём. Мы уже провели большую работу и инкапсулировали команду в объект и спрятали всё под интерфейсом ICommand. Но мы всё ещё не имеем расширяемого механизма преобразования строки в объект команды.
Вкратце проблема этого свича в следующем:
- Список доступных команд захардкожен и не может расширяться без переписи кода;
- Дублируется код валидации количества аргументов;
Мы героически это исправим в разделе “Рефлексия и атрибуты на примере команд”.
Переведите свой взгляд на кейс команды BuyGood. Он интересен тем, что в конструктор, сходного по названию класса, нужно передать заказ. Посмотрите откуда заказ берётся. Заметили? Там мы обращаемся к некоторому текущему состоянию нашего роутера. И это состояние выдаёт нам заказ.
Состояние скрыто под абстракцией RouterState (самый низ предыдущего листинга). Это абстрактный класс с одним методом MakeOrder. Он определяется в производных классах DefaultState и AdminState. Первый возвращает платный заказ, а второй бесплатный.
Переход в это состояние происходит в методах Login и Logout. Вот так мы решили задачу того, что у нас есть два состояния: административное и обычное. И эти состояния находятся в роутере. Инициатором переключения состояний и является команда Login.
А теперь пора рассмотреть реализации всех команд. Всего их 5.
Листинг WendingMachine/OOP/Commands/AddMoney.cs
class AddMoney : ICommand
{
private WendingMachine _machine;
private int _money;
public AddMoney(WendingMachine machine, int money)
{
_machine = machine;
_money = money;
}
public void Execute()
{
_machine.AddBalance(_money);
}
}
Листинг WendingMachine/OOP/Commands/BuyGood.cs
class BuyGood : ICommand
{
private WendingMachine _machine;
private IOrder _order;
public BuyGood(WendingMachine machine, IOrder order)
{
_machine = machine;
_order = order;
}
public void Execute()
{
_machine.TryProcessOrder(_order);
}
}
Листинг WendingMachine/OOP/Commands/GetChange.cs
class GetChange : ICommand
{
private WendingMachine _machine;
public GetChange(WendingMachine machine)
{
_machine = machine;
}
public void Execute()
{
_machine.DiscardBalance(_machine.Balance);
}
}
Листинг WendingMachine/OOP/Commands/Login.cs
class Login : ICommand
{
private Router _router;
public Login(Router router)
{
_router = router;
}
public void Execute()
{
_router.Login();
}
}
Листинг WendingMachine/OOP/Commands/ShowCommands.cs
class ShowCommands : ICommand
{
private string[] _commands;
public ShowCommands(params string[] commands)
{
_commands = commands;
}
public void Execute()
{
foreach (string command in _commands)
{
Console.WriteLine(command);
}
}
}
Всё, на этом исходный код закончился. Чего мы добились этим решением:
- Мы разделили логику программы на составные части, которые можем дорабатывать раздельно. При работе с мелкими частями, мы лучше понимаем контекст и нам сложней что-то сломать.
- Мы добавили швы, которые позволяют нам расширять функционал.
- Мы инкапсулировали команду в объект.