Подтвердить что ты не робот

С# - Компоновка объекта - Удаление кода котельной

Контекст/вопрос

Я работал над многочисленными .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?

4b9b3361

Ответ 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;
    }
}

Таким образом, вы можете применить разделение проблем между логикой домена и инфраструктурой, применяя посетитель-пэттер для стойкости. Привет!