ORM Паттерны Repository Repository (хранилище) ― выступает в роли посредника между слоем домена и слоем отображения данных, предоставляя интерфейс в виде коллекции для доступа к объектам домена. Пример Обратимся к упомянутому ранее примеру на NHibernate. Пусть у нас определѐн класс автора: public class Author { public virtual int Id { get; set; } public virtual string FirstName { get; set; } public virtual string LastName { get; set; } public virtual int YearOfBirth { get; set; } public virtual Iesi.Collections.Generic.ISet<Book> Books { get; set; } } Пример Файл, который отображает класс автора на таблицу БД: <class name="Books.Domain.Author, NHibernateDemo" table="Author"> <id name="Id" type="System.Int32" > <column name="Id" not-null="true" /> <generator class="identity"/> </id> <property name="FirstName" column="FirstName"/> <property name="LastName" column="LastName"/> <property name="YearOfBirth" type="System.Int32" column="YearOfBirth"/> <set name="Books" table="BookAuthor"> <key column="AuthorId"></key> <many-to-many class ="Books.Domain.Book, NHibernateDemo" column="BookId"></many-to-many> </set> </class> Пример Пусть нам необходимо заполнить выпадающий список именами авторов, скажем, для того чтобы впоследствии вывести список всех его работ: public void FillAuthorsComboBox(ComboBox comboBox, ISessionFactory factory) { ISession session = factory.OpenSession(); try { IQuery authorsQuery = session.CreateQuery("FROM Author"); IList<Author> authors = authorsQuery.List<Author>(); foreach (Author author in authors) comboBox.Items.Add(author.LastName + ", " + author.FirstName); } finally { session.Close(); } } Пример Пусть одной из функций нашего приложения является вывод информации о всех авторах в формате HTML: public string GetAllAuthorsAsHTML(ISessionFactory factory) { ISession session = factory.OpenSession(); try { IQuery authorsQuery = session.CreateQuery("FROM Author"); IList<Author> authors = authorsQuery.List<Author>(); StringBuilder result = new StringBuilder(); result.Append("<HTML>").Append("<BODY>"); foreach (Author author in authors) { result.Append("<h2>").Append(author.LastName + ", " + author.FirstName) .Append("</h2>"); result.Append("<p>Year of birth: ").Append(author.YearOfBirth.ToString()) .Append("</p>"); } result.Append("</BODY>").Append("</HTML>"); return result.ToString(); } finally { session.Close(); } } Пример Недостатки использованного подхода: неоправданное дублирование; зависимость от конкретной реализации ORM; непрозрачность кода; невозможность протестировать код. Схема Пример Определим класс Repository: public class Repository { private ISession session; public Repository(ISession session) { this.session = session; } public IEnumerable<Author> GetAllAuthors() { IQuery authorsQuery = session.CreateQuery("FROM Author"); return authorsQuery.List<Author>(); } } Пример Теперь функции, которые работают со списком авторов, можно переписать следующим образом: public void FillAuthorsComboBox( ComboBox comboBox, Repository repository) { IEnumerable<Author> authors = repository.GetAllAuthors(); foreach (Author author in authors) comboBox.Items.Add( author.LastName + ", " + author.FirstName); } Пример Функция экспорта в HTML преобразуется следующим образом: public string GetAllAuthorsAsHTML(Repository repository) { IEnumerable<Author> authors = repository.GetAllAuthors(); StringBuilder result = new StringBuilder(); result.Append("<HTML>").Append("<BODY>"); foreach (Author author in authors) { result.Append("<h2>") .Append(author.LastName + ", " + author.FirstName) .Append("</h2>"); result.Append("<p>Year of birth: ") .Append(author.YearOfBirth.ToString()) .Append("</p>"); } result.Append("</BODY>").Append("</HTML>"); return result.ToString(); } Плюсы Сокращение дублирования; прозрачность кода; возможность создания фиктивного хранилища для упрощения тестирования; скрытие деталей реализации. Specification Specification (спецификация) ― паттерн, который инкапсулирует логику отбора доменных объектов в отдельный объект. Specification Specification. Пример Предположим, что нам необходимо делать выборки авторов, удовлетворяющие разным критериям. Например, выбирать авторов, родившихся в определѐнный период или имя которых содержит заданное значение: public class Repository { public IEnumerable<Author> FindAuthors_BornBetween(int startYear, int endYear) { return session.QueryOver<Author>() .Where(a => a.YearOfBirth >= startYear && a.YearOfBirth <= endYear).List<Author>(); } public IEnumerable<Author> FindAuthors_NameContains(string value) { return session.QueryOver<Author>() .Where(a => a.FirstName.Contains(value)).List<Author>(); } } Интерфейс класса Repository может стать неоправданно большим. Кроме того, такая реализация нарушает принцип открытости/закрытости. Specification. Пример Решением является использование паттерна Спецификация. Рассмотрим пример применения данного паттерна: public interface ISpecification<T> { Expression<Func<T, bool>> IsSatisfiedBy(); } public class Repository { public IEnumerable<Author> FindAuthors( ISpecification<Author> specification) { return session.QueryOver<Author>() .Where(specification.IsSatisfiedBy()) .List<Author>(); } } Specification. Пример Класс, реализующий интерфейс спецификации и выполняющий проверку даты рождения на вхождение в определѐнный диапазон, будет выглядеть так: public class IsYearOfBirthInRange : ISpecification<Author> { private int endYear; private int startYear; public IsYearOfBirthInRange(int startYear, int endYear) { this.startYear = startYear; this.endYear = endYear; } public Expression<Func<Author, bool>> IsSatisfiedBy() { return author => author.YearOfBirth >= startYear && author.YearOfBirth <= endYear; } } Specification. Пример Класс, реализующий интерфейс спецификации и анализирующий имя автора, будет следующим: public class AuthorNameContains : ISpecification<Author> { private string value; public AuthorNameContains(string value) { this.value = value; } public Expression<Func<Author, bool>> IsSatisfiedBy() { return author => author.FirstName.Contains(value); } } Specification. Пример Рассмотрим теперь пример использования полученных классов. Выведем всех авторов, родившихся во второй половине XX века: public void DisplayAuthors(Repository repository) { IEnumerable<Author> authors = repository.FindAuthors(new IsYearOfBirthInRange(1950, 2000)); foreach (Author author in authors) Console.WriteLine(author.FirstName + " " + author.LastName); } Specification Для того чтобы сделать выборку, удовлетворяющую нескольким условиям, можно применить паттерны компоновщик и декоратор следующим образом: Specification Интерфейс спецификации следует расширить следующим образом: public interface ISpecification<T> { Expression<Func<T, bool>> IsSatisfiedBy(); ISpecification<T> Or(ISpecification<T> left); ISpecification<T> And(ISpecification<T> left); ISpecification<T> Not(); } Specification Класс составного условия будет следующим: public abstract class CompositeSpecification<T> : ISpecification<T> { public abstract Expression<Func<T, bool>> IsSatisfiedBy(); public ISpecification<T> Or(ISpecification<T> right) { return new OrSpecification<T>(this, right); } public ISpecification<T> And(ISpecification<T> right) { return new AndSpecification<T>(this, right); } public ISpecification<T> Not() { return new NotSpecification<T>(this); } } Specification Рассмотрим реализацию наследников упомянутого выше класса. Класс AndSpecification будет следующим: public class AndSpecification<T> : CompositeSpecification<T> { private ISpecification<T> left; private ISpecification<T> right; public AndSpecification(ISpecification<T> left, ISpecification<T> right) { this.left = left; this.right = right; } public override Expression<Func<T, bool>> IsSatisfiedBy() { return Expression.Lambda<Func<T, bool>>( Expression.And( left.IsSatisfiedBy(), right.IsSatisfiedBy())); } } Specification Класс OrSpecification: public class OrSpecification<T> : CompositeSpecification<T> { private ISpecification<T> left; private ISpecification<T> right; public OrSpecification(ISpecification<T> left, ISpecification<T> right) { this.left = left; this.right = right; } public override Expression<Func<T, bool>> IsSatisfiedBy() { return Expression.Lambda<Func<T, bool>>( Expression.Or( left.IsSatisfiedBy(), right.IsSatisfiedBy())); } } Specification Класс NotSpecification: public class NotSpecification<T> : CompositeSpecification<T> { private ISpecification<T> wrapped; public NotSpecification(ISpecification<T> wrapped) { this.wrapped = wrapped; } public override Expression<Func<T, bool>> IsSatisfiedBy() { return Expression.Lambda<Func<T, bool>>( Expression.Not(wrapped.IsSatisfiedBy())); } } Specification Теперь классы конкретных условий должны наследовать класс CompositeSpecification: public class IsYearOfBirthInRange : CompositeSpecification<Author> { private int endYear; private int startYear; public IsYearOfBirthInRange(int startYear, int endYear) { this.startYear = startYear; this.endYear = endYear; } public override Expression<Func<Author, bool>> IsSatisfiedBy() { return author => author.YearOfBirth >= startYear && author.YearOfBirth <= endYear; } } Specification Рассмотрим пример использования. Выберем всех авторов, которые родились не в первой половине XX века и имя которых содержит букву «А»: public static void DisplayAuthors(Repository repository) { ISpecification<Author> condition = ( (new IsYearOfBirthInRange(1950, 1999)).Not() ).And ( new AuthorNameContains("A") ); IEnumerable<Author> authors = repository.FindAuthors(condition); foreach (Author author in authors) Console.WriteLine(author.FirstName + " " + author.LastName); } Specification Такая возможность языка C# 3.0, как методы расширения позволяет реализовать подобную функциональность следующим образом: public static class SpecificationUtils { public static ISpecification<T> Or<T>(this ISpecification<T> left, ISpecification<T> right) { return new OrSpecification<T>(left, right); } public static ISpecification<T> And<T>(this ISpecification<T> left, ISpecification<T> right) { return new AndSpecification<T>(left, right); } public static ISpecification<T> Not<T>(this ISpecification<T> wrapped) { return new NotSpecification<T>(wrapped); } } Specification Класс AndSpecification будет выглядеть так: public class AndSpecification<T> : ISpecification<T> { private ISpecification<T> left; private ISpecification<T> right; public AndSpecification(ISpecification<T> left, ISpecification<T> right) { this.left = left; this.right = right; } public Expression<Func<T, bool>> IsSatisfiedBy() { return Expression.Lambda<Func<T, bool>>( Expression.And( left.IsSatisfiedBy(), right.IsSatisfiedBy())); } } Остальные подобные классы реализуются аналогично. Specification Клиентский код остаѐтся без изменений: public static void DisplayAuthors(Repository repository) { ISpecification<Author> condition = ( (new IsYearOfBirthInRange(1950, 1999)).Not() ).And ( new AuthorNameContains("A") ); IEnumerable<Author> authors = repository.FindAuthors(condition); foreach (Author author in authors) Console.WriteLine(author.FirstName + " " + author.LastName); }