Избавиться от дублирующегося кода

Мы будем препарировать листинг WendingMachine/ArraysAndcycles/Program.cs, который представлен на предыдущих страницах. Полностью я его приводить сейчас не буду из-за желания сэкономить бумагу и ваши деньги.

Наш основной инструмент в этот раз – метод (функция). Мы попробуем представить нашу программу как набор методов связанных друг с другом. Этим мы сразу же попробуем избавиться от дублирующегося кода. На методы мы будем также разбивать некоторые участки, а давая получившимся методам понятные имена, мы сделаем понятней весь наш код.

Чтобы победить дублирующийся код, нужно:

  1. Найти альфа дублирующийся код;
  2. Подойти к нему уверенно;
  3. Укусить его за ушко;

После этого весь остальной дублирующийся код станет следовать за вами и вы сможете вывести его куда подальше.

Шутки шутками, но нам действительно нужно найти дублирующийся код. И не только альфа, а весь. И не кусать его, а выделять функцию. Если читать наш листинг предыдущей главы, то бросается следующий блок кода.

Фрагмент 2.10    

switch (payment)
{
    case PaymentType.Coins:
        for(int i = 0; i < coinsValues.Length; i++)
        {
            Console.WriteLine($"Сколько монет номиналом {coinsValues[i]} вы хотите внести?");
            int count = 0;
            while (!int.TryParse(Console.ReadLine(),
                                    out count))
            {
                Console.WriteLine("Вы ввели не число!");
            }
            coinsQuantity[i] += count;
            balance += count * coinsValues[i];
        }
        break;
    case PaymentType.Card:
        Console.WriteLine("Сколько снять с вашей карты?");
        int balanceDelta = 0;
        while (!int.TryParse(Console.ReadLine(),
                out balanceDelta)) 
        {
            Console.WriteLine("Вы ввели не число!");
        }
        balance += balanceDelta;
        Console.WriteLine("Баланс успешно пополнен");
        break;
    default:
        break;
}

Обратите внимание на содержимое блоков case. У нас дублируется логика, которая состоит из цикла while. Она представляет из себя следующее:

  1. Запросить у пользователя строку;
  2. Удостоверится, что эта строка – число;
  3. Если это так, то использовать эту строку как число далее;
  4. Если это не так, запросить строку ещё раз и вывести поясняющее сообщение;

Дубликат 1.1

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

Дубликат 1.2

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

Как вы видите, разница этих дубликатов только в том, какая переменная будет использоваться для хранения результате. В данном случае нам не важно как в дальнейшем будет использоваться число. Ответственность этого блока – чтение строки и конвертация в число, если это возможно, а если нет, то запрос новой строки. И в рамках этой обязанности мы будем работать.

В первую очередь мы поместим один из дубликатов в метод. Для начала мы его создадим, я его определяю в классе Program.

Фрагмент 2.11

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

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

    return result;
}

У меня получился метод ReadInt, который служит для считывания числа. Давайте посмотрим как будет выглядеть код при использовании этого метода. Сейчас мы заменим два дубликата из фрагмента 10 и получим Фрагмент 2.12, который является частью полного листинга в конце этого раздела.

Фрагмент 2.12

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;
}

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

Фрагмент 2.13   

int id = Convert.ToInt32(rawData[0]);
int count = Convert.ToInt32(rawData[1]);

rawData – это массив строк. Тут задача превратить два элемента этого массива в значения двух переменных числового типа. Как мы можем это сделать? Сейчас мы пользуемся методом ToInt32 класса Convert. Он нас устраивает лишь до того момента, пока в ячейках массива есть подходящие числа в виде строки. Тогда всё хорошо и метод произведет конвертацию. Но если там не корректные данные, метод выбросит исключение. Что делать? Использовать метод int.TryParse, который работает немного иначе. Он также производит конвертацию, но если она не удалась, то не выбрасывается исключение, а возвращается false.

public static bool TryParse(string s, out int result)

Параметры

  • s
    • Type: System.String
    • Строка, содержащая преобразуемое число.
  • result
    • Type: System.Int32
    • При возвращении этим методом содержит 32-разрядное целочисленное значение со знаком, эквивалентное числу, содержащемуся в параметре s, если преобразование выполнено успешно, или нуль, если оно завершилось сбоем. Преобразование завершается сбоем, если параметр s равен null или String.Empty, не находится в правильном формате или представляет число меньше MinValue или больше MaxValue. Этот параметр передается неинициализированным; любое значение, первоначально предоставленное в объекте result, будет перезаписано.

Возвращаемое значение:

Type: System.Boolean

Значение true, если параметр s успешно преобразован; в противном случае — значение false.

Использование этого метода всегда превращается в несколько строк кода. Давайте попробуем воспользоваться им для преобразования первой ячейки массива.

Фрагмент 2.14    

int id = 0;
if(!int.TryParse(rawData[0], out id))
{
    Console.WriteLine("Ошибка в параметре id, он должен быть числом");
    break;
}

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

Фрагмент 2.15   

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;
}

Получился интересный метод MapParameter. С его помощью мы не совсем избавляемся от дублирующегося кода, давайте посмотрим в место его применения и потом начнём разбор того, что произошло.

Фрагмент 2.16

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

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

Часть дублирующегося кода мы действительно скрыли в методе. Но всё-таки дубляж остался и пока что мы от него избавиться не можем. Один из вариантов – использование декларативного подхода к программированию с помощью рефлексии. Этим мы займёмся в самом конце этого проекта, после того как пройдёмся по другим темам.

И всё же код мы сделали по-лучше. Давайте вернемся к фрагменту 15, который содержит метод MapParameter. Он прост, с помощью него мы можем связать некий входной параметр нашей команды с переменной с типом int. В имени метода не используется уточнения, что мы ожидаем именно тип int, а вместо этого используется тип выходного параметра (параметра помеченного модификатором out). Этот параметр более красноречив нежели имя метода.

Итак, методу на вход мы даём:

  • Аргументы, которые вводил пользователь, разбитые на элементы массива;
  • Переменная, в которую нужно поместить значение параметра (в нашем случае с конвертацией в int);
  • Параметр, с которым мы работаем, значение представлено в виде enum;

В ответ метод возвращает булевое значение true – если нет никаких проблем и false если что-то пошло не так. Я думаю, вы также заметили, что теперь при обращении к кому-нибудь параметру мы используем специально созданный тип BuyGoodParameter.

Фрагмент 2.17    

enum BuyGoodParameter
{
    Id = 0, 
    Count = 1
}

Сам шаг, я думаю, вам понятен, мы выделили большую часть логики в метод и сделали тип перечисления для более четкого представления параметров. Я думаю, многим читателям также интересна техническая реализация метода MapParameter.

Там есть два интересных момента.

Во-первых, мы можем использовать кастинг к типу int по отношению к типу перечисления. В результате будет какое-то определённое число, в нашем случае, если в метод придёт значение BuyGoodParameter.Id, то в результате кастинга будет значение 0. А если придёт значение BuyGoodParameter.Count, то будет значение 1.

Во-вторых, у нас в распоряжении есть метод  Enum.GetName, который возвращает читаемое представление значения перечисления. Т.е при значении BuyGoodParameter.Id он вернёт строку “Id”. Крайне удобно.

Это всё? Нет, давайте заглянем дальше, посмотрите на это выражение.

Фрагмент 2.18    

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

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

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

Фрагмент 2.19

int totalPrice = prices[id] * count;

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

Всё ли мы сделали правильно? И да и нет. Есть один из классических рефакторингов – замена переменной вызовом метода. Выглядит он вот так:

Фрагмент 2.20   

if (balance >= GetTotalPrice(prices[id], count))
{
    balance -= GetTotalPrice(prices[id], count);
    availableQuantity[id] -= count;
}

И конечно же нам нужен метод GetTotalPrice, выглядит она вот так:

Фрагмент 2.21   

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

Да мы сделали метод, который рассчитывает стоимость заказа на основе таблицы цен и количества товара. Он имеет смысл, но немного не в том формате, в котором он сейчас. То что мы сейчас сделали, раскрывает нам одну важную мысль – у нас есть товар и у каждого товара есть набор свойств. Подумайте над тем, что эту мысль нужно выразить более строго в коде.

В той общей организации, которая у нас сейчас, сделанный нами метод очень громоздкий. И он на самом-то деле ничего не изменил. Фактически, мы сделали код немного понятней, навесив читаемую метку на выражение. Но не избавились от дублирования. Также суть выделения методов не только в том, чтоб вешать ярлыки на операции, но и в том, чтобы изменять уровень абстракции и сокрыть всякие ненужные детали.

Учитывая всё это, я бы остановился в итоге на следующем варианте:

Фрагмент 2.22

int totalPrice = GetTotalPrice(prices[id], count);

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

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

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


Leave a Reply

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