воскресенье, 26 февраля 2012 г.

О валидации доменных объектов

Читаю эту старую, но не потерявшую популярность, книжку и не перестаю удивляться как давно люди пишут про "качество кода": ортогональность, high cohesion/low coupling и т.д. Все эти разговоры какие-то "неземные" - в реальной жизни приходится постоянно сталкиваться с трудностями в базовых вещах вокруг ООП...

Частая ситуация, с которой приходится сталкиваться, это невалидные данные при сохранении сущностей: это может быть сработавший constraint в базе, а может вы это обнаружите уже при вычитывании сущностей... Для примера рассмотрим такую "доменную" модель:

class Rectangle
{
    public int Height { get; set; }
    public int Width { get; set; }

    public int Area()
    {
        return Height * Width;
    }
}

class Square : Rectangle
{
}


В нашем примере проблема состоит в том, что в базе оказываются "квадраты" с разными сторонами. Проблему можно "решить" разными способами, например так:

class Rectangle
{
    public virtual int Height { get; set; }
    public virtual int Width { get; set; }

    public int Area()
    {
        return Height * Width;
    }
}

class Square : Rectangle
{
    private int _lengthOfSide;

    public override int Height
    {
        get
        {
            return _lengthOfSide;
        }
        set
        {
            _lengthOfSide = value;
        }
    }

    public override int Width
    {
        get
        {
            return _lengthOfSide;
        }
        set
        {
            _lengthOfSide = value;
        }
    }
}

Проблемы с "пуговицами", то есть с нецелостными данными в базе, уйдут, зато появятся другие... Достаточно быстро мы придем к идее валидации объекта перед его сохранением:

class Rectangle
{
    public int Height { get; set; }
    public int Width { get; set; }

    public int Area()
    {
        return Height * Width;
    }

    public virtual void Validate()
    {
    }
}

class Square : Rectangle
{
    public override void Validate()
    {
        base.Validate();

        if (Height != Width)
        {
            throw new InvalidProgramException("Height and Width must be equal!");
        }
    }
}

Проблема вроде бы решена, остается выбрать бросать исключение или, например, получать от метода коллекцию ошибок валидации для более удобного отображения информации пользователю. Однако есть несколько моментов:

  1. Наличие метода Validate "подрывает" наше доверие к объектам, подталкивает к тому, чтобы вызывать его постоянно, "на всякий случай". В случае больших сущностей и большого набора правил валидации это может быть накладно. После этого мы конечно же придумаем механизм оптимизации...
  2. Результат валидации бесполезен! Как так? Ну а что толку в том, что перед сохранением объекта мы узнали что он невалиден? Сохранить об этом запись в логе и бросить исключение - вот все что мы можем. Мы не знаем когда объект стал невалидным, а если это "развесистый" объект с десятком свойств, который по пути к нам пересек несколько границ процессов, то шансов найти то самое место в коде, где на самом деле произошла ошибка, становится практически нереальным. Получив подобную ошибку с "живой" площадки мы с большой вероятностью либо закроем ее как unable to reproduce либо она вечно будет висеть в состоянии open.
Что если бы мы падали с исключением и записью в логе в тот самый момент, когда пытались привести объект в невалидное состояние? Хотя бы так:

class Rectangle
{
    public int Height { get; private set; }
    public int Width { get; private set; }

    public int Area()
    {
        return Height * Width;
    }

    public virtual void Resize(int height, int width)
    {
        Height = height;
        Width = width;
    }
}

class Square : Rectangle
{
    public override void Resize(int height, int width)
    {
        if (height != width)
        {
            throw new ArgumentException("Height and Width must be equal!");
        }

        base.Resize(height, width);
    }
}


Идея проста: не надо валидировать доменные объекты, надо недопускать их невалидного состояния! Причем здесь ООП? При том что мы не дали внешнему коду менять состояние нашего объекта, а спрятали это действие и связанные с ним правила внутрь метода объекта. Не это ли называется инкапсуляцией?

P.S. Предвижу вопроcы типа "наши пользователи редактируют объекты как хотят, а мы должны подсвечивать неправильные данные". Ответ прост - то что пользователи видят и редактируют это совсем не доменные объекты...
Wider Two Column Modification courtesy of The Blogger Guide