web-dev-qa-db-fra.com

Suis-je en rupture OOP pratique avec cette architecture?

J'ai une application web. Je ne crois pas que la technologie soit importante. La structure est une application à N niveaux, illustrée dans l'image de gauche. Il y a 3 couches.

UI (modèle MVC), Business Logic Layer (BLL) et Data Access Layer (DAL)

Le problème que j'ai est mon BLL est énorme car il a la logique et les chemins à travers l'appel aux événements d'application.

Un flux typique dans l'application pourrait être:

Événement déclenché dans l'interface utilisateur, traverser vers une méthode dans le BLL, exécuter la logique (éventuellement dans plusieurs parties du BLL), éventuellement vers le DAL, revenir au BLL (où probablement plus de logique), puis renvoyer une valeur à l'interface utilisateur.

Le BLL dans cet exemple est très occupé et je pense comment le répartir. J'ai aussi la logique et les objets combinés que je n'aime pas.

enter image description here

La version de droite est mon effort.

La logique est toujours la façon dont l'application circule entre l'interface utilisateur et DAL, mais il n'y a probablement pas de propriétés ... Seules les méthodes (la majorité des classes de cette couche pourraient peut être statique car ils ne stockent aucun état). La couche Poco est l'endroit où existent des classes qui ont des propriétés (comme une classe Person où il y aurait le nom, l'âge, la taille, etc.). Ceux-ci n'auraient rien à voir avec le flux de l'application, ils ne stockent que l'état.

Le flux pourrait être:

Même déclenché à partir de l'interface utilisateur et transmet certaines données au contrôleur de couche UI (MVC). Cela traduit les données brutes et les convertit en modèle poco. Le modèle poco est ensuite transmis à la couche Logic (qui était le BLL) et finalement à la couche de requête de commande, potentiellement manipulé en cours de route. La couche de requête Command convertit le POCO en un objet de base de données (qui sont presque la même chose, mais l'un est conçu pour la persistance, l'autre pour le frontal). L'élément est stocké et un objet de base de données est renvoyé à la couche de requête de commande. Il est ensuite converti en un POCO, où il retourne à la couche Logic, potentiellement traité plus loin, puis enfin, de retour à l'interface utilisateur

La logique et les interfaces partagées est l'endroit où nous pouvons avoir des données persistantes, telles que MaxNumberOf_X et TotalAllowed_X et toutes les interfaces.

La logique/les interfaces partagées et le DAL sont tous deux la "base" de l'architecture. Ceux-ci ne savent rien du monde extérieur.

Tout sait sur poco autre que la logique/interfaces partagées et DAL.

Le flux est toujours très similaire au premier exemple, mais il rend chaque couche plus responsable d'une chose (que ce soit l'état, le flux ou autre) ... mais suis-je en train de casser OOP avec ceci approche?

Un exemple de démonstration de Logic et Poco pourrait être:

public class LogicClass
{
    private ICommandQueryObject cmdQuery;
    public PocoA Method1(PocoB pocoB) 
    { 
        return cmdQuery.Save(pocoB); 
    }

    /*This has no state objects, only ways to communicate with other 
    layers such as the cmdQuery. Everything else is just function 
    calls to allow flow via the program */
    public PocoA Method2(PocoB pocoB) 
    {         
        pocoB.UpdateState("world"); 
        return Method1(pocoB);
    }

}

public struct PocoX
{
     public string DataA {get;set;}
     public int DataB {get;set;}
     public int DataC {get;set;}

    /*This simply returns something that is part of this class. 
     Everything is self-contained to this class. It doesn't call 
     trying to directly communicate with databases etc*/
     public int GetValue()
     {

         return DataB * DataC; 
     }

     /*This simply sets something that is part of this class. 
     Everything is self-contained to this class. 
     It doesn't call trying to directly communicate with databases etc*/
     public void UpdateState(string input)
     {        
         DataA += input;  
     }
}
23
MyDaftQuestions

Oui, vous êtes très susceptible de casser le noyau OOP concepts. Cependant ne vous sentez pas mal, les gens font tout cela temps, cela ne signifie pas que votre architecture est "fausse". Je dirais qu'elle est probablement moins facile à entretenir qu'un bon OO design, mais c'est plutôt subjectif et ce n'est pas votre question de toute façon. ( ici est un de mes articles critiquant l'architecture à n niveaux en général).

Raisonnement : Le concept le plus élémentaire de OOP est que les données et la logique forment une seule unité (un objet). Bien que c'est une déclaration très simpliste et mécanique, même si, elle n'est pas vraiment suivie dans votre conception (si je vous comprends bien). Vous séparez assez clairement la plupart des données de la plupart de la logique. Avoir des apatrides (statiques) par exemple, les méthodes sont appelées "procédures" et sont généralement antithétiques à la POO.

Il y a bien sûr toujours des exceptions, mais cette conception viole généralement ces choses.

Encore une fois, je voudrais souligner "viole la POO"! = "Mauvais", donc ce n'est pas nécessairement un jugement de valeur. Tout dépend de vos contraintes d'architecture, de vos cas d'utilisation de maintenabilité, de vos exigences, etc.

55

L'un des principes fondamentaux de la programmation fonctionnelle est la fonction pure.

L'un des principes fondamentaux de la programmation orientée objet consiste à associer des fonctions aux données sur lesquelles elles agissent.

Ces deux principes fondamentaux disparaissent lorsque votre application doit communiquer avec le monde extérieur. En effet, vous ne pouvez être fidèle à ces idéaux que dans un espace spécialement préparé dans votre système. Toutes les lignes de votre code ne doivent pas répondre à ces idéaux. Mais si aucune ligne de votre code ne répond à ces idéaux, vous ne pouvez pas vraiment prétendre utiliser OOP ou FP.

Il est donc acceptable d'avoir uniquement des "objets" de données que vous jetez parce que vous en avez besoin pour franchir une frontière que vous ne pouvez tout simplement pas refactoriser pour déplacer le code intéressé. Sachez juste que ce n'est pas OOP. Voilà la réalité. OOP est quand, une fois à l'intérieur de cette limite, vous rassemblez toute la logique qui agit sur ces données en un seul endroit.

Non pas que vous ayez à le faire non plus. OOP n'est pas tout pour tout le monde. C'est ce que c'est. Ne revendiquez pas que quelque chose suit OOP quand ce n'est pas le cas ou vous '' va confondre les gens qui essaient de maintenir votre code.

Vos POCO semblent avoir une logique commerciale en eux, donc je ne m'inquiéterais pas trop d'être anémique. Ce qui me préoccupe, c'est qu'ils semblent tous très mutables. N'oubliez pas que les getters et setters ne fournissent pas une véritable encapsulation. Si votre POCO se dirige vers cette frontière, alors très bien. Comprenez simplement que cela ne vous donne pas tous les avantages d'un véritable objet encapsulé OOP. Certains l'appellent un objet de transfert de données ou DTO.

Une astuce que j'ai utilisée avec succès consiste à créer des objets OOP qui mangent des DTO. J'utilise le DTO comme objet paramètre . Mon constructeur lit son état (lu comme copie défensive ) et la jette de côté. J'ai maintenant une version entièrement encapsulée et immuable du DTO. Toutes les méthodes concernées par ces données peuvent être déplacées ici à condition qu'elles soient de ce côté de cette frontière .

Je ne fournis ni getters ni setters. Je suis dis, ne demande pas . Vous appelez mes méthodes et elles font ce qui doit être fait. Ils ne vous disent probablement même pas ce qu'ils ont fait. Ils le font juste.

Maintenant, quelque chose, quelque part, va se heurter à une autre frontière et tout s'écroule à nouveau. C'est très bien. Faites tourner un autre DTO et lancez-le sur le mur.

C'est l'essence même de l'architecture des ports et des adaptateurs. J'ai lu à ce sujet d'un perspective fonctionnelle . Cela vous intéressera peut-être aussi.

31
candied_orange

Si je lis correctement votre explication, vos objets ressemblent un peu à ceci: (délicat sans contexte)

public class LogicClass
{
    private ICommandQueryObject cmdQuery;
    public PocoA Method(PocoB pocoB) { ... }
}

public class PocoX
{
     public string DataA {get;set;}
     public int DataB {get;set;}
     ... etc
}

En ce que vos classes Poco contiennent uniquement des données et vos classes Logic contiennent les méthodes qui agissent sur ces données; oui, vous avez enfreint les principes du "Classic OOP"

Encore une fois, il est difficile de dire à partir de votre description générale, mais je risquerais que ce que vous avez écrit puisse être classé comme modèle de domaine anémique.

Je ne pense pas que ce soit une approche particulièrement mauvaise, et si vous considérez vos Poco comme des structures, cela casse nescarly OOP dans le sens le plus spécifique. En ce que vos objets sont maintenant les LogicClasses. En effet, si vous rendez votre Pocos immuable, le design peut être considéré comme très fonctionnel.

Cependant, lorsque vous faites référence à la logique partagée, aux Pocos qui sont presque mais pas les mêmes et aux statiques, je commence à m'inquiéter des détails de votre conception.

1
Ewan

Ne changez jamais votre code parce que vous pensez ou que quelqu'un vous dit que ce n'est pas ceci ou pas cela. Changez votre code s'il vous pose des problèmes et que vous avez trouvé un moyen d'éviter ces problèmes sans en créer d'autres.

Donc, à part vous qui n'aimez pas les choses, vous voulez investir beaucoup de temps pour faire un changement. Notez les problèmes que vous avez en ce moment. Notez comment votre nouveau design résoudrait les problèmes. Déterminez la valeur de l'amélioration et le coût de vos modifications. Ensuite - et c'est le plus important - assurez-vous que vous avez le temps de terminer ces changements, sinon vous vous retrouverez à moitié dans cet état, à moitié dans cet état, et c'est la pire situation possible. (J'ai déjà travaillé sur un projet avec 13 types de chaînes différents et trois efforts identifiables à moitié définis pour normaliser un type)

1
gnasher729

Un problème potentiel que j'ai vu dans votre conception (et il est très courant) - certains des pires codes "OO" que j'ai jamais rencontrés ont été causés par une architecture qui séparait les objets "Data" des objets "Code". Ce sont des trucs de niveau cauchemar! Le problème est que partout dans votre code d'entreprise lorsque vous souhaitez accéder à vos objets de données, vous avez tendance à le coder juste là en ligne (vous n'avez pas à le faire, vous pouvez créer une classe utilitaire ou une autre fonction pour le gérer, mais c'est ce que J'ai vu arriver à plusieurs reprises au fil du temps).

Le code d'accès/mise à jour n'est généralement pas collecté, vous vous retrouvez donc avec des fonctionnalités en double partout.

D'un autre côté, ces objets de données sont utiles, par exemple comme persistance de base de données. J'ai essayé trois solutions:

Copier des valeurs dans et hors de "vrais" objets et jeter votre objet de données est fastidieux (mais peut être une solution valable si vous voulez aller dans ce sens).

L'ajout de méthodes de gestion des données aux objets de données peut fonctionner, mais cela peut créer un gros objet de données en désordre qui fait plus d'une chose. Cela peut également rendre l'encapsulation plus difficile car de nombreux mécanismes de persistance veulent des accesseurs publics ... Je ne l'ai pas aimé quand je l'ai fait mais c'est une solution valable

La solution qui a le mieux fonctionné pour moi est le concept d'une classe "Wrapper" qui encapsule la classe "Data" et contient toutes les fonctionnalités de traitement des données - alors je n'expose pas du tout la classe de données (pas même les setters et les getters sauf si elles sont absolument nécessaires). Cela supprime la tentation de manipuler directement l'objet et vous oblige à ajouter à la place des fonctionnalités partagées à l'encapsuleur.

L'autre avantage est que vous pouvez vous assurer que votre classe de données est toujours dans un état valide. Voici un exemple rapide de pseudo-code:

// Data Class
Class User {
    String name;
    Date birthday;
}

Class UserHolder {
    final private User myUser // Cannot be null or invalid

    // Quickly wrap an object after getting it from the DB
    public UserHolder(User me)
    {
        if(me == null ||me.name == null || me.age < 0)
            throw Exception
        myUser=me
    }

    // Create a new instance in code
    public UserHolder(String name, Date birthday) {
        User me=new User()
        me.name=name
        me.birthday=birthday        
        this(me)
    }
    // Methods access attributes, they try not to return them directly.
    public boolean canDrink(State state) {
        return myUser.birthday.year < Date.yearsAgo(state.drinkingAge) 
    }
}

Notez que la vérification de l'âge n'est pas répartie dans votre code dans différentes zones et que vous n'êtes pas tenté de l'utiliser car vous ne pouvez même pas comprendre ce qu'est l'anniversaire (sauf si vous en avez besoin pour autre chose, dans auquel cas vous pouvez l'ajouter).

J'ai tendance à ne pas simplement étendre l'objet de données parce que vous perdez cette encapsulation et la garantie de sécurité - à ce stade, vous pourriez tout aussi bien ajouter les méthodes à la classe de données.

De cette façon, votre logique métier ne dispose pas d'un tas de fichiers indésirables/itérateurs d'accès aux données, elle devient beaucoup plus lisible et moins redondante. Je recommande également de prendre l'habitude de toujours envelopper les collections pour la même raison - en gardant les constructions en boucle/recherche hors de votre logique métier et en vous assurant qu'elles sont toujours en bon état.

1
Bill K

La catégorie "POO" est beaucoup plus large et plus abstraite que ce que vous décrivez. Il ne se soucie pas de tout cela. Il se soucie d'une responsabilité claire, de la cohésion, du couplage. Donc, au niveau que vous demandez, cela n'a pas beaucoup de sens de poser des questions sur la "pratique OOPS".

Cela dit, à votre exemple:

Il me semble qu'il y a un malentendu sur ce que signifie MVC. Vous appelez votre interface utilisateur "MVC", séparément de votre logique métier et de votre contrôle "backend". Mais pour moi, MVC comprend toute l'application Web:

  • Modèle - contient les données métier + la logique
    • Couche de données comme détail d'implémentation du modèle
  • Affichage - Code d'interface utilisateur, modèles HTML, CSS, etc.
    • Comprend des aspects côté client comme JavaScript, ou les bibliothèques pour les applications Web "une page", etc.
  • Contrôle - la colle côté serveur entre toutes les autres pièces
  • (Il y a des extensions comme ViewModel, Batch etc. dans lesquelles je n'entrerai pas ici)

Il y a ici excessivement hypothèses de base importantes:

  • Une classe/des objets Model n'ont jamais aucune connaissance sur aucune des autres parties (View, Control, ...). Il ne les appelle jamais, il ne suppose pas être appelé par eux, il n'obtient aucun attribut/paramètre de sesssion ni rien d'autre le long de cette ligne. C'est complètement seul. Dans les langues qui le supportent (par exemple, Ruby), vous pouvez lancer une ligne de commande manuelle, instancier des classes de modèle, travailler avec elles au contenu de votre cœur et faire tout elles le font sans aucune instance de Control ou Afficher ou toute autre catégorie. Il n'a aucune connaissance des sessions, des utilisateurs, etc., surtout.
  • Rien ne touche la couche de données sauf à travers un modèle.
  • La vue n'a qu'une légère touche sur le modèle (affichage de choses, etc.) et rien d'autre. (Notez qu'une bonne extension est "ViewModel" qui sont des classes spéciales qui effectuent un traitement plus substantiel pour le rendu des données d'une manière compliquée, qui ne conviendrait pas bien au modèle ou à la vue - c'est un bon candidat pour supprimer/éviter le ballonnement dans le modèle pur).
  • Le contrôle est aussi léger que possible, mais il est chargé de rassembler tous les autres acteurs et de transférer des éléments entre eux (c.-à-d., Extraire les entrées utilisateur d'un formulaire et les transmettre au modèle, transmettre les exceptions de la logique métier à un utile messages d'erreur pour l'utilisateur, etc.). Pour les API Web/HTTP/REST, etc., toutes les autorisations, la sécurité, la gestion des sessions, la gestion des utilisateurs, etc. se produisent ici (et seulement ici).

Surtout: l'interface utilisateur est un partie de MVC. Pas l'inverse (comme dans votre diagramme). Si vous adhérez à cela, les gros modèles sont en fait assez bons - à condition qu'ils ne contiennent en effet pas de choses qu'ils ne devraient pas.

Notez que "fat models" signifie que toute la logique métier est dans la catégorie Model (package, module, quel que soit le nom dans la langue de votre choix). Les classes individuelles doivent évidemment être structurées en POO dans le bon sens selon les directives de codage que vous vous donnez (c'est-à-dire, quelques lignes de code maximum par classe ou par méthode, etc.).

Notez également que la façon dont la couche de données est implémentée a des conséquences très importantes; en particulier si la couche modèle peut fonctionner sans couche de données (par exemple, pour les tests unitaires ou pour les bases de données en mémoire bon marché sur l'ordinateur portable du développeur au lieu des bases de données Oracle coûteuses ou tout ce que vous avez). Mais c'est vraiment un détail d'implémentation au niveau de l'architecture que nous examinons en ce moment. Évidemment, ici, vous voulez toujours avoir une séparation, c'est-à-dire que je ne voudrais pas voir du code qui a une logique de domaine pure directement entrelacée avec l'accès aux données, couplant intensément cela ensemble. Un sujet pour une autre question.

Pour revenir à votre question: il me semble qu'il y a un grand chevauchement entre votre nouvelle architecture et le schéma MVC que j'ai décrit, donc vous n'êtes pas sur une mauvaise voie, mais vous semblez soit réinventer certaines choses, ou l'utiliser parce que votre environnement de programmation/bibliothèques actuel le suggère. Difficile à dire pour moi. Je ne peux donc pas vous dire exactement si ce que vous envisagez est particulièrement bon ou mauvais. Vous pouvez le découvrir en vérifiant si chaque "chose" a exactement une classe responsable; si tout est très cohésif et faiblement couplé. Cela vous donne une bonne indication, et est, à mon avis, suffisant pour une bonne conception OOP (ou un bon repère de la même chose, si vous voulez).

0
AnoE