Alexander Beletsky's Development Blog: 2010-04

Web development: Реализация бизнес объекта

В прошлой статье я начал рассмотрение общих вопросов разработки под веб с ASP.net (C#) 3.5. Сейчас рассмотрим более детальный пример, в котором я покажу одной из бизнес функций для сайта.



Веб сайт, на который надо добавить страницу регистрации. Лайаут страницы регистрации выглядит следующим образом (шаблон сайта взят http://www.oswd.org/):




Регистрация, это функция данного сайта, поэтому, как и все другие функции она будет реализована в библиотеке BLL (busines logic layer). Начнем, как обычно, с тестов. В проект Company.Product.BLL.Test добаляем новую фикстуру и первый, самый простой тест.




using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NUnit.Framework;

namespace Company.Product.BLL.Tests
{
  [TestFixture]
  public class RegistrationTests
  {
    [Test]
    public void Smoke()
    {
      var t = new Registration();
    }
  }
}


* This source code was highlighted with Source Code Highlighter.




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



Добавим Registration.cs класс в проект Company.Product.BLL, сделаем тестовый проект собирабельным и запустим тест. Тест будет зеленым, а это значит, что пора начинать.


Бизнес требования


Какие же конкретные действия мы ожидаем от страницы регистрации? Запишем их:


- если регистрация прошла успешно, показать приветсвие и перевести на главную страницу

- если пользователь с таким имейлом уже есть в системе, вывести предупреждение

- если во время регистрации произошла ошибка, показать сведения об ошибке пользователю





По моему это самое необходимое.. Если что-то будет нужно дополнительно, добавим в процессе реализации.




Подготовительные действия



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




using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Company.Projects.Views;
using Company.Product.DAL;

namespace Company.Product.BLL
{
  public class Registration
  {
    private IRegistrationView _view;
    private IRegistrationData _data;

    public Registration(IRegistrationView view, IRegistrationData data)
    {
      _view = view;
      _data = data;
    }
  }
}

* This source code was highlighted with Source Code Highlighter.




Соответсвенно в проекты View и DAL добавим декларации интерфейсов IRegistrationView, IRegistrationData, которые пока что не содержат ни одного метода. Это позволит собрать проект BLL, но не тестовый проект.





В тестовый проект добавим фолдер Mocks, а в него два класса, RegistrationViewMock и RegistrationDataMock, которые реализовывают интерфейсы отображения и данных. Также нужно добавить референсы на View и DAL проекты.






Смоук тест после модификаций выглядит следующим образом




[Test]
public void Smoke()
{
  var t = new Registration(new RegistrationViewMock(), new RegistrationDataMock());
}

* This source code was highlighted with Source Code Highlighter.




Забегая вперед отмечу, что почти все бизнес объекты имеют схожий конструктор.. принимающий 2 аргумента - отображение и данные (хотя не исключены и частные случаи, когда объекту нужны только отображение или, наоборот, только данные).





Тестовый проект запускается и собирается, поехали дальше.




Регистрация нового пользователя



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





Реализуем данное требование в виде теста.




[Test]
public void RegisterNewUser()
{
  //INIT
  var view = new RegistrationViewMock();
  var data = new RegistrationDataMock();
  var register = new Registration(view, data);

  //ACT
  register.RegisterUser("email", "secret phrase", "password");

  //POST
  Assert.That(view.RegisterSuccess, Is.True);
}


* This source code was highlighted with Source Code Highlighter.




Добавим реализацию метода RegisterUser в бизнес объект (просто пустой метод), мок-объект отображения будет таким,




class RegistrationViewMock : IRegistrationView
{
  private bool _success = false;

  public bool RegisterSuccess
  {
    get { return _success; }
  }
}

* This source code was highlighted with Source Code Highlighter.




Тест валится с сообщением:




TestCase 'Company.Product.BLL.Tests.RegistrationTests.RegisterNewUser' failed:
Expected: True
But was: False

* This source code was highlighted with Source Code Highlighter.




(это правильно, вообще тесты надо создавать так, чтобы первый запуск всегда был неудачным).





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




public void RegisterUser(string email, string secret, string password)
{
  _view.Success();
}

* This source code was highlighted with Source Code Highlighter.




Используя средства студии генерируем новый метод интерфейса View, и добавляем реализацию метода в мок-объект.




#region IRegistrationView Members

public void Success()
{
  _success = true;
}

#endregion

* This source code was highlighted with Source Code Highlighter.




Перезапустим тесты, и получим зеленый результат.




Регистрация пользователя с одинаковым имейлом



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





В коде это предлождение выглядит так,




[Test]
public void RegisterUserWithSameEmail()
{
  //INIT
  var view = new RegistrationViewMock();
  var data = new RegistrationDataMock();
  var register = new Registration(view, data);

  //ACT
  register.RegisterUser("email", "secret phrase", "password");
  register.RegisterUser("email", "secret phrase", "password");

  //POST
  Assert.That(view.RegisterSuccess, Is.False);
  Assert.That(view.FailureMessage, Is.EqualTo("Sorry, but user with same e-mail is already registered on site"));
}

* This source code was highlighted with Source Code Highlighter.




В мок для отображения добавим новое свойство типа string, FailureMessage. Запустим тест, и получим красный результат.


TestCase 'Company.Product.BLL.Tests.RegistrationTests.RegisterUserWithSameEmail'
failed:
Expected: False
But was: True

* This source code was highlighted with Source Code Highlighter.




Добавим самую простую реализацию, которая приходит в голову:




public void RegisterUser(string email, string secret, string password)
{
  if (_email == email)
  {
    _view.Fail("Sorry, but user with same e-mail is already registered on site");
    return;
  }

  _email = email;
  _view.Success();
}

* This source code was highlighted with Source Code Highlighter.




Для отображения сгенеруем новый метод Fail, и реализуем его в моке.




public void Fail(string message)
{
  _failureMessage = message;
  _success = false;
}

* This source code was highlighted with Source Code Highlighter.




ОК, запустим тест.. и получим зеленый результат!





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





Корректный сценарий использования выглядит так,




//ACT
register.RegisterUser("email", "secret phrase", "password");
register = new Registration(view, data);
register.RegisterUser("email", "secret phrase", "password");

* This source code was highlighted with Source Code Highlighter.




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





Изменим реализацию метода регистрации




public void RegisterUser(string email, string secret, string password)
{
  if (_data.IsUserExists(email))
  {
    _view.Fail("Sorry, but user with same e-mail is already registered on site");
    return;
  }

  _data.RegisterUser(email, secret, password);
  _view.Success();
}


* This source code was highlighted with Source Code Highlighter.




И сгенирируем 2 новых метода у интерфейса данных.




#region IRegistrationData Members

public bool IsUserExists(string email)
{
  throw new NotImplementedException();
}

public void RegisterUser(string email, string secret, string password)
{
  throw new NotImplementedException();
}

#endregion

* This source code was highlighted with Source Code Highlighter.




Тесты не проходят, потому как оба метода выбрасывают исключения. Добавим реализацию,




#region IRegistrationData Members

public bool IsUserExists(string email)
{
  return false;
}

public void RegisterUser(string email, string secret, string password)
{

}

#endregion

* This source code was highlighted with Source Code Highlighter.




Перезапустим тест, он все еще красный, но уже по другой причине:




TestCase 'Company.Product.BLL.Tests.RegistrationTests.RegisterUserWithSameEmail'
failed:
Expected: False
But was: True

* This source code was highlighted with Source Code Highlighter.




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




class RegistrationDataMock : IRegistrationData
{
  private IList<string> _userEmails = new List<string>();

  #region IRegistrationData Members

  public bool IsUserExists(string email)
  {
    return (_userEmails.Contains(email));
  }

  public void RegisterUser(string email, string secret, string password)
  {
    //register user
    _userEmails.Add(email);
  }

  #endregion
}

* This source code was highlighted with Source Code Highlighter.




После сборки и запуска, тест зеленый.




Неожиданная ошибка во время регистрации



Мы должны учитывать тот факт, что DAL будет обращатся к реальному источнику данных, а это значит, что мы должны быть готовы к тому, что методы DAL могут генерировать исключения.




[Test]
public void RegisterFailed()
{
  //INIT
  bool failOnRegister = true;
  var view = new RegistrationViewMock();
  var data = new RegistrationDataMock(failOnRegister);
  var register = new Registration(view, data);

  //ACT
  register.RegisterUser("email", "secret phrase", "password");

  //POST
  Assert.That(view.RegisterSuccess, Is.False);
  Assert.That(view.FailureMessage, Is.EqualTo("Sorry, but unexcpected exception happend during operation. Please try to register later"));
}

* This source code was highlighted with Source Code Highlighter.




Констуруктор дата объекта модифицируем так, чтобы он принимал флаг об успешном или не успешном прохождении регистрации. А метод регистрации поменяем так, чтобы он учитывал данный флаг.




public void RegisterUser(string email, string secret, string password)
{
  if (_fail)
  {
    throw new Exception("RegisterUser method failed!");
  }

  //register user
  _userEmails.Add(email);
}

* This source code was highlighted with Source Code Highlighter.




Результат запуска теста будет следующим,




TestCase 'Company.Product.BLL.Tests.RegistrationTests.RegisterFailed'
failed: System.Exception : RegisterUser method failed!

* This source code was highlighted with Source Code Highlighter.




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





Заключим все обращения к DAL в try/catch блок. И если исключение произошло, сообщим об этом в отображение.




public void RegisterUser(string email, string secret, string password)
{
  try
  {
    if (_data.IsUserExists(email))
    {
      _view.Fail("Sorry, but user with same e-mail is already registered on site");
      return;
    }

    _data.RegisterUser(email, secret, password);
    _view.Success();
  }
  catch (Exception)
  {
    _view.Fail("Sorry, but unexcpected exception happend during operation. Please try to register later");
  }
}

* This source code was highlighted with Source Code Highlighter.




Самое время перезапустить все тесты, дабы убедится что ничего не сломано предыдущимим изменениями.




------ Test started: Assembly: Company.Product.BLL.Tests.dll ------


4 passed, 0 failed, 0 skipped, took 1,58 seconds (NUnit 2.4).

* This source code was highlighted with Source Code Highlighter.



Заключение


Посмотрим назад и оценим проделанную работу.





Интерфейсы отображения и данных:




namespace Company.Product.DAL
{
  public interface IRegistrationData
  {
    bool IsUserExists(string email);
    void RegisterUser(string email, string secret, string password);
  }
}

namespace Company.Projects.Views
{
  public interface IRegistrationView
  {
    void Success();
    void Fail(string p);
  }
}

* This source code was highlighted with Source Code Highlighter.




Код бизнес объекта:




namespace Company.Product.BLL
{
  public class Registration
  {
    private IRegistrationView _view;
    private IRegistrationData _data;

    public Registration(IRegistrationView view, IRegistrationData data)
    {
      _view = view;
      _data = data;
    }

    public void RegisterUser(string email, string secret, string password)
    {
      try
      {
        if (_data.IsUserExists(email))
        {
          _view.Fail("Sorry, but user with same e-mail is already registered on site");
          return;
        }

        _data.RegisterUser(email, secret, password);
        _view.Success();
      }
      catch (Exception)
      {
        _view.Fail("Sorry, but unexcpected exception happend during operation. Please try to register later");
      }
    }
  }
}

* This source code was highlighted with Source Code Highlighter.




Мы прошли путь от бизнес требований, к реализации бизнес объекта, через тестирование. Именно бизнес объект стоял в центре разработки, и его нужды определяют конкретные интерфейсы IView и IData. Мы с состоянии протестировать бизнес объект даже не задумываясь как конкретно будет реализован DAL или View, но теперь мы точно знаем что именно необходимо получить от этих объектов.





В следующей статье мы продолжим реализацию, и создадим реальный DAL класс, реализующий интрефейс, требуемый бизнес объектом.



Web development: cтруктура приложения ASP.NET

Вступительная статья в серии общих рекомендаций при построении веб приложений на ASP.net (C#).

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


Организация исходного кода проекта

WebAppilcation - стандартное ASP.net приложение. При разработке следует старатся делать веб приложение, как можно "тоньше". По сути это View, поэтому не должно быть ничего кроме:
- разметки страниц - клиентского java script кода - кастомных контролов

Код объекта страницы должен делегировать вызов к соответвующему объекту BLL. Допускается также выполнение минимальной серверной валидации на странице.


Company.Product.BLL - библиотека классов, содержащая в себе реализацию бизнес логики приложения. Все функции, которые реализует приложение содержатся именно здесь. Добавлнение новой функции в продукт, это добаление нового (или модификация существующего) класса БЛ, тестирование этого класса и последующая интеграция на соответсвующую веб страницу.


Company.Product.BLL.Tests - библиотека классов, содержащая в себе реализацию тестов BLL. Так как все классы BLL проектируются таким образом, что взаимодействие с View и Data Model происходят на уровне интерфейсов (конструктор бизнесс объекта получает объекты View и DataModel, через соответвующие интерфейсы), то внутри тестов удобно использовать mock-объекты. Это позволяет реализовать и протестировать бизнес объект, еще до того, как мы имеем саму страницу (View) и также класс модели данных (Data Model). Более того, при таком подходе именно бизнес объект диктует, каким именно должен быть интерфейс View и Data Model чтобы отвечать поставленным требованиям. Идя по пути


Требования - Тесты - Бизнес объект - Данные - Отображение


мы приходите к работающей реализации наболее коротким путем.


Company.Product.DAL - библиотека классов, содержащая реализацию DAL. Именно она содержит в себе классы Data Model (упомянутые выше), которые используют классы BLL. Я предпочитаю выносить DAL в отдельную сборку, подчеркивая требования паттерна MVC о разделении - модели, отображения и контролера, а также для удобства в работе (над DAL-ом может работать разработчик, в не зависимости от остальных частей приложения, реализовывая интерфейсы требующиеся бизнес объектом).


Company.Product.DAL.Tests - библиотека классов, содержащая в себе реализацию тестов классов DAL. При тестировании DAL желательно избегать mock'ов, а тестировать на той среде, где будет выполянятся приложение (SQL server, Web service, .NET remoting etc). В противном случае есть риск привращения тестирования в полный фейк. Держать тесты DAL отдельно от тестов BLL также удобно, так как, как правило, время работы тестов DAL намного привышает BLL, а перезапускать тесты BLL приходится чаще.


Company.Product.Views - библиотека классов, содержащая интрефейсы отображений, использующихся в приложении. Так как реализовывать отображения будут конкретные станицы или контролы из WebApplication, ничего кроме декларации интерфейсов, в этом проекте не будет.


WebApplication.Tests - библиотека классов содержащая тесты страниц веб приложения. Наверное самая главная тестовая сборка. Именно она ответсвенна за интеграционное тестирование сайта. Существуют 2 типа тестирования:
- Web Server based - Web Server less

Web Server based, более сложные требуещие запущенного IIS, отправляющие реальные HTTP запросы страницы и проверяющиее респонс. Довольно тяжеловесные, но наиболее приближенные к реальному условию использования. Web Server less, способ тестирования без веб сервера (я описал его в одной их прошлых статей), простой и удобный, но больше отдаленный от реальности.


Связи между проектами.

Теперь необходимо установить правильные ссылки (references) между проектами. Символ "->" будет означать "имеет ссылку на".

Company.Product.BLL.Tests -> Company.Product.BLL
Company.Product.DAL.Tests -> Company.Product.DAL
Company.Product.BLL -> Company.Product.DAL, Company.Product.Views
WebApplication.Tests -> WebAppilcation
WebAppilcation -> Company.Product.BLL, Company.Product.DAL, Company.Product.Views

* This source code was highlighted with Source Code Highlighter.

Это скелет, на основании которого, можно строить почти любое data-driven приложение, используя ASP.net в качестве фрейморка, MVC как основопологающий паттерн и TDD как методику разработки.


Refactoring: DAL и рефакторинг кода

Data Access Layer - уровень доступа к данным. DAL это абстракция, определяющая операции по работе с данными (часто опеределяемые как CRUD) и скрывает за собой непосредсвенные детали того, как эти операции осуществляются. Наиболее частым источником данных выступает SQL сервер, поэтому методы DAL реализуют SQL запросы к серверу (select, insert, update, delete). В последнее время web-services также часто выступают как источники данных.

При проектировании приложения DAL необходимо уделять самое пристальное внимание. В конце концов любой задача любого софта это - получить данные, обработать их и предоставить на их основе результат. В Visual Studio встаиваются средства, для облегчения создания DAL. Один из них Microsoft Entity Framework. Рекомендую с ним ознакомиться.

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

useDimension = a.useDimension(ctx.Econ.GetAftaleNrFraOpsaetning(ctx, virkOpsaetning));

sql.Clear();
sql.Append("UPDATE ArdRapport SET Periode1Start=").AddVal(periode1Start).Append(", Periode1Slut=").AddVal(periode1End).Append(", Periode2Start=").AddVal(periode2Start);
sql.Append(", Periode2Slut=").AddVal(periode2End);
sql.Append(" WHERE Opsaetning=").AddVal(usedOpsaetning).Append(" AND AdminAftale=").AddVal(adminaftale).Append(" AND Rapportnr=").AddVal(usedReport);
ctx.Econ.Db.ExecUpdate(ctx, sql);

sql.Clear();
sql.Append("UPDATE ArdRapportVirksomhed SET RegnskabsAar=").AddVal(accountingyear);
sql.Append(" WHERE Opsaetning=").AddVal(usedOpsaetning).Append(" AND AdminAftale=").AddVal(adminaftale);
sql.Append(" AND Rapport=").AddVal(usedReport).Append(" AND Id=").AddVal(iCompany);
ctx.Econ.Db.ExecUpdate(ctx, sql);


sql.Clear();
sql.Append("SELECT VirksomhedsAftale FROM ArdRapportVirksomhed");
sql.Append(" WHERE AdminAftale=").AddVal(adminaftale);
sql.Append(" AND Rapport=").AddVal(usedReport);
sql.Append(" AND Opsaetning=").AddVal(usedOpsaetning);
sql.Append(" AND Id=").AddVal(iCompany);

* This source code was highlighted with Source Code Highlighter.

Какие основные негативные моменты связанны с использованием SQL запросов прямо со страницы.

  1. Код становился абсолютно не читаем. SQL команды рябили в глазах, и страница превращалась в один большой SQL скрипт.
  2. Поддержка такого кода очень усложнялась. Особенно там, где SQL запросы использовались в циклах, разобрать и что-то исправить было очень сложно.
  3. Re-use SQL был не использовался, в результате чего, одни и теже кусочки просто копировались в нужное место, что приводило к дуплицированию кода.
  4. Страница получалась жестко связанна с источником данных, что затрудняло ее юнит тестирование (в некоторых случаях даже делала ее невозможной).

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

После этого SQL код можно вынести в DAL классы. DAL класс, это самый обыкновенный класс, в методах которого идет непосредвенное обращение к источнику данных. Выглядит это примерно следующим образом,

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Company.AnnualReport.DAL
{
  public class ClosingSheetData
  {
    private readonly ASPNetContext _context;

    public ClosingSheetData(ASPNetContext context)
    {
      _context = context;
    }

    public IResultSet GetCompanyAgreement(int agreementNumber, int companyId, int report, int setup, int template)
    {
      var sql = new Sql();
      sql.Append("SELECT VirksomhedsAftale FROM ArdRapportVirksomhed WHERE AdminAftale=").AddVal(agreementNumber);
      sql.Append(" AND id=").AddVal(companyId).Append(" AND Rapport=").AddVal(report);
      sql.Append(" AND Opsaetning=").AddVal(setup).Append(" AND Skabelon=").AddVal(template);
      IResultSet result = _context.Econ.Db.ExecQuery(_context, sql);

      return result;
    }

    public void UpdatePeriods(DateTime? periode1Start, DateTime? periode1End, DateTime? periode2Start, DateTime? periode2End, int setup, int adminAgreement, int report)
    {
      var sql = new Sql();
      sql.Append("UPDATE ArdRapport SET Periode1Start=").AddVal(periode1Start).Append(", Periode1Slut=").AddVal(periode1End).Append(", Periode2Start=").AddVal(periode2Start);
      sql.Append(", Periode2Slut=").AddVal(periode2End);
      sql.Append(" WHERE Opsaetning=").AddVal(setup).Append(" AND AdminAftale=").AddVal(adminAgreement).Append(" AND Rapportnr=").AddVal(report);
      _context.Econ.Db.ExecUpdate(_context, sql); 
    }

* This source code was highlighted with Source Code Highlighter.

Т.е., внутри методов мы инкапсулируем весь SQL код, возвращая результаты (для select запросов) или имея void методы (для insert, update, delete).

В результате переноса всего SQL кода в DAL класс, его надо интегрировать в страницу.

private ClosingSheetData _dataAccessor;

* This source code was highlighted with Source Code Highlighter.

В методе Page_Load, мы просто инициализируем его новым объектом:

  /// <summary>
  /// Page entry point
  /// </summary>
  /// <param name="sender"></param>
  /// <param name="e"></param>
  protected void Page_Load(object sender, EventArgs e)
  {
    //...
    _dataAccessor = new ClosingSheetData(_context);
    //...

* This source code was highlighted with Source Code Highlighter.

Но в тестовом методе, лучше передавать его как аргумент.

  /// <summary>
  /// Page entry point (used for testing)
  /// </summary>
  /// <param name="ctx">Context</param>
  /// <param name="controls">Dictionary of page controls</param>
  /// <param name="data">Page data class</param>
  /// <param name="sender">Sender object</param>
  /// <param name="e">Arguments</param>
  /// <param name="postback">Postback flag</param>
  public void _Page_Load(ASPNetContext ctx, IDictionary<string, object> controls, ClosingSheetData data, object sender, EventArgs e, bool postback)
  {
      // ...
     _dataAccessor = data;

* This source code was highlighted with Source Code Highlighter.

Код самой страницы, необходимо поменять, дабы вместо прямых обращений к базе мы делегировали вызов к _dataAccessor объекту.

_useDimension = _reportDesigner.useDimension(_context.Econ.GetAftaleNrFraOpsaetning(_context, _virkOpsaetning));

_dataAccessor.UpdatePeriods(periodeStart, periodeEnd, null, null, setupId, adminAgreement, reportId);
_dataAccessor.UpdateFinancialYear(accountingyear, setupId, adminAgreement, reportId, companyId);
_dataAccessor.UpdateReportClosingSheetColumns(templateAdmin, templateId, setupId, adminAgreement, companyId, reportId);

* This source code was highlighted with Source Code Highlighter.

По моему так гораздо лучше.

Что достигается таким подходом?

  1. Код проще читать и понимать.
  2. Весь SQL находится в одном классе и его проще контролировать.
  3. Re-use кода также увеличился, так как повторно можно вызывать одни и теже методы.
  4. Тестировать страницы можно гораздо проще, так как имея возможность передачи data-класса внутрь страницы, в режиме запуска тестов его можно подменить mock-классом тем самым полностью отвязать страницу от базы данных.

Рекомендую применять всегда, даже в не больших приложениях.

Отдельно стоит отметить тестирование самого DAL. Как всегда, в выборе между тестировать и не тестировать, должен выбиратся только первый вариант. Но тут есть свои сложности. - тесты зависят от состояния базы данных, перед тестами это состояние нужно готовить, после тестов очищать, поддержка тразакционности тестов, изоляция тестов друг от друга, перформанс и т.д. Все эти аспекты нужно принимать к сведению, начиная тестирования DAL. Пусть эти сложности не останавливают вас от благих намерений...и как и везде необходимо идти про принципу самого простого пути, постепенно усложная фреймворк для тестирования DAL. Так в моем случае, мы просто делаем DbSetup классы, которые создают необходимую структуру данных а также могут ее очищать.

    #region setup and teardown
    [SetUp]
    public void SetUp()
    {
      //...
      _dbSetup = new R1UnitTest.Tests.ClosingSheetTests.DbSetup(_ctx);
      _dbSetup.Init();
      //...
    }

    [TearDown]
    public void TearDown()
    {
      //...
      _dbSetup.Clean();
      //...
    }
    #endregion setup and teardown

* This source code was highlighted with Source Code Highlighter.

Для более детального рассмотрения аспектов тестирования, и вообще для вдохновления, рекомендую вот эти статьи

http://msdn.microsoft.com/en-us/magazine/cc163772.aspx
http://www.4guysfromrolla.com/articles/040605-1.aspx