web-dev-qa-db-fra.com

Le modèle de générateur est-il approprié à utiliser pour mettre à jour les objets dans une couche de service?

Actuellement, dans notre couche de service, nous transmettons un identifiant ainsi qu'une nouvelle valeur mise à jour, quelque chose qui ressemble à

updatePersonName(Person person, Name name)

qui, à son tour, appelle les fonctions de référentiel correspondantes. Maintenant, cela fonctionne bien si nous voulons mettre à jour une seule valeur. Mais une fois que nous commençons à vouloir mettre à jour plusieurs valeurs en même temps, nous sommes soit coincés à appeler différentes méthodes de service d'affilée, soit à définir une méthode de service qui prend plusieurs arguments à mettre à jour. C'est supportable, mais cela devient encore pire lorsque nous devons mettre à jour plusieurs valeurs qui sont forcées d'être mises à jour ensemble, ce qui signifie que nous sommes obligés de définir une méthode (peut-être nouvelle) dans le service qui s'assure que la combinaison est respectée. Et si vous commencez à avoir certaines limitations (peut-être une personne marquée comme non modifiable pour différentes raisons), cela augmente encore la complexité.

Récemment, j'ai pensé à utiliser le modèle de générateur, mais pas pour la création d'objets, mais pour la mise à jour. Quelque chose comme ça (toutes les méthodes choisies complètement arbitraires, mais vous obtenez le point):

PersonService.updater(person)
    .setName("newName")
    .addRole(newRoleObject)
    .setFlag(PersonFlag.NOT_UNDERAGE)
    .overrideBlock()
    .withEventDescription("Person changed her name on birthday!")
    .update();

Le constructeur pouvait résoudre la logique en interne sans exposer trop de complexité à l'extérieur. L'API fluide est facile à utiliser dans tous les autres services/composants qui nécessitent un accès. Je n'ai pas besoin de créer une pléthore de méthodes pour couvrir toutes les exigences. Les mises à jour multiples sont faciles à enchaîner, et si vous souhaitez mettre à jour quelque chose qui est maintenant autorisé, cela peut bloquer cela en interne à moins que vous ne remplaciez ledit bloc. Et il pourrait forcer des champs spécifiques en raison de la sécurité du type, par exemple, un EventObject.

Plus important encore, cela pourrait également, à son tour, nous assurer que nous ne faisons qu'un seul voyage dans le référentiel, au lieu de plusieurs. Cela améliorerait l'exécution, en particulier dans les algorithmes critiques qui nécessitent sinon de nombreuses passes vers la base de données.

Je peux également voir quelques problèmes avec cette approche. Il est encombrant, est une API non conventionnelle pour les personnes non expérimentées et pourrait entraîner une mauvaise utilisation. L'implémenter n'est pas anodin tout en s'assurant que la logique interne reste cohérente. Cependant, je pense que les positifs l'emportent sur les négatifs dans ma situation.

Suis-je en train de manquer quelque chose?

Edit: Je sais que généralement, vous avez une fonction save () à l'intérieur de votre DTO qui s'occupe de l'enregistrement, mais en raison de l'infrastructure existante, ce n'est pas une option à ce stade.

6
Joe

Tout d'abord, ce n'est pas le GoF Builder Pattern . Cela pourrait être le Joshua Bloch Builder Pattern . Il est utilisé pour simuler arguments nommés dans des langages (comme Java) qui n'en ont pas.

Les arguments nommés rendent les longues listes d'arguments tolérables en étiquetant clairement les arguments. Ils activent également les arguments facultatifs afin que la liste n'ait pas à inclure tous les arguments possibles comme le font les arguments positionnels.

Si c'est tout ce que vous faites, vous faites vraiment la construction d'objets parce que c'est ce que le Joshua Bloch Builder vous donne, un objet immuable avec tous ces champs définis.

Si ce n'est pas ce que vous faites, il s'agit d'un langage spécifique au domaine interne ou intégré (DSL). Ceux-ci ont le pouvoir de contrôler ce qui vient et ce qui ne vient pas ensuite. Ils le font parce que les méthodes renvoient différents types qui permettent de nouvelles méthodes.

Cela vous donne beaucoup de puissance et suffisamment de corde pour vous accrocher. C'est aussi beaucoup de travail à mettre en place dans les coulisses. Mais il a de bonnes utilisations. Par exemple JOOQ et flux Java 8 . Je l'ai utilisé avec succès pour formaliser la construction d'un objet divin cauchemardesque qui a également été utilisé pour mettre à jour la base de données et était trop ancré pour se réformer.

L'essentiel avec cela est que les règles métier sont appliquées par le mini langage que vous créez. C'est super, mais ça les met dans la pierre. Les DSL ne sont pas faciles à écrire et ils ne sont pas faciles à modifier. Utilisez-les quand ils seront beaucoup utilisés et très peu modifiés.

Cela dit, vous pouvez éviter de coder en dur l'implémentation avec une petite injection de dépendance.

happyNewName(PersionService personService, Person person, Role newRoleObject) {
    personService
        .updater(person)
        .setName("newName")
        .addRole(newRoleObject)
        .setFlag(PersonFlag.NOT_UNDERAGE)
        .overrideBlock()
        .withEventDescription("Person changed her name on birthday!")
        .update()
    ;
}

De cette façon, la "source" des DSL et son implémentation peuvent être modifiées indépendamment.

Cela vous permet de créer des façades qui guident les codeurs à suivre des cérémonies complexes qui pourraient être nécessaires. Cependant, souvent, ils ne sont pas vraiment nécessaires si vous faites simplement le travail pour simplifier le système. Le danger ici est que si vous faites une DSL à la place, vous creuserez ce trou dette technique encore plus profondément. Assurez-vous donc que vous n'avez vraiment pas de meilleure solution, car c'est l'option nucléaire.

Ce style fluide a tendance à l'heure actuelle, mais sachez que vous devez concevoir votre DSL de bout en bout. Aucun sournois ramasser des classes aléatoires au milieu de cela. C'est une énorme violation de Loi de Déméter . Ne parcourez que les cours conçus pour fonctionner ensemble comme ça. Pas ceux au hasard qui mentent sur l'endroit.

6
candied_orange

En fin de compte, il semble que vous ayez plusieurs cas d'utilisation uniques pour mettre à jour une personne, et pour cette raison, vous avez créé une classe "constructeur" qui encapsule la logique de ces mises à jour individuelles. En chaînant des mises à jour individuelles avec une API fluide, vous répliquez un cas d'utilisation . Le problème ici est que vous devez savoir quel cas d'utilisation vous exécutez afin d'appeler la bonne séquence de méthodes.

Bien qu'il s'agisse d'une nouvelle approche, cela ressemble à une mauvaise utilisation du modèle de générateur, qui fournit une couche d'abstraction sur la création d'objet. Il brise les attentes des gens sur ce que fait un "objet constructeur", et pour cette raison, je ne pense pas que ce soit un bon modèle.

Dans Clean Architecture le comportement d'un cas d'utilisation est littéralement appelé un cas d'utilisation - une classe qui se spécialise dans la coordination de toutes les opérations pour un cas d'utilisation particulier dans l'application. Vous obtenez les avantages de nommer quelque chose de similaire à un terme utilisé dans d'autres architectures, et vous obtenez simplement de meilleurs noms.

Tenez compte de toutes les combinaisons d'appels de méthode chaînés que vous effectuez sur votre classe "générateur" et créez une classe "cas d'utilisation" pour chaque séquence d'appels de méthode. Éliminez le modèle de générateur inutile, car vous ne construisez pas un nouvel objet ici, et demandez à la classe de cas d'utilisation de gérer les nuances des méthodes et des objets qui sont nécessaires dans le référentiel, et comment ces objets interagissent:

var useCase = new NameChangeUseCase(repository);

useCase.Execute(person, dateOfChange, "newName");

La méthode execute de NameChangeUseCase peut définir la description de l'événement, gérer le NOT_UNDERAGE drapeau basé sur dateOfChange et attribuez également des rôles en fonction de ces conditions:

public class NameChangeUseCase
{
    private IPersonRepository repository;

    public NameChangeUseCase(IPersonRepository repository)
    {
        this.repository = repository;
    }

    public void Execute(Person person, DateTime dateOfChange, string newName)
    {
        if (person.BirthDate == dateOfChange)
        {
            // Add event with description "Person changed her name on birthday!"
        }

        if (person.CalculateAgeInYears(dateOfChange) >= 18)
        {
            // set NOT_UNDERAGE flag
        }

        // get new role and add to person
        person.ChangeName(newName);
        repository.Save(person);
    }
}

C'est beaucoup plus facile à suivre en tant que développeur et beaucoup plus difficile à foutre.

1
Greg Burghardt