À plusieurs reprises, j'ai été confronté au problème de conception suivant que je ne sais pas comment résoudre. Imaginez, par exemple, une application qui à un moment donné reçoit un objet JSON d'une API. Avant de pouvoir utiliser cet objet, l'application doit effectuer plusieurs transformations: par exemple, une transformation peut convertir des dates d'un format à un autre, et une autre transformation peut ajouter des données supplémentaires à partir du cache (comme une entité décrivant un utilisateur, basée sur un ID utilisateur unique de l'objet d'origine).
Afin de pouvoir choisir les transformations à appliquer lors de l'exécution, les transformations sont définies dans des classes spécifiques, telles que DateFormatTransform
et ExtraDataFromCacheTransform
.
Fondamentalement, le code qui effectue la transformation serait comme ceci:
foreach (var transform in this.GetRelevantTransforms())
{
entity = transform.Apply(entity);
}
this.GetRelevantTransforms
renverrait une instance de DateFormatTransform
, ExtraDataFromCacheTransform
et d'autres classes. Chacune de ces classes implémente une interface contenant la méthode:
SampleEntity Apply(SampleEntity x)
Plus tard, des besoins supplémentaires peuvent survenir, nécessitant de modifier la signature de Apply
. Par exemple, on peut avoir besoin des transformations pour connaître l'utilisateur actuel, car selon l'utilisateur, certaines transformations peuvent être exécutées différemment:
SampleEntity Apply(SampleEntity x, User currentUser)
Le problème est qu'entre-temps, je peux avoir quelques dizaines de transformations, et une telle modification de base nécessiterait de parcourir ces dizaines de fichiers, en changeant à la fois l'interface (telle que IDateFormatTransform
) et la classe (telle que DateFormatTransform
).
Je suppose que devoir changer une cinquantaine de fichiers juste pour ajouter un argument simple n'est pas le signe d'une base de code propre.
Quelles sont les alternatives?
Dois-je utiliser un DTO, tel que TransformArguments
, qui serait toujours le seul paramètre de la méthode Apply
? Ou existe-t-il d'autres techniques?
Ou mon approche avec un ensemble de transformations est-elle défectueuse depuis le début, et si oui, comment puis-je la corriger?
Essentiellement, cela se résume à savoir si vous pouvez ou non proposer une interface généralisée utile pour le (s) site (s) d'appel, d'une manière qui dissocie les paramètres passant de l'appel réel - en encapsulant ces paramètres dans l'objet de transformation et en s'appuyant sur le polymorphisme . Vous pouvez le faire si vous pouvez déterminer les paramètres au moment de la création, mais cela peut nécessiter une refonte et une réorganisation du code (mais, hé, c'est plus ou moins inévitable).
Donc, au lieu de changerentity = transform.Apply(entity);
àentity = transform.Apply(entity, user);
partout dans la base de code, vous feriez:
var transform = new UserBasedTransform(user); // at creation site
//--------------
entity = transform.Apply(entity); // at call site
Ou, si vous ne pouvez pas déterminer le paramètre à ce moment-là, vous pouvez passer dans une usine qui peut retourner le paramètre plus tard quand cela devient possible:Transform transform = new UserBasedTransform(userFactory); // at creation site
Dans un sens, l'interface de la transformation est une abstraction (même si ce ne sont que les méthodes publiques d'une classe, et non un type d'interface réel) qui représente une façon généralisée de travailler avec les transformations. Vous voulez baser vos abstractions sur les aspects du problème qui sont relativement stables à mesure que la base de code évolue; dans votre cas, ce sont les paramètres qui changent constamment, alors traitez-les comme un détail d'implémentation et ne les référencez nulle part dans le code client. Ce qui semble stable, c'est que vous appliquez une transformation à une entité, alors organisez votre interface client autour de cela (ou, si ce n'est pas le cas, trouvez quelque chose qui a cette propriété et utilisez-la). Notez que cela est spécifique aux modèles de changement que vous avez observés, il ne s'agit donc pas de trouver une solution qui convient à toutes les situations, mais de trouver une conception qui fonctionne bien pour votre domaine particulier.
Cela simplifiera probablement vos clients, mais cela peut nécessiter une approche différente pour construire vos objets de transformation.
Il semble que le fait que le transformateur ait ou non besoin de connaître l'utilisateur est un détail de mise en œuvre et non pertinent pour l'appelant.
Compte tenu de cela, pouvez-vous injecter un contexte utilisateur dans le transformateur qui en a besoin?
class UserTransform {
public UserTransform(IUserContextAccessor userContextAccessor) {
_userContextAccessor = userContextAccessor
}
public SampleEntity Apply(SampleEntity x) {
var user = _userContextAccessor.CurrentUser;
// do user specific thing
}
}