Je cherche depuis un certain temps une bonne solution aux problèmes présentés par le modèle typique de référentiel (liste croissante de méthodes pour les requêtes spécialisées, etc. voir: http://ayende.com/blog/3955/repository- est-le-nouveau-singleton ).
J'aime vraiment l'idée d'utiliser les requêtes de commande, en particulier via l'utilisation du modèle de spécification. Cependant, mon problème avec la spécification est qu'elle ne concerne que les critères de sélections simples (essentiellement, la clause where), et ne traite pas des autres problèmes de requêtes, tels que la jonction, le regroupement, la sélection de sous-ensembles ou la projection, etc. fondamentalement, tous les obstacles supplémentaires que de nombreuses requêtes doivent parcourir pour obtenir le bon ensemble de données.
(note: j'utilise le terme «commande» comme dans le modèle de commande, également connu sous le nom d'objets de requête. Je ne parle pas de commande comme dans la séparation commande / requête où il y a une distinction entre les requêtes et les commandes (mise à jour, suppression, insérer))
Je recherche donc des alternatives qui encapsulent toute la requête, mais toujours suffisamment flexibles pour ne pas simplement échanger des référentiels spaghetti pour une explosion de classes de commande.
J'ai utilisé, par exemple, Linqspecs, et bien que je trouve une certaine valeur à pouvoir attribuer des noms significatifs aux critères de sélection, ce n'est tout simplement pas suffisant. Peut-être suis-je à la recherche d'une solution mixte qui combine plusieurs approches.
Je suis à la recherche de solutions que d'autres pourraient avoir développées pour résoudre ce problème ou pour résoudre un problème différent, tout en satisfaisant à ces exigences. Dans l'article lié, Ayende suggère d'utiliser directement le contexte nHibernate, mais je pense que cela complique grandement votre couche métier car elle doit désormais contenir des informations de requête.
J'offrirai une prime à ce sujet, dès que la période d'attente sera écoulée. Alors, s'il vous plaît, rendez vos solutions dignes de récompense, avec de bonnes explications et je sélectionnerai la meilleure solution et je voterai pour les finalistes.
REMARQUE: je recherche quelque chose qui est basé sur ORM. Il n'est pas nécessaire que ce soit EF ou nHibernate explicitement, mais ce sont les plus courants et conviendraient le mieux. S'il peut être facilement adapté à d'autres ORM, ce serait un bonus. La compatibilité Linq serait également bien.
MISE À JOUR: Je suis vraiment surpris qu'il n'y ait pas beaucoup de bonnes suggestions ici. Il semble que les gens sont soit totalement CQRS, soit complètement dans le camp du référentiel. La plupart de mes applications ne sont pas assez complexes pour justifier le CQRS (quelque chose avec la plupart des défenseurs du CQRS disent volontiers que vous ne devriez pas l'utiliser pour).
MISE À JOUR: Il semble y avoir un peu de confusion ici. Je ne recherche pas une nouvelle technologie d'accès aux données, mais plutôt une interface raisonnablement bien conçue entre l'entreprise et les données.
Idéalement, ce que je recherche, c'est une sorte de croisement entre les objets de requête, le modèle de spécification et le référentiel. Comme je l'ai dit ci-dessus, le modèle de spécification ne traite que de l'aspect de la clause where, et non des autres aspects de la requête, tels que les jointures, les sous-sélections, etc. Les référentiels traitent l'ensemble de la requête, mais deviennent incontrôlables après un certain temps . Les objets de requête traitent également l'ensemble de la requête, mais je ne veux pas simplement remplacer les référentiels par des explosions d'objets de requête.
la source
Réponses:
Avertissement: comme il n'y a pas encore de bonnes réponses, j'ai décidé de publier une partie d'un excellent article de blog que j'ai lu il y a quelque temps, copié presque textuellement. Vous pouvez trouver le billet de blog complet ici . Alors voilà:
Nous pouvons définir les deux interfaces suivantes:
Le
IQuery<TResult>
spécifie un message qui définit une requête spécifique avec les données qu'il renvoie à l'aide duTResult
type générique. Avec l'interface précédemment définie, nous pouvons définir un message de requête comme celui-ci:Cette classe définit une opération de requête avec deux paramètres, qui aboutira à un tableau d'
User
objets. La classe qui gère ce message peut être définie comme suit:Nous pouvons maintenant laisser les consommateurs dépendre de l'
IQueryHandler
interface générique :Immédiatement, ce modèle nous donne beaucoup de flexibilité, car nous pouvons maintenant décider quoi injecter dans le
UserController
. Nous pouvons injecter une implémentation complètement différente, ou une implémentation qui encapsule l'implémentation réelle, sans avoir à modifier leUserController
(et tous les autres consommateurs de cette interface).L'
IQuery<TResult>
interface nous donne un support lors de la compilation lors de la spécification ou de l'injectionIQueryHandlers
dans notre code. Lorsque nous modifions leFindUsersBySearchTextQuery
pour retourner à laUserInfo[]
place (en implémentantIQuery<UserInfo[]>
), laUserController
compilation échouera, car la contrainte de type générique surIQueryHandler<TQuery, TResult>
ne pourra pas être mappéeFindUsersBySearchTextQuery
àUser[]
.Injecter le
IQueryHandler
Cependant, interface dans un consommateur présente des problèmes moins évidents qui doivent encore être résolus. Le nombre de dépendances de nos consommateurs peut devenir trop important et entraîner une surinjection du constructeur - lorsqu'un constructeur prend trop d'arguments. Le nombre de requêtes qu'une classe exécute peut changer fréquemment, ce qui nécessiterait des changements constants dans le nombre d'arguments du constructeur.Nous pouvons résoudre le problème d'avoir à en injecter trop
IQueryHandlers
avec une couche supplémentaire d'abstraction. Nous créons un médiateur qui se situe entre les consommateurs et les gestionnaires de requêtes:Le
IQueryProcessor
est une interface non générique avec une méthode générique. Comme vous pouvez le voir dans la définition de l'interface, leIQueryProcessor
dépend de l'IQuery<TResult>
interface. Cela nous permet d'avoir une prise en charge du temps de compilation chez nos consommateurs qui dépendent duIQueryProcessor
. Réécrivons leUserController
pour utiliser le nouveauIQueryProcessor
:Le
UserController
dépend maintenant d'unIQueryProcessor
qui peut gérer toutes nos requêtes. La méthodeUserController
sSearchUsers
appelle laIQueryProcessor.Process
méthode en passant un objet de requête initialisé. Puisque l' interfaceFindUsersBySearchTextQuery
implémente l'IQuery<User[]>
interface, nous pouvons la passer à laExecute<TResult>(IQuery<TResult> query)
méthode générique . Grâce à l'inférence de type C #, le compilateur est capable de déterminer le type générique et cela nous évite d'avoir à énoncer explicitement le type. Le type de retour duProcess
méthode est également connu.Il est désormais de la responsabilité de la mise en œuvre de la
IQueryProcessor
recherche du droitIQueryHandler
. Cela nécessite un typage dynamique, et éventuellement l'utilisation d'un framework d'injection de dépendances, et peut tout être fait avec seulement quelques lignes de code:La
QueryProcessor
classe construit unIQueryHandler<TQuery, TResult>
type spécifique en fonction du type de l'instance de requête fournie. Ce type est utilisé pour demander à la classe de conteneur fournie d'obtenir une instance de ce type. Malheureusement, nous devons appeler laHandle
méthode en utilisant la réflexion (en utilisant le mot-clé dymamic C # 4.0 dans ce cas), car à ce stade, il est impossible de convertir l'instance du gestionnaire, car l'TQuery
argument générique n'est pas disponible au moment de la compilation. Cependant, à moins que laHandle
méthode ne soit renommée ou obtienne d'autres arguments, cet appel n'échouera jamais et si vous le souhaitez, il est très facile d'écrire un test unitaire pour cette classe. Utiliser la réflexion donnera une légère baisse, mais il n'y a pas vraiment à s'inquiéter.Pour répondre à l'une de vos préoccupations:
Une conséquence de l'utilisation de cette conception est qu'il y aura beaucoup de petites classes dans le système, mais avoir beaucoup de petites classes / focalisées (avec des noms clairs) est une bonne chose. Cette approche est clairement bien meilleure que d'avoir de nombreuses surcharges avec des paramètres différents pour la même méthode dans un référentiel, car vous pouvez les regrouper dans une classe de requête. Ainsi, vous obtenez toujours beaucoup moins de classes de requêtes que de méthodes dans un référentiel.
la source
TResult
paramètre générique de l'IQuery
interface n'est pas utile. Cependant, dans ma réponse mise à jour, leTResult
paramètre est utilisé par laProcess
méthode duIQueryProcessor
pour résoudre leIQueryHandler
à l'exécution.IQueryable
et en m'assurant de ne pas énumérer la collection, puis à partir de laQueryHandler
chaîne que je viens d'appeler / chaîner les requêtes. Cela m'a donné la flexibilité de tester mes requêtes et de les chaîner. J'ai un service d'application en plus de monQueryHandler
, et mon contrôleur est chargé de parler directement avec le service au lieu du gestionnaireMa façon de gérer cela est en fait simpliste et indépendante de l'ORM. Mon point de vue pour un référentiel est le suivant: le travail du référentiel est de fournir à l'application le modèle requis pour le contexte, de sorte que l'application demande simplement au dépôt ce qu'il veut mais ne lui dit pas comment l' obtenir.
Je fournis la méthode du référentiel avec un critère (oui, style DDD), qui sera utilisé par le référentiel pour créer la requête (ou tout ce qui est nécessaire - il peut s'agir d'une demande de service Web). Les jointures et les groupes à mon avis sont des détails sur le comment, pas le quoi et un critère ne devrait être que la base pour construire une clause where.
Modèle = l'objet final ou la structure de données dont l'application a besoin.
Vous pouvez probablement utiliser les critères ORM (Nhibernate) directement si vous le souhaitez. L'implémentation du référentiel doit savoir comment utiliser les critères avec le stockage sous-jacent ou DAO.
Je ne connais pas votre domaine et les exigences du modèle, mais il serait étrange que le meilleur moyen soit que l'application crée la requête elle-même. Le modèle change tellement que vous ne pouvez pas définir quelque chose de stable?
Cette solution nécessite clairement du code supplémentaire, mais elle ne couple pas le reste du à un ORM ou à tout ce que vous utilisez pour accéder au stockage. Le référentiel fait son travail pour agir comme une façade et l'OMI il est propre et le code de `` traduction des critères '' est réutilisable
la source
J'ai fait ceci, soutenu ceci et annulé ceci.
Le problème majeur est le suivant: peu importe comment vous le faites, l'abstraction ajoutée ne vous gagne pas en indépendance. Il fuira par définition. En substance, vous inventez une couche entière juste pour rendre votre code mignon ... mais cela ne réduit pas la maintenance, n'améliore pas la lisibilité ou ne vous procure aucun type d'agnosticisme de modèle.
La partie amusante est que vous avez répondu à votre propre question en réponse à la réponse d'Olivier: "il s'agit essentiellement de dupliquer la fonctionnalité de Linq sans tous les avantages que vous obtenez de Linq".
Demandez-vous: comment cela pourrait-il ne pas être?
la source
Vous pouvez utiliser une interface fluide. L'idée de base est que les méthodes d'une classe retournent l'instance actuelle cette même classe après avoir effectué une action. Cela vous permet d'enchaîner les appels de méthode.
En créant une hiérarchie de classes appropriée, vous pouvez créer un flux logique de méthodes accessibles.
Tu l'appellerais comme ça
Vous ne pouvez créer qu'une nouvelle instance de
Query
. Les autres classes ont un constructeur protégé. Le but de la hiérarchie est de "désactiver" les méthodes. Par exemple, laGroupBy
méthode retourne aGroupedQuery
qui est la classe de base deQuery
et n'a pas deWhere
méthode (la méthode where est déclarée dansQuery
). Par conséquent, il n'est pas possible d'appelerWhere
aprèsGroupBy
.Ce n'est cependant pas parfait. Avec cette hiérarchie de classes, vous pouvez successivement masquer les membres, mais pas en afficher de nouveaux. Par conséquent
Having
lève une exception lorsqu'elle est appelée auparavantGroupBy
.Notez qu'il est possible d'appeler
Where
plusieurs fois. Cela ajoute de nouvelles conditions avec unAND
aux conditions existantes. Cela facilite la construction de filtres par programmation à partir de conditions uniques. La même chose est possible avecHaving
.Les méthodes acceptant les listes de champs ont un paramètre
params string[] fields
. Il vous permet de transmettre des noms de champ uniques ou un tableau de chaînes.Les interfaces Fluent sont très flexibles et ne vous obligent pas à créer beaucoup de surcharges de méthodes avec différentes combinaisons de paramètres. Mon exemple fonctionne avec des chaînes, mais l'approche peut être étendue à d'autres types. Vous pouvez également déclarer des méthodes prédéfinies pour des cas spéciaux ou des méthodes acceptant des types personnalisés. Vous pouvez également ajouter des méthodes comme
ExecuteReader
ouExceuteScalar<T>
. Cela vous permettrait de définir des requêtes comme celle-ciMême les commandes SQL construites de cette manière peuvent avoir des paramètres de commande et ainsi éviter les problèmes d'injection SQL et en même temps permettre aux commandes d'être mises en cache par le serveur de base de données. Ce n'est pas un remplacement pour un mappeur O / R mais peut aider dans les situations où vous créeriez les commandes en utilisant une simple concaténation de chaînes autrement.
la source