C# - Object Composition - Removing Boilerplate Code

空扰寡人 提交于 2019-11-29 20:10:25

I have a single generic repository interface, which is implemented only once for a particular data storage. Here it is:

public interface IRepository<T> where T : class
{
    IQueryable<T> GetAll();
    T Get(object id);
    void Save(T item);
    void Delete(T item);
}

I have implementations of it for EntityFramework, NHibernate, RavenDB storages. Also I have an in-memory implementation for unit testing.

For example, here is a part of the in-memory collection-based repository:

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);
        }
    }
}

Generic repository interface frees me from creating lot's of similar classes. You have only one generic repository implementation, but also freedom in querying.

IQueryable<T> result from GetAll() method allows me to make any queries I want with the data, and separate them from the storage-specific code. All popular .NET ORMs have their own LINQ providers, and they all should have that magic GetAll() method - so no problems here.

I specify repository implementation in the composition root using IoC container:

ioc.Bind(typeof (IRepository<>)).To(typeof (RavenDbRepository<>));

In the tests I'm using it's in-memory replacement:

ioc.Bind(typeof (IRepository<>)).To(typeof (InMemoryRepository<>));

If I want to add more business-specific queries for the repository, I will add an extension method (similar to your extension method in the answer):

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));
    }
}

So you can use and mix those methods in the business logic layer queries, saving testability and easiness of repository implementations, like:

var freshVegetables = repo.GetAll().SelectVegetables().FreshOnly();

If you don't want to use a different namespace for those extension methods (like me) - ok, put them in the same namespace where repository implementation resides (like MyProject.Data), or, even better, to some existing business specific namespace (like MyProject.Products or MyProject.Data.Products). No need to remember additional namespaces now.

If you have some specific repository logic for some kind of entities, create a derived repository class overriding the method you want. For example, if products can only be found by ProductNumber instead of Id and don't support deleting, you can create this class:

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");
    }
}

And make IoC return this specific repository implementation for products:

ioc.Bind(typeof (IRepository<>)).To(typeof (RavenDbRepository<>));
ioc.Bind<IRepository<Product>>().To<ProductRepository>();

That's how I leave in piece with my repositories ;)

Checkout T4 Files for code generation. T4 is built into Visual Studio. See a tutorial here.

I have created T4 files for code generating POCO entities by inspecting a LINQ DBML and for their repositories, I think it would serve you well here. If you generate partial classes with your T4 file, you could just write code for the special cases.

To me, it seems that you divide the base classes and then want the functionality from both of them in one inheritor class. In such a case, composition is the choice. Multiple class inheritance would also be nice if C# supported it. However, because I feel the inheritance is nicer and reusability is still fine, my first option choice would go with it.

Option 1

I would rather have one more base class instead of the composition of the two. Reusability can be solved with static methods as well rather than the inheritance:

Reusable part is not visible outside. No need to remember the namespace.

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(); }
}

Of course there's some minor repeated code, but that's just calling the ready-made functions from the common library.

Option 2

My implementation of the composition (or imitation of the multiple inheritance):

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);
    }
}

Inheritance and composition (below):

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"); }
}

Composition. Not sure if this is enough to say that no boilerplate code exists.

class ChildWithUpdateAndArchive : Composite<ChildWithUpdate, ChildWithArchive>
{
    public ChildWithUpdateAndArchive(ChildWithUpdate cwu, ChildWithArchive cwa)
        : base(cwu, cwa)
    {
    }
}

Code using all this looks kind of OK, but still unusual (invisible) type casts in assignments. This is a pay off for having less boilerplate code:

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();

Here is my version:

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; }
}

I don't like generic repository interface as it puts additional restrictions and makes it harder to work with it later. I use generic methods instead.

Instead of using header interface for repository I use role interfaces for each repository method. This lets me add additional functionality to repository methods, like logging, publishing changes to PubSub and so on.

I don't use repository for custom queries as I yet didn't find any good and simple querying abstraction that would fit any database. My version of repository can only get item by id or get all items of same type. Other queries is done in memory (if performance is good enough) or I have some other mechanism.

For convenience IRepository interface could be introduced so you would not have to constantly write 4 interfaces for something like crud controllers

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);
    }
}

I mentioned that with role interfaces i can add additional behavior, here is couple examples using decorators

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);
    }
}

It's not hard to implement in memory store for testing

class InMemoryStore : IRepository
{
    private readonly IDictionary<Type, Dictionary<object, object>> db;

    public InMemoryStore(IDictionary<Type, Dictionary<object, object>> db)
    {
        this.db = db;
    }

    ...
}

Finally put all together

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);

You can use the visitor pattern, read an implementation here so you can only implement the necesary functionality.

Here´s the idea:

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;
    }
}

So, you can apply separation of concerns between domain logic and infraestructure applying the visitor patter for persistance. Regards!

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!