четверг, 14 августа 2008 г.

Короткий синтаксис ADO .NET (часть II)

Короткий синтаксис ADO .NET (часть I)
Короткий синтаксис ADO .NET (часть III)
Короткий синтаксис ADO .NET (часть IV)

В предыдущей части я привел перечень действий, необходимых для выполнения запроса и корректного овсобождения ресурсов. Начнем с открытия и закрытия соединения. В пределе хочется иметь сущность, которая бы:
  1. анализировала состояние указанного соединения, и открывала бы его, если оно не открыто;
  2. гарантированно закрывала бы соединение после использования, в случае если соединение было первоначально закрыто;
  3. обращение к такой конструкции не занимало бы много места.
Отличный повод воспользоваться конструкцией using и интерфейсом IDisposable чтобы избежать конструкция try/finally. Вот код требуемой сущности:
public class ConnectionSession : IDisposable
{
   public ConnectionSession(IDbConnection dbConnection)
   {
       if (dbConnection == null)
       {
           throw new ArgumentNullException("dbConnection");
       }
       DbConnection = dbConnection;

       WasOpened = ConnectionState.Open == (DbConnection.State & ConnectionState.Open);

       if (!WasOpened)
       {
           DbConnection.Open();
       }
   }

   public IDbConnection DbConnection { get; private set; }
   public bool WasOpened { get; private set; }

   public void Dispose()
   {
       if (!WasOpened)
       {
           DbConnection.Close();
       }
   }
}
Прежде чем начну комментировать код, хочу заметить, что в связи с грядущим переходом на C# 3.0, я начинаю пробовать на вкус новый синтаксис. Однако, все или почти все можно повторить на C# 2.0.

Как я уже сказал, интерфейс IDisposable реализован чисто для поддержания короткого синтаксиса using. Сей объект не нуждается в финализации и DbConnection сам будет освобождать необходимые ресурсы при сборке мусора. Быть может когда-нибудь я уделю связке using/IDisposable больше внимания.

Почему я использовал IDbConnection интерфейс, вместо базового класса DbConnection? Очень просто. При тестировании я могу подменить объект без средств тестирования, использующих инструментацию сборок. Для генерации mock объектов и заглушек (stub) я использую Rhino Mocks. Это первая библиотека мокогенераторов, которую я использую, и очень доволен ей. Пока не было поводов переходить на другие. А с новым синтаксисом (для .NET 3.5) я просто обожаю Rhino (это не реклама! Rhino Mocks - бесплатная библиотека и мне за упоминание о ней никто не платит).

В постах я буду приводить некоторые тесты, но далеко не все, которые пишу.
[TestMethod]
   public void UsingConstructionWithOpenedConnectionTest()
   {
       var connection = MockRepository.GenerateStub<IDbConnection>();

       connection.Stub(x => x.State).Return(ConnectionState.Closed);

       using (new ConnectionSession(connection))
       {
           connection.AssertWasCalled(x => x.Open());
           connection.AssertWasNotCalled(x => x.Close());
       }

       connection.AssertWasCalled(x => x.Close());
   }

   [TestMethod]
   public void UsingConstructionWithClosedConnectionTest()
   {
       var connection = MockRepository.GenerateStub<IDbConnection>();

       connection.Stub(x => x.State).Return(ConnectionState.Open);

       using (new ConnectionSession(connection))
       {
           connection.AssertWasNotCalled(x => x.Open());
       }

       connection.AssertWasNotCalled(x => x.Close());
   }
Несмотря на то, что в каждом тесте рекомендуется проверять не более одного условия, я напичкал их множеством условий, потому как я тестировал поведение ConnectionSession в рамках конструкции using. Это уже даже не unit тест, а некий интеграционный. Unit тесты класса ConnectionSession я опустил. Их больше, чем хотелось бы вставлять в пост. Оговорки в условиях к ConnectionSession (вначале поста) нужны для безопасного использования соединения в области действия нескольких вложенных блоков using для ConnectionSession объектов. В рамках C# 3.0 будет удобен следующй extension метод:
public static class ConnectionSessionExtensions
{
    public static ConnectionSession CreateSession(this IDbConnection connection)
    {
        return new ConnectionSession(connection);
    }
}
это позволит писать код в стиле
using (connection.CreateSession())
{
    return command.ExecuteNonQuery();
}
Довольно емкий смысл вложен в одну строчку:
если соединение не открыто, то открыть его, если было закрыто, то закрыть после выхода из блока.
Уже этот код позволит многим сделать обращения к ADO (и не только) более лаконичными. Но на этом не остановимся, продолжение следует.

2 комментария:

  1. Не совсем понятно, почему в методе Dispose класса ConnectionSession стоит проверка
    if (!WasOpened) {...}

    Может быть, должно быть
    if (WasOpened) {...} ?

    ОтветитьУдалить
  2. WasOpened хранит признак того, было ли открыто соединение до создания экземпляра ConnectionSession. И если оно было открыто, то за закрытие соединения должен отвечать тот, кто его открыл. А если оно не было открыто и его открыл именно этот экземпляр сессии, то в этом случае следует закрыть соединение.

    Такая проверка позволяет использовать несколько вложенных сессий с одним соединением.

    ОтветитьУдалить