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

YAGNI (You ain't gonna need it)


Леонард потратил много времени на то, что (как он считал) ему понадобится в будущем - забронировал столик ресторане, купил билеты в кино и т.п. Из-за этого он не успел сделать то, что важно для Пенни здесь и сейчас - купить цветы и одеться поприличнее. Если бы Леонард не пытался предсказать будущее, а делал то, что действительно нужно, шансов на свидание у него было бы больше. Кроме того, он сэкономил бы время, потраченное на то, что так и не пригодилось.

Приблизительно так же мы выглядим, когда пишем код, который может нам понадобится в будущем. К чему это приводит:
  • тратится время, которое можно (и нужно) было потратить на добавление, тестирование и улучшение действительно необходимой сейчас функциональности
  • становится больше кода, который нужно сопровождать
  • ограничивается и усложняется добавление новой, действительно необходимой функциональности
Это при условии, что мы угадали, и код действительно понадобился в будущем. К сожалению, люди не очень хорошие предсказатели (иначе букмекеры давно бы обанкротились), и скорее всего такой код просто будет захламлять приложение. Зато нам свойственно учиться на ошибках! Поэтому возник более практичный подход к разработке, основанный на принципе YAGNI:
Реализуйте функциональность только тогда, когда она действительно нужна, а не когда вы предвидете, что она вам понадобится.
Этот принцип очень популярен в экстримальном программировании. В первую очередь это связано с применением методологии Scrum, где набор добавляемой функциональности чётко определяется перед началом очередной итерации, а приоритеты требованиям расставляет заказчик. Добавляя то, что отсутствует в бэклоге спринта, разработчик рискует не успеть сделать запланированную функциональность. Кроме того, он ставит собственное мнение о приоритетах выше мнения заказчика!

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

public class Calculator
{
    public double Result { get; private set; }

    public void Add(double value)
    {
        Result += value;
    }
}

По-моему было бы здорово, если бы сохранялась история операций. Это можно сделать инкапсулировав операции в объекты-комманды. Чтобы не пришлось потом вносить много изменений, лучше сделать это сразу:

public class Calculator
{
    public double Result { get; private set; }
    private List<IOperation> _operations = new List<IOperation>();

    public void Add(double value)
    {
        IOperation operation = new AdditionOperation(Result, value);
        Result = operation.Execute();
        _operations.Add(operation);
    }
}

public interface IOperation
{
    double Execute();
}

public class AdditionOperation : IOperation
{
    private double _augend;
    private double _addend;

    public AdditionOperation(double augend, double addend)
    {
        _augend = augend;
        _addend = addend;
    }

    public double Execute()
    {
        return _augend + _addend;
    }

    public override string ToString()
    {
        return String.Format("{0} + {1}", _augend, _addend);
    }
}

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

public class Calculator
{
    public double Result { get; private set; }
    private List<IOperation> _operations = new List<IOperation>();

    public void Add(double value)
    {
        _operations.Add(new AdditionOperation(Result, value));
        ExecuteLastOperation();
    }

    public void Subtract(double value)
    {
        _operations.Add(new SubtractionOperation(Result, value));
        ExecuteLastOperation();
    }

    private double ExecuteLastOperation()
    {
        if (_operations.Count == 0)
            return 0;

        return _operations.Last().Execute();
    }
}

public class SubtractionOperation : IOperation
{
    private double _subtrahend;
    private double _minuend;

    public SubtractionOperation(double minuend, double subtrahend)
    {
        _minuend = minuend;
        _subtrahend = subtrahend;
    }

    public double Execute()
    {
        throw new NotImplementedException();
    }
}

// IOperation
// AdditionOperation

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

public class Calculator
{
    public double Result { get; private set; }
    private List<IOperation> _operations = new List<IOperation>();

    public void Add(double value)
    {
        _operations.Add(new AdditionOperation(Result, value));
        _operations.Last().Execute();
    }

    public void Subtract(double value)
    {
        _operations.Add(new SubtractionOperation(Result, value));
        _operations.Last().Execute();
    }

    public void Divide(double value)
    {
        Guard.NotZero(value);
        Result = Result / value;
    }

    public void Multiply(double value)
    {
        Result = Result * value;
    }    
}

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

Принцип YAGNI помогает обуздать нашу фантазию и направить усилия на реализацию того, что действительно надо. Позже заказчик сам определит, на сколько важна отмена операций (если она вообще нужна). Кроме того, позже у нас будет больше знаний о системе и о функциональности, которую необходимо добавить. Например, может оказаться, что нужна возможность изменять параметры операций так, чтобы все последующие операции пересчитывались. Или параметры вообще будут не нужны - сойдёт и обычный стек результатов. Откуда нам знать эти нюансы сейчас? Поэтому наша первоочердная цель - простой, понятный код, не содержащий дублирования и готовый к добавлению новой функциональности:

public class Calculator
{
    public double Result { get; private set; }

    public void Add(double value)
    {
        Result += value;
    }

    public void Subtract(double value)
    {
        Result -= value;
    }

    public void Divide(double value)
    {
        Guard.NotZero(value);
        Result = Result / value;
    }

    public void Multiply(double value)
    {
        Result *= value;
    }    
}

Допустим, нам необходимо сейчас добавить функциональность А. Почему возникает желание добавить функциональность Б, в которой сейчас нет необходимости?

Easy Now Hard Later - иногда кажется, что сейчас функциональность Б добавить проще, чем потом. На самом деле, если код хорошо факторизирован, это не будет сложно. Иначе - ненужная функциональность Б усложнит добавление нужной функциональности А.

Really Will Need It - функциональность Б действительно присутствует в списке требований, почему бы не добавить её сейчас? Потому что сейчас надо добавить функциональность А. Добавляя что-то другое можно выбиться из графика. Кроме того, любой список требований может измениться, например, если у заказчика закончатся деньги.

Might Lose The Idea - если не добавить функциональность Б сейчас, то о ней можно забыть. На практике не все идеи оказываются такими хорошими, какими кажутся вначале. Не все хорошие идеи одобряются заказчиком. В любом случае, существует масса способов не забыть идею - внести её в список задач, записать на бумажке, обычный TODO комментарий, в конце концов.

В общем, стоит добавлять лишь функциональность, которая действительно необходима для приложения. А все попытки добавить то, что возможно понадобится в будущем, лучше отложить до тех времён, когда вы разбогатеете на тотализаторе!

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

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