среда, 7 сентября 2011 г.

DRY: Part 2 - Behavior duplication

Другие части эпической саги о вреде дублирования:

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

Почему мы видим одинаковый код несколько раз? Потому что для системы именно эта последовательность действий имеет определённое значение, этот код несёт в себе единицу знаний. Хочешь написать что-то ещё раз? Задумайся почему. Небольшой пример - начисление зарплаты в штатах:

public class Employee
{
    private int _id;
    private decimal _hourlyPayRate;
    private decimal _federalTax;
    private decimal _stateTax;

    public decimal CalculatePayment(int hoursWorked)
    {
        return _hourlyPayRate * hoursWorked
               - _federalTax - _stateTax
               - _hourlyPayRate * hoursWorked * 7.65M / 100;
    }

    // etc.
}

В нашем случае дважды встречается выражение _hourlyPayRate * hoursWorked. Первым делом надо понять, что оно значит (а оно определённо что-то значит, раз нам пришлось написать одно и то же дважды). Это ни что иное, как вычисление начисленной заработной платы. Прямо бинго! Теперь надо избавиться от дублирования этих знаний, потому что даже в таком простом случае ваш коллега, добавляя к зарплате рождественский бонус, может забыть изменить одну из копий.

Бороться с дублированием не сложно. Если одинаковый код встречается в пределах одного метода, можно применить рефакторинг Introduce Explaining Variable:

public decimal CalculatePayment(int hoursWorked)
{
    decimal grossPayment = _hourlyPayRate * hoursWorked;
    return grossPayment - _federalTax - _stateTax
               - grossPayment * 7.65M / 100;
}

Когда одинаковый код встречается в нескольких методах одного класса, либо он объёмен, помогает рефакторинг Extract Method:

public decimal CalculatePayment(int hoursWorked)
{
    decimal grossPayment = CalculateGrossPayment(hoursWorked);
    return grossPayment - _federalTax - _stateTax
           - grossPayment * 7.65M / 100;
}

private decimal CalculateGrossPayment(int hoursWorked)
{
    int workHoursInWeek = 40;
    int hoursOvertime = hoursWorked - workHoursInWeek;

    decimal result = hoursWorked * _hourlyPayRate;

    if (hoursOvertime > 0)
        result += hoursOvertime * _hourlyPayRate * 1.5M;

    if (DateTime.Today.Month == 12)
        result += _christmasBonus;

    return result;
}

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

Код может дублироваться не только в пределах одного класса, но и среди нескольких классов, порой даже совершенно независимых. В этом случае на помощь приходят рефакторинги Extract Superclass и Extract Class.

Например, мне довольно быстро надоедает у каждого класса в домене создавать поле _id. Принцип DRY указывает на то, что это поле имеет одинаковый смысл в рамках системы. И действительно - это Identity Field - поле, хранящее идентификатор записи базы данных для поддержки соответствия между объектом и строкой базы данных. Поэтому я выделяю Layer Supertype - базовый класс для всех объектов слоя. И выношу в него код, дублирующийся у всех объектов этого слоя:

public abstract class Entity<TId>
{
    private readonly TId _id;

    protected Entity(TId id)
    {
        _id = id;
    }

    public TId Id
    {
        get { return _id; }
    }

    // other common behavior
}

public class Employee : Entity<int>
{
    // keep in mind - id is not here anymore
    private decimal _hourlyPayRate;
    private decimal _federalTax;
    private decimal _stateTax;

    // etc.
}

Комментариев нет:

Отправить комментарий