понедельник, 17 декабря 2012 г.

Unit Of Work + NHibernate Explained

Мой вариант реализации Unit Of Work, применительно к NHibernate.

1. Интерфейс IUnitOfWork:

 public interface IUnitOfWork : IServiceProviderIDisposable
 {
     void Commit();
 }

  • Лишь один метод Commit, больше ничего не нужно. Добавление метода Rollback может привести к тому, что в результате ошибки мы можем попасть в третье состояние, когда ни Commit ни Rollback не были вызваны.
  • Никаких методов Add/Remove/Save/Get etc. Это не является ответственностью UoW.
  • Наследование от IServiceProvider/IDisposable нужно лишь в случае, если не используется IoC контейнер с поддержкой nested lifetime scope.
2. Интерфейс IServiceProvider:

 public interface IServiceProvider
 {
     IRepository GetRepository();
 }

  • Как я уже сказал он нужен только в случае отсутствия нормального IoC контейнера.
  • Нужен для получения scoped сервисов, чье время жизни будет связано с UoW.
  • По хорошему метод должен быть таким:
       T GetService();
    
         но я решил упростить пример :)

3. Интерфейс IRepository:

 public interface IRepository
 {
     T Get(int id);
 
     void Add(T obj);
 
     void Delete(T obj);
 }

  • Вот он то и отвечает за все получение/сохранение объектов
  • Бросается в глаза отсутствие метода Save :)
4. Пример использования:

 var uowFactory = new UnitOfWorkFactory();
 
 int catId;
 using (var uow = uowFactory.CreateUnitOfWork())
 {
     var repository = uow.GetRepository<Cat>();
     var cat = new Cat { Name = "Chalky", Age = 3 };
     repository.Add(cat);
 
     uow.Commit();
 
     catId = cat.Id;
 }
 
 using (var uow = uowFactory.CreateUnitOfWork())
 {
     var repository = uow.GetRepository<Cat>();
     var cat = repository.Get(catId);
     cat.Age = 4;
 
     uow.Commit();
 }
5. Реализация:

 internal sealed class UnitOfWork : IUnitOfWork
 {
     private readonly ISession _session;
     private readonly ITransaction _transaction;
 
     public UnitOfWork(ISession session)
     {
         _session = session;
         _transaction = session.BeginTransaction();
     }
 
     public void Commit()
     {
         if (!_session.IsOpen)
         {
             throw new InvalidOperationException("Session is already closed!");
         }
 
         _transaction.Commit();
         _session.Close();
     }
 
     public IRepository GetRepository()
     {
         return new Repository(_session);
     }
 
     public void Dispose()
     {
         try
         {
             _session.Dispose();
         }
         catch
         {
              // TODO: log exception
         }
     }
 }


Если есть желание сэкономить на спичках, то можно задуматься о lazy создании транзакции, чтобы UoW с no-op не приводил к открытию соединения.


 internal sealed class Repository : IRepository
 {
     private readonly ISession _session;
 
     public Repository(ISession session)
     {
         _session = session;
     }
 
     public T Get(int id)
     {
         return _session.Get(id);
     }
 
     public void Add(T obj)
     {
         _session.Save(obj);
     }
 
     public void Delete(T obj)
     {
         _session.Delete(obj);
     }
 }
 
 public sealed class UnitOfWorkFactory
 {
     private readonly ISessionFactory _sessionFactory;
 
     public UnitOfWorkFactory()
     {
         // TODO:
         //_sessionFactory = configuration.BuildSessionFactory();
     }
 
     public IUnitOfWork CreateUnitOfWork()
     {
         return new UnitOfWork(_sessionFactory.OpenSession());
     }
 }
 
 public sealed class Cat
 {
     public int Id { getset; }
 
     public string Name { getset; }
 
     public int Age { getset; }
 }

четверг, 15 ноября 2012 г.

Все еще делаете CRUD?

Сегодня наткнулся на замечательную статью. Ее замечательность состоит в том, что она полностью соответствует моему пониманию CRUD-style приложений, а именно:

  1. Если вы делаете CRUD, то пользы от вашего приложения мало. Самое полезное чего добилось ваше приложение это спасло пару деревьев для бобров.
  2. Пользователи продолжают вбивать данные, которые раньше записывались на бумаге, в электронном виде. Скорее всего существуют какие то бизнес процессы, но они находятся в головах сотрудников, а не в приложении.
  3. Ваш бизнес имеет плохой Bus Factor :)
  4. Вам следует задуматься о создании задачеориентированного интерфейса (кто-нибудь знает лучший русский эквивалент?) чтобы добавить "полезности" вашему приложению.
  5. Первый признак CRUD ориентированности это множество гридов (ну не могу я подобрать нормальный синоним!)
Оригинал статьи здесь.

пятница, 26 октября 2012 г.

CQRS Journey

Решил ознакомиться с результами трудов P&P и был приятно удивлен! В кое то веки P&P выпустило что-то стоящее :) Пусть это не замена всем остальным разбросанным по просторам интернета знаниям о DDD/CQRS/ES, но для начального ознакомления очень даже подойдет. Продолжаю читать.

среда, 19 сентября 2012 г.

Expression.Property breaking change in .NET 4.0


Если вы переезжаете на .NET 4.0 и используете expression trees то возможно вас заинтересует, что раньше такой код был валиден и для статических свойств:

var comparerExpr = Expression.Property(Expression.Constant(comparerType), comparerProperty);

Теперь же требуется дополнительная проверка:

var comparerExpr = Expression.Property(isStatic ? null : Expression.Constant(comparerType), comparerProperty);

Все дело во внутренностях метода Expression.Property!

Было:

public static MemberExpression Property(Expression expression, PropertyInfo property)
{
    if (property == null)
    {
        throw Error.ArgumentNull("property");
    }
    if (!property.CanRead)
    {
        throw Error.PropertyDoesNotHaveGetter(property);
    }
    if (!property.GetGetMethod(true).IsStatic)
    {
        if (expression == null)
        {
            throw Error.ArgumentNull("expression");
        }
        if (!AreReferenceAssignable(property.DeclaringType, expression.Type))
        {
            throw Error.PropertyNotDefinedForType(property, expression.Type);
        }
    }
    return new MemberExpression(expression, property, property.PropertyType);
}

Стало:

public static MemberExpression Property(Expression expression, PropertyInfo property)
{
    ContractUtils.RequiresNotNull(property, "property");
    MethodInfo info = property.GetGetMethod(true) ?? property.GetSetMethod(true);
    if (info == null)
    {
        throw Error.PropertyDoesNotHaveAccessor(property);
    }
    if (info.IsStatic)
    {
        if (expression != null)
        {
            throw new ArgumentException(Strings.OnlyStaticPropertiesHaveNullInstance, "expression");
        }
    }
}


пятница, 4 мая 2012 г.

"Лишние" ссылки в проектах


Известно что «лишняя» ссылка из проекта А в проект В в результате компиляции не попадет в A.dll – об этом позаботится компилятор. Казалось бы ну и нет проблемы. Однако с точки зрения билд-процесса эта «лишняя» ссылка существует и поэтому если у вас изменились исходные файлы в проекте В, то будет перестроен и он и проект А, даже если последний не менялся. Так что для ускорения билдов неплохо было бы подчистить эти «лишние» ссылки. Для этого можно воспользоваться R#-ом и его «Optimize references».

Все сказанное помогает «нормальным» проектам. Нормальные проекты - это проекты, чье дерево зависимостей между проектами выглядит «широким», то есть имеет много «листовых», ни от кого независящих проектов. Если же у вас есть одна сборка Помойка.dll, от которой зависит большинство остальных сборок (таким образом ваше дерево проектов выглядит как Александрийский столп), то вы сами себе буратины – можете ходить и пить кофе на каждый билд.

P.S. Дерево проектов и его ширину-глубину можно посмотреть в билд-логе под расширенным логгированием.

воскресенье, 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