Pertanyaan C # - Komposisi Objek - Menghapus Kode Boilerplate


Konteks / Pertanyaan

Saya telah bekerja di banyak proyek .NET yang telah diminta untuk menyimpan data dan biasanya berakhir dengan menggunakan Gudang pola. Apakah ada yang tahu strategi yang baik untuk menghapus kode boilerplate sebanyak tanpa mengorbankan skalabilitas basis kode?

Strategi Warisan

Karena begitu banyak kode Repositori adalah pelat boiler dan perlu diulang, saya biasanya membuat kelas dasar untuk mencakup dasar-dasar seperti penanganan pengecualian, pencatatan dan dukungan transaksi serta beberapa metode CRUD dasar:

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

Jadi ini berfungsi dengan baik ketika saya memiliki domain sederhana, saya dapat dengan cepat membuat kelas repositori DRY untuk setiap entitas. Namun, ini mulai rusak ketika domain menjadi lebih kompleks. Katakanlah entitas baru diperkenalkan yang tidak memungkinkan pembaruan. Saya dapat memecah kelas dasar dan memindahkan metode Pembaruan ke kelas yang berbeda:

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

Solusi ini tidak skala yang baik. Katakanlah saya memiliki beberapa Entitas yang memiliki metode umum:     arsip Virtual void publik (entitas T) {}

tetapi beberapa Entitas yang dapat Diarsipkan juga dapat Diperbarui sementara yang lain tidak bisa. Jadi solusi Warisan saya rusak, saya harus membuat dua kelas dasar baru untuk menangani skenario ini.

Strategi Komputasi

Saya telah menjelajahi pola Compositon, tetapi tampaknya ini meninggalkan banyak kode plat boiler:

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

The MyEntityRepository sekarang dimuat dengan kode boilerplate. Apakah ada alat / pola yang dapat saya gunakan untuk menghasilkan ini secara otomatis?

Jika saya bisa mengubah MyEntityRepository menjadi sesuatu seperti ini, saya pikir itu akan menjadi sangat ideal:

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

Pemrograman Berorientasi Aspek

Saya melihat ke dalam menggunakan kerangka AOP untuk ini, secara khusus PostSharp dan mereka Aspek Komposisi, yang terlihat seperti itu harus melakukan trik, tetapi untuk menggunakan Repositori saya harus memanggil Post.Cast <> (), yang menambahkan bau yang sangat aneh pada kode. Adakah yang tahu jika ada cara yang lebih baik untuk menggunakan AOP untuk membantu menyingkirkan kode compositor boilerplate?

Pembuat Kode Khusus

Jika semua yang lain gagal, saya kira saya bisa bekerja untuk membuat plug Custom Code Generator Visual Studio yang bisa menghasilkan kode plat boiler menjadi file kode parsial. Apakah sudah ada alat di luar sana yang akan melakukan ini?

[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)'
    }
} 

Metode Perpanjangan

Lupa menambahkan ini ketika saya awalnya menulis pertanyaan (maaf). Saya juga mencoba bereksperimen dengan metode perluasan:

public static class GetByIDExtenions
{
     public T GetByID<T>(this IGetByID<T> repository, int id){ }        
}

Namun, ini memiliki dua masalah, a) Saya harus ingat namespace dari kelas metode penyuluhan dan menambahkannya di mana-mana dan b) metode ekstensi tidak dapat memenuhi dependensi antarmuka:

public interface IMyEntityRepository : IGetByID<MyEntity>{}
public class MyEntityRepository : IMyEntityRepository{}

Memperbarui: Akan T4 Template menjadi solusi yang mungkin?


32
2018-03-16 17:31


asal


Jawaban:


Saya memiliki antarmuka repositori generik tunggal, yang diimplementasikan hanya sekali untuk penyimpanan data tertentu. Ini dia:

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

Saya memiliki implementasi untuk EntityFramework, NHibernate, penyimpanan RavenDB. Juga saya memiliki implementasi dalam memori untuk pengujian unit.

Sebagai contoh, di sini adalah bagian dari repositori berbasis koleksi di memori:

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

Antarmuka repositori generik membebaskan saya dari membuat banyak kelas serupa. Anda hanya memiliki satu implementasi repositori generik, tetapi juga kebebasan dalam kueri.

IQueryable<T> hasil dari GetAll() metode memungkinkan saya membuat kueri apa pun yang saya inginkan dengan data, dan memisahkannya dari kode khusus penyimpanan. Semua OR NET populer. Memiliki penyedia LINQ sendiri, dan mereka semua harus memiliki sihir itu GetAll() metode - jadi tidak ada masalah di sini.

Saya menetapkan implementasi repositori di akar komposisi menggunakan kontainer IoC:

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

Dalam tes yang saya gunakan itu adalah penggantian dalam memori:

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

Jika saya ingin menambahkan lebih banyak permintaan khusus bisnis untuk repositori, saya akan menambahkan metode ekstensi (mirip dengan metode ekstensi Anda dalam jawabannya):

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

Jadi Anda dapat menggunakan dan mencampur metode tersebut dalam kueri lapisan logika bisnis, menyimpan kemampuan uji dan kemudahan penerapan repositori, seperti:

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

Jika Anda tidak ingin menggunakan namespace yang berbeda untuk metode ekstensi tersebut (seperti saya) - ok, letakkan di namespace yang sama tempat implementasi repositori berada (seperti MyProject.Data), atau, lebih baik lagi, untuk beberapa ruang nama khusus bisnis yang ada (seperti MyProject.Products atau MyProject.Data.Products). Tidak perlu mengingat ruangnama tambahan sekarang.

Jika Anda memiliki beberapa logika repositori spesifik untuk beberapa jenis entitas, buat kelas repositori turunan yang menggantikan metode yang Anda inginkan. Misalnya, jika produk hanya dapat ditemukan oleh ProductNumber dari pada Id dan tidak mendukung penghapusan, Anda dapat membuat kelas ini:

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

Dan buat IoC mengembalikan implementasi repositori spesifik ini untuk produk:

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

Begitulah cara saya meninggalkan bagian dengan repositori saya;)


11
2018-03-16 22:42



Checkout T4 File untuk pembuatan kode. T4 dibangun ke dalam Visual Studio. Lihat tutorial di sini.

Saya telah membuat file T4 untuk menghasilkan kode entitas POCO dengan memeriksa LINQ DBML dan untuk repositori mereka, saya pikir ini akan melayani Anda dengan baik di sini. Jika Anda menghasilkan kelas parsial dengan file T4 Anda, Anda bisa menulis kode untuk kasus-kasus khusus.


4
2018-04-11 19:32



Bagi saya, tampaknya Anda membagi kelas dasar dan kemudian menginginkan fungsionalitas dari keduanya dalam satu kelas pewaris. Dalam kasus seperti itu, komposisi adalah pilihannya. Multiple class inheritance juga akan bagus jika C # mendukungnya. Namun, karena saya merasa warisan lebih baik dan dapat digunakan kembali masih baik, pilihan opsi pertama saya akan pergi dengan itu.

Pilihan 1

Saya lebih suka memiliki satu lagi kelas dasar daripada komposisi keduanya. Reusability dapat diselesaikan dengan metode statis juga daripada warisan:

Bagian yang dapat digunakan kembali tidak terlihat di luar. Tidak perlu mengingat 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(); }
}

Tentu saja ada beberapa kode kecil yang diulang, tetapi itu hanya memanggil fungsi yang sudah jadi dari perpustakaan umum.

pilihan 2

Implementasi saya terhadap komposisi (atau peniruan dari 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);
    }
}

Warisan dan komposisi (di bawah):

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

Komposisi. Tidak yakin apakah ini cukup untuk mengatakan bahwa tidak ada kode boiler.

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

Kode yang menggunakan semua ini terlihat oke, tapi masih tidak biasa (tidak terlihat) ketik gips dalam tugas. Ini adalah hasil dari kurangnya kode boilerplate:

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

2
2018-04-17 19:59



Ini versi saya:

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

Saya tidak suka antarmuka repositori umum karena menempatkan pembatasan tambahan dan membuatnya lebih sulit untuk bekerja dengannya nanti. Saya menggunakan metode generik sebagai gantinya.

Alih-alih menggunakan antarmuka header untuk repositori yang saya gunakan antarmuka peran untuk setiap metode repositori. Ini memungkinkan saya menambahkan fungsionalitas tambahan ke metode repositori, seperti logging, menerbitkan perubahan ke PubSub dan seterusnya.

Saya tidak menggunakan repositori untuk kueri khusus karena saya belum menemukan abstraksi query yang bagus dan sederhana yang sesuai dengan basis data apa pun. Versi repositori saya hanya bisa mendapatkan item dengan id atau mendapatkan semua item dengan tipe yang sama. Pertanyaan lain dilakukan dalam memori (jika kinerja cukup baik) atau saya memiliki beberapa mekanisme lain.

Untuk kenyamanan antarmuka IRepository dapat diperkenalkan sehingga Anda tidak perlu terus-menerus menulis 4 antarmuka untuk sesuatu seperti kontroler kasar

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

Saya menyebutkan bahwa dengan antarmuka peran saya dapat menambahkan perilaku tambahan, di sini adalah beberapa contoh menggunakan dekorator

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

Ini tidak sulit untuk diterapkan di toko memori untuk pengujian

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

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

    ...
}

Akhirnya mengumpulkan semuanya

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

1
2017-11-25 11:21



Anda dapat menggunakan pola pengunjung, membaca sebuah implementasi sini sehingga Anda hanya dapat menerapkan fungsi yang diperlukan.

Inilah idenya:

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

Jadi, Anda dapat menerapkan pemisahan kekhawatiran antara logika domain dan infrastruktur yang menerapkan derai pengunjung untuk ketekunan. Salam!


1
2017-12-05 18:32