2016-09-27 3 views
0

ContextePolymorphisme par types génériques où les types ne partagent pas une classe de base

Ceci est une question de refactoring. J'ai un tas de méthodes qui ont plus ou moins exactement le même code mais qui agissent sur différents types. Il y a essentiellement une méthode par type et je veux les combiner tous en un qui peut utiliser un type générique.

Code actuel

Peut-être que le code ci-dessous vous aidera à comprendre ce que je suis en train -

Les méthodes suivantes diffèrent principalement dans le DbSet <> argument entité. À l'intérieur du code de méthode, ils utilisent principalement exactement les mêmes propriétés, mais dans une ou deux lignes, ils peuvent utiliser des propriétés qui ne sont pas partagées par les types d'entité. Par exemple, AccountId (à partir de l'entité Account) et CustomerId (à partir de l'entité Customer).

int? MethodToRefactor(DbSet<Account> entity, List someCollection, string[] moreParams) 
     { 
      int? keyValue = null; 
      foreach (var itemDetail in someCollection) 
      { 
       string refText = GetRefTextBySource(itemDetail, moreParams); 
//Only the below two lines differ in all MethodToRefactor because they use entity's properties that are not shared by all entities 
       if (entity.Count(a => a.Name == refText) > 0) 
        keyValue = entity.Where(a => a.Name == refText).First().AccountId; 
       if (...some conditional code...) 
        break; 
      } 
      return keyValue; 
     } 

int? MethodToRefactor(DbSet<Customer> entity, List someCollection, string[] moreParams) 
{ 
      int? keyValue = null; 
      foreach (var itemDetail in someCollection) 
      { 
       string refText = GetRefTextBySource(itemDetail, moreParams); 
//Only the below two lines differ in all MethodToRefactor because they use entity's properties that are not shared by all entities 
       if (entity.Count(c => c.CustomerName == refText) > 0) 
        keyValue = entity.Where(c => c.CustomerName == refText).First().CustomerId; 
       if (...some conditional code...) 
        break; 
      } 
      return keyValue; 
     } 

Ci-dessous le code qui appelle les méthodes ci-dessus -

void Caller() 
     { 
        foreach (var entity in EntityCollection) 
        { 
         if (entity.Name == "Account") 
         { 
          id = MethodToRefactor(db.Accounts,...); 
         } 
         else if (entity.Name == "Customer") 
         { 
          id = MethodToRefactor(db.Customers,...); 
         } 
      } 
    } 

Problème

Ce n'est pas extensible pour une chose car il faut copier/coller une nouvelle MethodToRefactor pour chaque nouveau entité ajoutée. C'est difficile à maintenir aussi bien. Je peux peut-être refactoriser le code commun à tous les MethodToRefactors dans une méthode séparée et y faire un ifelse par entité, mais je fusionnerais fondamentalement l'appelant avec MethodToRefactor. Je suis à la recherche d'une solution plus simple avec des changements minimes dans la méthode Caller, comme décrit ci-dessous.

Idéal/désiré Code refactorisé

C'est un excellent candidat pour les types génériques/modèles. Comme on le voit ci-dessous, je peux changer l'entité actuelle pour qu'elle soit un T générique et passer les deux lignes qui n'utilisent pas les propriétés communes parmi les entités en tant qu'expressions/méthodes. Voici le pseudo-code de type C# qui illustre la solution idéale, mais je ne sais pas comment le faire réellement en C#.

int? MethodToRefactor<T>(DbSet<T> entity, Expression<Func<T, T> filterMethod, 
Expression<Func<T, T> getIdMethod, List someCollection, string[] moreParams) where T : Account, Customer //This will fail 
{ 
      int? keyValue = null; 
      foreach (var itemDetail in someCollection) 
      { 
       string refText = GetRefTextBySource(itemDetail, moreParams); 
       if (filterMethod(entity) == true) 
        keyValue = getIdMethod(entity); 
       if (...some conditional code...) 
        break; 
      } 
      return keyValue; 
     } 

void Caller() 
     { 
        foreach (var entity in EntityCollection) 
        { 
         if (entity.Name == "Account") 
         { 
          id = MethodToRefactor<Account>(db.Accounts,() => {entity.Count(a => a.Name == refText) > 0},() => {entity.Where(a => a.Name == refText).First().AccountId},...); 
         } 
         else if (entity.Name == "Customer") 
         { 
          id = MethodToRefactor<Customer>(db.Customer,() => {entity.Count(c => c.CustomerName == refText) > 0},() => {entity.Where(c => c.CustomerName == refText).First().CustomerId},...); 
         } 
      } 
    } 

Avantages/objectifs atteints 1. Nous avons combiné tous MethodToRefactors en un seul et éliminé tout le code en double. 2. Nous avons éliminé les opérations spécifiques à l'entité à l'appelant. Ceci est important car cette logique est déplacée vers l'emplacement logique qui sait comment les différentes entités diffèrent les unes des autres (Caller avait une entité par entité ifelse pour commencer) et comment ces différences doivent être utilisées. 2. En déléguant le code spécifique d'entité à l'appelant, nous l'avons également rendu plus flexible, de sorte que nous n'avons pas besoin de créer un seul MethodToRefactor par logique spécifique à l'entité.

Note: Je ne suis pas un grand fan d'Adaptateur, Stratégie etc, je préfère les solutions qui peuvent atteindre ces objectifs en utilisant les fonctionnalités de langage C#. Cela ne veut pas dire que je suis anti-classique-design-patterns, c'est juste que je n'aime pas l'idée de créer un tas de nouvelles classes quand je peux le faire en refactorisant en quelques méthodes.

Répondre

2

Si les entités n'ont pas la même classe de base, le mieux que vous puissiez faire est d'avoir une contrainte de classe.Comme les deux expressions sont essentiellement les mêmes, vous devez simplement passer une expression et une fonction pour obtenir la valeur de clé de l'entité.

Les méthodes Count et First peuvent également être fusionnées en une seule instruction et la vérification des null.

int? MethodToRefactor<T>(DbSet<T> entities, Func<string, Expression<Func<T, bool>>> expressionFilter, Func<T, int> getIdFunc, IList<string> someCollection, string[] moreParams) 
    where T : class 
{ 
    int? keyValue = null; 
    foreach (var itemDetail in someCollection) 
    { 
     string refText = GetRefTextBySource(itemDetail, moreParams); 
     var entity = entities.FirstOrDefault(expressionFilter(refText)); 
     if (entity != null) 
     { 
      keyValue = getIdFunc(entity); 
     } 
     if (...some conditional code...) 
      break; 
    } 
    return keyValue; 
} 

Vous appelleraient la méthode comme celui-ci ici

id = MethodToRefactor<Account>(db.Accounts, txt => a => a.Name == txt, a => a.AccountId, ...); 
id = MethodToRefactor<Customer>(db.Customers, txt => c => c.CustomerName == txt, c => c.CustomerId, ...); 
+0

Merci. Je l'ai juste essayé et la 'var entity = entities.FirstOrDefault (filterExpression (refText));' line semble lancer l'erreur "Nom de la méthode attendue". Je ne suis pas sûr de ce qui le cause. – Achilles

+0

c'est parce que la méthode donnée à FirstOrDefault n'a pas la bonne signature. FirstOrDefault besoin d'une méthode comme celle-ci Func , vous donnez Func . Ou cela peut être dû à votre expression. N'importe qui, corrigez-moi si je me trompe, je ne suis pas vraiment expérimenté avec les expressions pour l'instant. –

+0

entities.FirstOrDefault (e => filterExpression (e, refText)) –

1

comment vous pouvez le faire.

un type donné T, nous avons tous besoin d'un accesseur à une propriété string à comparer avec refText et aussi un accesseur à une propriété int pour keyValue. Le premier est exprimé par Expression<Func<T, string>> nameSelector et le second par Expression<Func<T, int>> keySelector, donc ceux-ci devraient être les paramètres supplémentaires au MethodToRefactor.

Qu'en est-il la mise en œuvre, le code

if (entity.Count(a => a.Name == refText) > 0) 
    keyValue = entity.Where(a => a.Name == refText).First().AccountId; 

peut être plus optimale (à l'aide d'une seule requête de base de données retournant un seul champ) comme celui-ci (pseudo code):

keyValue = entity.Where(e => nameSelector(e) == refText) 
       .Select(e => (int?)keySelector(e)) 
       .FirstOrDefault(); 

Le int? cast est nécessaire pour permettre le retour null quand refText n'existe pas.

Afin de mettre en œuvre, nous avons besoin de deux expressions dérivées des arguments:

Expression<Func<T, bool>> predicate = e => nameSelector(e) == refText; 

et

Expression<Func<T, int?>> nullableKeySelector = e => (int?)keySelector(e); 

Bien sûr, le n'est pas une syntaxe valide, mais peut facilement dessus être construit avec System.Linq.Expressions.

Avec tout cela étant dit, la méthode refondus pourrait ressembler à ceci:

et l'utilisation:

compte:

id = MethodToRefactor(db.Accounts, e => e.Name, e => e.AccountId, ...); 

Client:

id = MethodToRefactor(db.Customer, e => e.CustomerName, e => e.CustomerId, ...); 
1

Je comprends que vous ne possédez pas de classe de base, mais votre méthode est définitivement applicable uniquement aux classes de votre dal. En tant que tel, je marquerais définitivement les classes disponibles avec une interface. Cela aidera les autres membres de votre équipe à avoir une idée de l'endroit où ils peuvent utiliser votre méthode. J'ai toujours ajouté une interface de base à mes classes dal.

Je ne pense pas que la définition de la propriété de clé soit de la responsabilité de votre interlocuteur. La clé est quelque chose que l'entité devrait fournir.

Avoir une interface, vous pouvez déjà la propriété abstraite clef, ayant

internal interface IEntity 
{ 
    int Key { get; } 
} 

Bien sûr, vous pouvez l'avoir générique par le keytype, si vous avez plus d'un. En ce qui concerne votre propriété de recherche de terme, c'est quelque chose que vous devez décider. Soit c'est aussi une propriété de l'entité (si cette propriété est utilisée à plus d'un endroit), ou est utilisée seulement dans cette méthode. Je suppose que pour des raisons de simplicité, ceci est utilisé seulement ici.

Dans ce cas, votre méthode ressemblerait à ceci:

int? MethodToRefactor<T>(EfContext context, IEnumerable<Expression<Func<T, string>>> searchFields, IEnumerable<string> someCollection, string[] moreParams) 
    where T : class, IEntity 
{ 
    int? keyValue = null; 
    foreach (var itemDetail in someCollection) 
    { 
     string refText = GetRefTextBySource(itemDetail, moreParams); 
     if (searchFields.Any()) 
     { 
      var filter = searchFields.Skip(1).Aggregate(EqualsValue(searchFields.First(), refText), (e1, e2) => CombineWithOr(e1, EqualsValue(e2, refText))); 
      var entity = context.Set<T>().FirstOrDefault(filter); 
      if (entity != null) 
      { 
       keyValue = entity.Key; 
      } 
      if (... some condition ...) 
       break; 
     } 
    } 
    return keyValue; 
} 

private Expression<Func<T, bool>> EqualsValue<T>(Expression<Func<T, string>> propertyExpression, string strValue) 
{ 
    var valueAsParam = new {Value = strValue}; // this is just to ensure that your strValue will be an sql parameter, and not a constant in the sql 
     // this will speed up further calls by allowing the server to reuse a previously calculated query plan 
     // this is a trick for ef, if you use something else, you can maybe skip this 
    return Expression.Lambda<Func<T, bool>>(
     Expression.Equal(propertyExpression.Body, Expression.MakeMemberAccess(Expression.Constant(valueAsParam), valueAsParam.GetType().GetProperty("Value"))), 
     propertyExpression.Parameters); // here you can cache the property info 
} 

private class ParamReplacer : ExpressionVisitor // this i guess you might have already 
{ 
    private ParameterExpression NewParam {get;set;} 
    public ParamReplacer(ParameterExpression newParam) 
    { 
     NewParam = newParam; 
    } 
    protected override Expression VisitParameter(ParameterExpression expression) 
    { 
     return NewParam; 
    } 
} 

private Expression<Func<T, bool>> CombineWithOr<T>(Expression<Func<T, bool>> e1, Expression<Func<T, bool>> e2) // this is also found in many helper libraries 
{ 
    return Expression.Lambda<Func<T, bool>>(Expression.Or(e1.Body, new ParamReplacer(e1.Parameters.Single()).VisitAndConvert(e2.Body, MethodBase.GetCurrentMethod().Name)), e1.Parameters); 
} 

Maintenant, cela va évidemment vous obliger à mettre en œuvre la propriété clé sur toutes vos entités, qui à mon avis n'est pas une si mauvaise chose. Apparemment, vous utilisez aussi vos propriétés clés pour d'autres choses (sinon pourquoi cette méthode retournerait-elle une clé seulement). Sur une autre note, vous récupérez l'entité entière lorsqu'une correspondance est trouvée, mais vous vous souciez uniquement de la clé. Cela pourrait être amélioré en ne récupérant que la clé, par ex. ajouter un select à la fin de l'expression. Malheureusement dans ce cas vous auriez besoin d'un peu plus de "magie" pour que ef (ou votre fournisseur de linq) comprenne l'expression .Select (e => e.Key) (au moins ef ne sortira pas de la boîte). Depuis j'espère que vous avez besoin de l'ensemble de l'entité dans votre "... certaines conditions ...", je ne suis pas d'inclure cette version dans cette réponse (aussi pour le garder court: P).

Donc finalement votre interlocuteur ressemblerait à ceci:

void Caller() 
    { 
       foreach (var entity in EntityCollection) 
       { 
        if (entity.Name == "Account") 
        { 
         id = MethodToRefactor<Account>(db, new [] {a => a.Name}, ...); 
        } 
        else if (entity.Name == "Customer") 
        { 
         id = MethodToRefactor<Customer>(db, new [] {c => c.FirstName, c => c.LastName}, ...); 
        } 
     } 
}