32 votes

C # - Composition d'objet - Suppression du code de la chaudière

Contexte / Question

J'ai travaillé sur de nombreux .NET les projets qui ont été nécessaires pour maintenir les données et ont généralement fini à l'aide d'un Dépôt de modèle. Personne ne sait d'une bonne stratégie pour sortir autant de code réutilisable sans sacrifier la base de code de l'évolutivité?

La Stratégie D'Héritage

Car une grande partie du code de Référentiel est la plaque de la chaudière et doit être répété j'ai l'habitude de créer une classe de base pour couvrir les notions de base comme la gestion des exceptions, l'enregistrement et le soutien à la transaction, ainsi que quelques méthodes CRUD de base:

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

Si cela fonctionne bien quand j'ai un simple domaine, je me permet de créer rapidement un SÈCHE classe de dépôt pour chaque entité. Cependant, cela commence à se décomposer lorsque le domaine devient de plus en plus complexe. Disons une nouvelle entité est introduit qui ne permet pas de mises à jour. Je peux briser les classes de base et déplacer la méthode de mise à Jour dans une classe différente:

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

Cette solution ne permet pas à l'échelle. Disons que j'ai plusieurs personnes qui ont une méthode commune: public virtual void Archive(T entity){}

mais certaines Entités qui peuvent être Archivés peuvent également être mis à Jour, tandis que d'autres ne le peuvent pas. Donc, mon Héritage solution se décompose, je dois créer deux nouvelles classes de base pour faire face à ce scénario.

Compoisition Stratégie

J'ai exploré la Composition du motif, mais ce qui semble laisser beaucoup de chaudière plaque de code:

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

Le MyEntityRepository est maintenant chargé de code réutilisable. Est-il un outil / motif que je peux utiliser pour générer automatiquement ce?

Si je pouvais tourner la MyEntityRepository en quelque chose comme ceci, je pense que ce serait loin d'être idéal:

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

La Programmation Orientée Aspects

J'ai regardé dans l'aide d'une AOP cadre pour ce faire, spécifiquement PostSharp et leur Composition Aspect, qui ressemble, il devrait faire l'affaire, mais dans le but d'utiliser un Référentiel je vais appeler la Poste.Cast<>(), ce qui ajoute une très étrange odeur du code. Quelqu'un sait si il ya une meilleure façon d'utiliser l'AOP pour aider à se débarrasser du compositeur code réutilisable?

Personnalisé Générateur De Code

Si tout le reste échoue, je suppose que je pourrais travailler à la création d'une Coutume Générateur de Code de Visual Studio plug in qui pourrait produire de la chaudière de la plaque de code dans un partiel fichier de code. Il y a déjà un outil qui permettrait de faire cela?

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

Les Méthodes D'Extension

Oublié d'ajouter cela, lorsque j'ai d'abord écrit à la question (désolé). J'ai aussi essayé d'expérimenter avec des méthodes d'extension:

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

Cependant, cela a deux problèmes, d'une part, je dois rappeler à l'espace de noms de l'extension des méthodes de la classe et de l'ajouter partout et b) les méthodes d'extension ne peuvent pas satisfaire à l'interface de dépendances:

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

Mise à jour: Serait - Modèles T4 être une solution possible?

11voto

whyleee Points 1954

J'ai un générique référentiel unique de l'interface, qui est mis en œuvre qu'une seule fois pour un stockage de données. Ici, il est:

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

J'ai implémentations pour EntityFramework, NHibernate, RavenDB de stockage. J'ai également en mémoire la mise en œuvre de tests unitaires.

Par exemple, voici une partie de la collection en mémoire référentiel basé sur:

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

Générique référentiel interface me libère de la création de beaucoup de classes comparables. Vous n'avez qu'un générique référentiel de mise en œuvre, mais aussi la liberté dans l'interrogation.

IQueryable<T> résultat d' GetAll() méthode me permet de faire toutes les questions que je veux avec les données, et de les séparer du stockage de code spécifiques. Tous les populaire .NET Orm ont leurs propres fournisseurs LINQ, et ils devraient tous avoir cette magie, GetAll() méthode, donc pas de problèmes ici.

Je précise référentiel de mise en œuvre dans la composition de la racine à l'aide de conteneur IoC:

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

Dans les tests que j'utilise c'est en mémoire de remplacement:

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

Si je veux ajouter plus d'affaires requêtes spécifiques pour le dépôt, je vais ajouter une méthode d'extension (similaire à votre méthode d'extension dans la réponse):

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

De sorte que vous pouvez utiliser et mélanger les méthodes de la couche de logique requêtes, économie de la testabilité et de la facilité de dépôt des mises en œuvre, comme:

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

Si vous ne souhaitez pas utiliser un espace de noms différent de ceux des méthodes d'extension (comme moi) - ok, les mettre dans le même espace de noms où référentiel de mise en œuvre réside (comme MyProject.Data), ou, mieux encore, pour certaines affaires spécifiques de l'espace de noms (comme MyProject.Products ou MyProject.Data.Products). Pas besoin de se souvenir des espaces de noms supplémentaires maintenant.

Si vous avez quelques référentiel spécifique logique pour certains types d'entités, de créer un dérivé du référentiel de la classe de substitution de la méthode que vous voulez. Par exemple, si les produits ne peuvent être trouvés en ProductNumber au lieu de Id et ne prennent pas en charge la suppression, vous pouvez créer cette classe:

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

Et faire du Cio de retour de ce référentiel spécifique de mise en œuvre pour les produits:

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

C'est la façon dont je l'ai laisser dans la pièce avec mon dépôts ;)

4voto

sidesinger Points 98

Checkout T4 Files pour la génération de code. T4 est intégré à Visual Studio. Voir un tutoriel ici .

J'ai créé des fichiers T4 pour le code générant des entités POCO en inspectant un DBML LINQ et pour leurs référentiels, je pense que cela vous servirait bien ici. Si vous générez des classes partielles avec votre fichier T4, vous pouvez simplement écrire du code pour les cas particuliers.

2voto

Tengiz Points 2800

Pour moi, il semble que vous divisez les classes de base et que vous souhaitez la fonctionnalité des deux à la fois dans l'un héritier de la classe. Dans un tel cas, la composition est le choix. Plusieurs héritage de classe serait bien aussi si C# soutenu. Cependant, parce que je sens que l'héritage est plus agréable et la réutilisabilité est toujours très bien, mon premier choix d'option serait d'aller avec elle.

Option 1

Je préfère avoir un plus de la classe de base au lieu de la composition des deux. La réutilisation peut être résolu avec des méthodes statiques ainsi, plutôt que de l'héritage:

Partie réutilisable n'est pas visible de l'extérieur. Nul besoin de rappeler l'espace de noms.

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

Bien sûr, il y a quelques petites code à répétition, mais c'est juste l'appel de la prêt-à-fonctions de la bibliothèque commune.

Option 2

Ma mise en œuvre de la composition (ou de l'imitation de l'héritage multiple):

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

L'héritage et la composition (ci-dessous):

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

De la Composition. Vous ne savez pas si cela est suffisant pour dire que pas de code réutilisable existe.

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

Code à l'aide de tout cela ressemble un peu OK, mais encore inhabituel (invisible) type jette dans les affectations. C'est un payer pour avoir moins de code réutilisable:

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X