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

DRY: Part 3 - Creational logic duplication

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

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

Всем известно, что в компании DcMonald's ленивых, грубых и неопрятных сотрудников превращают в зомби. При этом в каждом из этих случаев приходится повторять непростой процесс изготовления шаманом порошка из рыбы фугу, отравления сотрудника и чтения заклинания:

foreach (var employee in _employeeRepository.FindLazyEmployees())
{
    Bokor bokor = new Bokor();
    Fish fish = new Pufferfish();
    Powder ttx = bokor.PrepareTetrodotoxinFrom(fish);
    employee.Drink(ttx);
    bokor.CastSpellOn(employee);
    Zombie zombie = new Zombie(employee);
    // now we have good zombie and can use it
}

Любая ошибка в этой последовательности приведёт к утрате ценного сотрудника. Но избежать дублирования таких ценных знаний можно с помощью рефакторинга Move Creation Knowledge to Factory, либо Extract Method.

public class ZombieFactory
{
    private Bokor _bokor;

    public ZombieFactory(Bokor bokor)
    {
        _bokor = bokor;
    }

    public Zombie CreateZombieFrom(Employee employee)
    {
        Fish fish = new Pufferfish();
        Powder ttx = _bokor.PrepareTetrodotoxinFrom(fish);
        employee.Drink(ttx);
        _bokor.CastSpellOn(employee);
        return new Zombie(employee);
    }
}

Теперь любой из отделов компании может создавать зомби, не вдаваясь в подробности сложного ритуала. И что самое главное - когда знания о создании зомби имеют единственное представление в системе, мы можем легко и быстро внести изменения в этот процесс (например, заменить ритуал вуду просмотром рекламы iPhone).

foreach (var employee in _employeeRepository.FindLazyEmployees())
{
    Zombie zombie = _zombieFactory.CreateZombieFrom(employee);
    // now we have good zombie and can use it
}

Другой случай - дублирование параметров конструкторов. Если периодически приходится повторять какое-то магическое сочетание параметров, значит в этом наборе параметров содержиться знание о каком-то особенном типе объекта. Например, я заметил, что в тестах несколько раз повторялись вызовы такого конструктора (первый параметр - размер ссуды, второй - дата возврата):

Loan loan = new Loan(100, DateTime.Now.AddDays(-1));

Немного поразмыслив, можно догадаться, что так создаётся просроченная ссуда. Чтобы устранить дублирование такого типа, можно воспользоваться рефакторингами Extract Class / Extract Subclass, либо Replace Constructors with Creation Methods:

public Loan CreateExpiredLoan(decimal amount)
{
    return new Loan(amount, DateTime.Now.AddDays(-1));
}

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

И ещё один случай - дублирование логики инициализации объекта в разных конструкторах:

public class Employee
{
    private string _name;
    private decimal _hourlyPayRate;

    public Employee(string name)
    { 
        if (String.IsNullOrEmpty(name))
            throw new ArgumentException("Name cannot be empty.");

        _name = name;
        _hourlyPayRate = 25M;
    }

    public Employee(string name, decimal hourlyPayRate)
    {
        _name = name; // oops, can be null
        _hourlyPayRate = hourlyPayRate;
    }

    // etc.
}

Да, это простой случай. Но что, если конструкторов будет 5 и параметров побольше? Что, если надо добавить проверку параметров или другую логику? Знания о правильной инициализации объекта должны присутствовать в единственном экземпляре. И для этого тоже есть свой рефакторинг - Chain Constructors:

public class Employee
{
    private string _name;
    private decimal _hourlyPayRate;

    public Employee(string name)
        : this(name, 25M)
    {
    }

    public Employee(string name, decimal hourlyPayRate)
    {
        if (String.IsNullOrEmpty(name))
            throw new ArgumentException("Name cannot be empty.");

        _name = name;
        _hourlyPayRate = hourlyPayRate;
    }
}

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

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