С# - Компоновка объекта - Удаление кода котельной
Контекст/вопрос
Я работал над многочисленными .NET-проектами, которые требовались для сохранения данных и обычно заканчивались использованием Repository pattern, Кто-нибудь знает о хорошей стратегии для удаления большого кода шаблонов без ущерба для масштабирования базы кода?
Стратегия наследования
Так как большая часть кода репозитория является плитой котла, и ее необходимо повторить, я обычно создаю базовый класс для покрытия основ, таких как обработка исключений, регистрация и поддержка транзакций, а также несколько базовых методов CRUD:
public abstract class BaseRepository<T> where T : IEntity
{
protected void ExecuteQuery(Action query)
{
//Do Transaction Support / Error Handling / Logging
query();
}
//CRUD Methods:
public virtual T GetByID(int id){}
public virtual IEnumerable<T> GetAll(int id){}
public virtual void Add (T Entity){}
public virtual void Update(T Entity){}
public virtual void Delete(T Entity){}
}
Итак, это хорошо работает, когда у меня есть простой домен, я могу быстро создать класс DRY repository для каждого объекта. Однако это начинает разрушаться, когда домен становится более сложным. Допустим, добавлен новый объект, который не позволяет обновлять. Я могу разбить базовые классы и перенести метод Update в другой класс:
public abstract class BaseRepositorySimple<T> where T : IEntity
{
protected void ExecuteQuery(Action query);
public virtual T GetByID(int id){}
public virtual IEnumerable<T> GetAll(int id){}
public virtual void Add (T entity){}
public void Delete(T entity){}
}
public abstract class BaseRepositoryWithUpdate<T> :
BaseRepositorySimple<T> where T : IEntity
{
public virtual void Update(T entity){}
}
Это решение плохо масштабируется. Скажем, у меня есть несколько Сущностей, которые имеют общий метод: public virtual void Archive (T entity) {}
но некоторые объекты, которые могут быть архивированы, также могут быть обновлены, а другие не могут. Поэтому решение My Inheritance ломается, мне нужно создать два новых базовых класса для решения этого сценария.
Стратегия Compoisition
Я исследовал шаблон Compositon, но это, похоже, оставляет много кода плиты котла:
public class MyEntityRepository : IGetByID<MyEntity>, IArchive<MyEntity>
{
private Archiver<MyEntity> _archiveWrapper;
private GetByIDRetriever<MyEntity> _getByIDWrapper;
public MyEntityRepository()
{
//initialize wrappers (or pull them in
//using Constructor Injection and DI)
}
public MyEntity GetByID(int id)
{
return _getByIDWrapper(id).GetByID(id);
}
public void Archive(MyEntity entity)
{
_archiveWrapper.Archive(entity)'
}
}
Теперь MyEntityRepository загружается с шаблоном. Есть ли инструмент/шаблон, который я могу использовать для автоматического создания этого?
Если бы я мог превратить MyEntityRepository во что-то подобное, я думаю, что это было бы идеальным:
[Implement(Interface=typeof(IGetByID<MyEntity>),
Using = GetByIDRetriever<MyEntity>)]
[Implement(Interface=typeof(IArchive<MyEntity>),
Using = Archiver<MyEntity>)
public class MyEntityRepository
{
public MyEntityRepository()
{
//initialize wrappers (or pull them in
//using Constructor Injection and DI)
}
}
Аспектно-ориентированное программирование
Я изучил использование рамки AOP для этого, в частности PostSharp и их "Аспект композиции" , который выглядит так, будто он должен делать трюк, но для использования репозитория мне придется вызвать Post.Cast < > (), что добавляет очень странный запах к код. Кто-нибудь знает, есть ли лучший способ использовать АОП, чтобы избавиться от шаблона шаблона композитора?
Генератор пользовательских кодов
Если все остальное не работает, я полагаю, что я мог бы работать над созданием подключаемого модуля Visual Studio Generator Visual Studio, который мог бы генерировать код плиты котла в файл частичного кода. Есть ли там инструмент, который бы сделал это?
[Implement(Interface=typeof(IGetByID<MyEntity>),
Using = GetByIDRetriever<MyEntity>)]
[Implement(Interface=typeof(IArchive<MyEntity>),
Using = Archiver<MyEntity>)
public partial class MyEntityRepository
{
public MyEntityRepository()
{
//initialize wrappers (or pull them in
//using Constructor Injection and DI)
}
}
//Generated Class file
public partial class MyEntityRepository : IGetByID<MyEntity>, IArchive<MyEntity>
{
private Archiver<MyEntity> _archiveWrapper;
private GetByIDRetriever<MyEntity> _getByIDWrapper;
public MyEntity GetByID(int id)
{
return _getByIDWrapper(id).GetByID(id);
}
public void Archive(MyEntity entity)
{
_archiveWrapper.Archive(entity)'
}
}
Методы расширения
Забыл добавить это, когда я изначально написал вопрос (извините). Я также попытался экспериментировать с методами расширения:
public static class GetByIDExtenions
{
public T GetByID<T>(this IGetByID<T> repository, int id){ }
}
Однако у этого есть две проблемы: a) мне нужно будет запомнить пространство имен класса методов расширения и добавить его повсюду, а б) методы расширения не могут удовлетворять зависимостям интерфейса:
public interface IMyEntityRepository : IGetByID<MyEntity>{}
public class MyEntityRepository : IMyEntityRepository{}
Обновление: возможно ли T4 Templates?
Ответы
Ответ 1
У меня есть один общий интерфейс репозитория, который реализуется только один раз для определенного хранилища данных. Вот он:
public interface IRepository<T> where T : class
{
IQueryable<T> GetAll();
T Get(object id);
void Save(T item);
void Delete(T item);
}
У меня есть его реализация для хранилищ EntityFramework, NHibernate, RavenDB. Кроме того, у меня есть встроенная память для модульного тестирования.
Например, вот часть хранилища на основе коллекции:
public class InMemoryRepository<T> : IRepository<T> where T : class
{
protected readonly List<T> _list = new List<T>();
public virtual IQueryable<T> GetAll()
{
return _list.AsReadOnly().AsQueryable();
}
public virtual T Get(object id)
{
return _list.FirstOrDefault(x => GetId(x).Equals(id));
}
public virtual void Save(T item)
{
if (_list.Any(x => EqualsById(x, item)))
{
Delete(item);
}
_list.Add(item);
}
public virtual void Delete(T item)
{
var itemInRepo = _list.FirstOrDefault(x => EqualsById(x, item));
if (itemInRepo != null)
{
_list.Remove(itemInRepo);
}
}
}
Общий интерфейс репозитория освобождает меня от создания множества подобных классов. У вас есть только одна общая реализация репозитория, но также и свобода в запросах.
IQueryable<T>
Результат из метода GetAll()
позволяет мне делать любые запросы, которые я хочу с данными, и отделять их от кода, относящегося к хранилищу. Все популярные .NET ORM имеют свои собственные поставщики LINQ, и все они должны иметь этот магический метод GetAll()
, поэтому никаких проблем здесь нет.
Я определяю реализацию репозитория в корне композиции с использованием контейнера IoC:
ioc.Bind(typeof (IRepository<>)).To(typeof (RavenDbRepository<>));
В тестах я использую замену в памяти:
ioc.Bind(typeof (IRepository<>)).To(typeof (InMemoryRepository<>));
Если я хочу добавить к репозиторию больше бизнес-запросов, я добавлю метод расширения (похожий на ваш метод расширения в ответе):
public static class ShopQueries
{
public IQueryable<Product> SelectVegetables(this IQueryable<Product> query)
{
return query.Where(x => x.Type == "Vegetable");
}
public IQueryable<Product> FreshOnly(this IQueryable<Product> query)
{
return query.Where(x => x.PackTime >= DateTime.Now.AddDays(-1));
}
}
Таким образом, вы можете использовать и смешивать эти методы в запросах уровня бизнес-логики, сохраняя возможность тестирования и легкость реализации репозитория, например:
var freshVegetables = repo.GetAll().SelectVegetables().FreshOnly();
Если вы не хотите использовать другое пространство имен для этих методов расширения (например, я) - ok, поместите их в то же пространство имен, где выполняется реализация репозитория (например, MyProject.Data
) или, что еще лучше, для некоторых существующих бизнес-пространство имен (например, MyProject.Products
или MyProject.Data.Products
). Теперь не нужно запоминать дополнительные пространства имен.
Если у вас есть определенная логика репозитория для каких-то сущностей, создайте производный класс репозитория, переопределяющий метод, который вы хотите. Например, если продукты могут быть найдены только ProductNumber
вместо Id
и не поддерживают удаление, вы можете создать этот класс:
public class ProductRepository : RavenDbRepository<Product>
{
public override Product Get(object id)
{
return GetAll().FirstOrDefault(x => x.ProductNumber == id);
}
public override Delete(Product item)
{
throw new NotSupportedException("Products can't be deleted from db");
}
}
И заставить IoC вернуть эту конкретную реализацию репозитория для продуктов:
ioc.Bind(typeof (IRepository<>)).To(typeof (RavenDbRepository<>));
ioc.Bind<IRepository<Product>>().To<ProductRepository>();
Что я оставляю в кучу с моими репозиториями;)
Ответ 2
Оформить заказ T4 Файлы для генерации кода. T4 встроен в Visual Studio. См. учебник здесь.
Я создал файлы T4 для кода, генерирующего объекты POCO, путем проверки LINQ DBML и для их репозиториев, я думаю, что это послужит вам хорошо здесь. Если вы создаете частичные классы с вашим T4 файлом, вы можете просто написать код для особых случаев.
Ответ 3
Для меня кажется, что вы делите базовые классы, а затем хотите, чтобы функциональность от них обоих в одном классе-наследовании. В этом случае композиция - это выбор. Множественное наследование классов было бы неплохо, если бы С# поддерживал его. Однако, поскольку я чувствую, что наследование более приятное, а повторное использование все еще прекрасное, мой первый вариант выбора будет с ним.
Вариант 1
Я предпочел бы иметь еще один базовый класс вместо состава двух. Повторное использование может быть решено также статическими методами, а не наследованием:
Повторяемая часть не видна снаружи. Не нужно запоминать пространство имен.
static class Commons
{
internal static void Update(/*receive all necessary params*/)
{
/*execute and return result*/
}
internal static void Archive(/*receive all necessary params*/)
{
/*execute and return result*/
}
}
class Basic
{
public void SelectAll() { Console.WriteLine("SelectAll"); }
}
class ChildWithUpdate : Basic
{
public void Update() { Commons.Update(); }
}
class ChildWithArchive : Basic
{
public void Archive() { Commons.Archive(); }
}
class ChildWithUpdateAndArchive: Basic
{
public void Update() { Commons.Update(); }
public void Archive() { Commons.Archive(); }
}
Конечно, есть незначительный повторный код, но это просто вызов готовых функций из общей библиотеки.
Вариант 2
Моя реализация композиции (или имитация множественного наследования):
public class Composite<TFirst, TSecond>
{
private TFirst _first;
private TSecond _second;
public Composite(TFirst first, TSecond second)
{
_first = first;
_second = second;
}
public static implicit operator TFirst(Composite<TFirst, TSecond> @this)
{
return @this._first;
}
public static implicit operator TSecond(Composite<TFirst, TSecond> @this)
{
return @this._second;
}
public bool Implements<T>()
{
var tType = typeof(T);
return tType == typeof(TFirst) || tType == typeof(TSecond);
}
}
Наследование и состав (ниже):
class Basic
{
public void SelectAll() { Console.WriteLine("SelectAll"); }
}
class ChildWithUpdate : Basic
{
public void Update() { Console.WriteLine("Update"); }
}
class ChildWithArchive : Basic
{
public void Archive() { Console.WriteLine("Archive"); }
}
Состав. Не уверен, что этого достаточно, чтобы сказать, что никакого шаблона кода не существует.
class ChildWithUpdateAndArchive : Composite<ChildWithUpdate, ChildWithArchive>
{
public ChildWithUpdateAndArchive(ChildWithUpdate cwu, ChildWithArchive cwa)
: base(cwu, cwa)
{
}
}
Код, использующий все это, выглядит как ОК, но все же необычный (невидимый) тип приведения в присваивания. Это отплата за меньший код шаблона:
ChildWithUpdate b;
ChildWithArchive c;
ChildWithUpdateAndArchive d;
d = new ChildWithUpdateAndArchive(new ChildWithUpdate(), new ChildWithArchive());
//now call separated methods.
b = d;
b.Update();
c = d;
c.Archive();
Ответ 4
Вот моя версия:
interface IGetById
{
T GetById<T>(object id);
}
interface IGetAll
{
IEnumerable<T> GetAll<T>();
}
interface ISave
{
void Save<T>(T item) where T : IHasId; //you can go with Save<T>(object id, T item) if you want pure pure POCOs
}
interface IDelete
{
void Delete<T>(object id);
}
interface IHasId
{
object Id { get; set; }
}
Мне не нравится общий интерфейс репозитория, поскольку он создает дополнительные ограничения и затрудняет работу с ним позже. Вместо этого я использую общие методы.
Вместо использования интерфейса заголовка для репозитория я использую интерфейсы ролей для каждого метода репозитория. Это позволяет мне добавлять дополнительные функции к методам репозитория, такие как ведение журнала, публикация изменений в PubSub и т.д.
Я не использую репозиторий для пользовательских запросов, поскольку я еще не нашел никакой хорошей и простой абзацции запросов, которая бы соответствовала любой базе данных. Моя версия репозитория может получить только элемент по id или получить все элементы одного типа. Другие запросы выполняются в памяти (если производительность достаточно хорошая), или у меня есть другой механизм.
Для удобства может быть введен интерфейс IRepository, поэтому вам не придется постоянно писать 4 интерфейса для чего-то вроде crud-контроллеров
interface IRepository : IGetById, IGetAll, ISave, IDelete { }
class Repository : IRepository
{
private readonly IGetById getter;
private readonly IGetAll allGetter;
private readonly ISave saver;
private readonly IDelete deleter;
public Repository(IGetById getter, IGetAll allGetter, ISave saver, IDelete deleter)
{
this.getter = getter;
this.allGetter = allGetter;
this.saver = saver;
this.deleter = deleter;
}
public T GetById<T>(object id)
{
return getter.GetById<T>(id);
}
public IEnumerable<T> GetAll<T>()
{
return allGetter.GetAll<T>();
}
public void Save<T>(T item) where T : IHasId
{
saver.Save(item);
}
public void Delete<T>(object id)
{
deleter.Delete<T>(id);
}
}
Я упомянул, что с ролевыми интерфейсами я могу добавить дополнительное поведение, вот пара примеров с использованием декораторов
class LogSaving : ISave
{
private readonly ILog logger;
private readonly ISave next;
public LogSaving(ILog logger, ISave next)
{
this.logger = logger;
this.next = next;
}
public void Save<T>(T item) where T : IHasId
{
this.logger.Info(string.Format("Start saving {0} : {1}", item.ToJson()));
next.Save(item);
this.logger.Info(string.Format("Finished saving {0}", item.Id));
}
}
class PublishChanges : ISave, IDelete
{
private readonly IPublish publisher;
private readonly ISave nextSave;
private readonly IDelete nextDelete;
private readonly IGetById getter;
public PublishChanges(IPublish publisher, ISave nextSave, IDelete nextDelete, IGetById getter)
{
this.publisher = publisher;
this.nextSave = nextSave;
this.nextDelete = nextDelete;
this.getter = getter;
}
public void Save<T>(T item) where T : IHasId
{
nextSave.Save(item);
publisher.PublishSave(item);
}
public void Delete<T>(object id)
{
var item = getter.GetById<T>(id);
nextDelete.Delete<T>(id);
publisher.PublishDelete(item);
}
}
Это не сложно реализовать в хранилище памяти для тестирования
class InMemoryStore : IRepository
{
private readonly IDictionary<Type, Dictionary<object, object>> db;
public InMemoryStore(IDictionary<Type, Dictionary<object, object>> db)
{
this.db = db;
}
...
}
Наконец, все вместе
var db = new Dictionary<Type, Dictionary<object, object>>();
var store = new InMemoryStore(db);
var storePublish = new PublishChanges(new Publisher(...), store, store, store);
var logSavePublish = new LogSaving(new Logger(), storePublish);
var repo = new Repository(store, store, logSavePublish, storePublish);
Ответ 5
Вы можете использовать шаблон посетителя, прочитать реализацию здесь, чтобы вы могли реализовать только необходимые функции.
Вот идея:
public class Customer : IAcceptVisitor
{
private readonly string _id;
private readonly List<string> _items = new List<string>();
public Customer(string id)
{
_id = id;
}
public void AddItems(string item)
{
if (item == null) throw new ArgumentNullException(nameof(item));
if(_items.Contains(item)) throw new InvalidOperationException();
_items.Add(item);
}
public void Accept(ICustomerVisitor visitor)
{
if (visitor == null) throw new ArgumentNullException(nameof(visitor));
visitor.VisitCustomer(_items);
}
}
public interface IAcceptVisitor
{
void Accept(ICustomerVisitor visitor);
}
public interface ICustomerVisitor
{
void VisitCustomer(List<string> items);
}
public class PersistanceCustomerItemsVisitor : ICustomerVisitor
{
public int Count { get; set; }
public List<string> Items { get; set; }
public void VisitCustomer(List<string> items)
{
if (items == null) throw new ArgumentNullException(nameof(items));
Count = items.Count;
Items = items;
}
}
Таким образом, вы можете применить разделение проблем между логикой домена и инфраструктурой, применяя посетитель-пэттер для стойкости.
Привет!