Suis-je briser la pratique de la POO avec cette architecture?

23

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.

entrez la description de l'image ici

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 aucune propriété ... Seules les méthodes (la majorité des classes de cette couche peuvent être statiques car elles 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 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 cela rend chaque couche plus responsable d'une chose (que ce soit l'état, le flux ou autre) ... mais suis-je en train de rompre la POO avec cette 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;  
     }
}
MyDaftQuestions
la source
Je ne vois rien de fondamentalement mal avec votre architecture telle que vous l'avez décrite actuellement.
Robert Harvey
19
Il n'y a pas suffisamment de détails fonctionnels dans votre exemple de code pour fournir des informations supplémentaires. Les exemples de mousse fournissent rarement une illustration suffisante.
Robert Harvey
1
Soumis à votre considération: Baruco 2012: Déconstruire le cadre, par Gary Bernhardt
Theraot
4
Peut - on trouver un meilleur titre pour cette question il peut être trouvé en ligne plus facilement?
Soner Gönül
1
Juste pour être pédant: un niveau et une couche ne sont pas la même chose. Un «niveau» parle de déploiement, une «couche» de logique. Votre couche de données sera déployée à la fois au niveau du code côté serveur et à celui de la base de données. Votre couche d'interface utilisateur sera déployée sur les niveaux de code côté client Web et côté serveur. L'architecture que vous montrez est une architecture à 3 couches. Vos niveaux sont "Client Web", "Code côté serveur" et "Base de données".
Laurent LA RIZZA

Réponses:

54

Oui, vous brisez très probablement les concepts de base de la POO. Mais ne vous sentez pas mal, les gens le font tout le temps, cela ne signifie pas que votre architecture est "fausse". Je dirais qu'il est probablement moins facile à entretenir qu'une conception OO appropriée, mais c'est plutôt subjectif et ce n'est pas votre question de toute façon. ( Voici un de mes articles critiquant l'architecture à n niveaux en général).

Raisonnement : Le concept le plus élémentaire de la POO est que les données et la logique forment une seule unité (un objet). Bien que ce soit une déclaration très simpliste et mécanique, même ainsi, 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. Par exemple, le fait d'avoir des méthodes sans état (de type statique) est appelé «procédures» et est généralement antithétique à 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.

Robert Bräutigam
la source
9
Avoir un vote positif, c'est une bonne réponse, si j'écrivais le mien, je le copierais et le collerais, mais ajouterais également que, si vous trouvez que vous n'écrivez pas de code OOP, vous devriez peut-être envisager un langage non-OOP car il vient avec beaucoup de frais supplémentaires dont vous pouvez vous passer si vous ne l'utilisez pas
TheCatWhisperer
2
@TheCatWhisperer: Les architectures d'entreprise modernes ne jettent pas la POO entièrement, juste de manière sélective (par exemple pour les DTO).
Robert Harvey
@RobertHarvey D'accord, je voulais dire si vous n'utilisez presque pas la POO n'importe où dans votre conception
TheCatWhisperer
@TheCatWhisperer de nombreux avantages dans un oop comme c # ne sont pas nécessairement dans la partie oop du langage mais dans le support disponible comme les bibliothèques, le studio visuel, la gestion de la mémoire, etc.
@Orangesandlemons Je suis sûr qu'il existe de nombreuses autres langues bien prises en charge ...
TheCatWhisperer
31

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é. La POO est lorsque, 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. La POO n'est pas tout pour tout le monde. C'est ce que c'est. Ne prétendez pas que quelque chose suit la POO quand ce n'est pas le cas ou vous allez dérouter les gens qui essaient de maintenir votre code.

Vos POCO semblent très bien avoir une logique commerciale, 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 POO encapsulé. Certains appellent cela 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 en lit l'état (lu comme copie défensive ) et le 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 dit, 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 point de vue fonctionnel . Cela vous intéressera peut-être aussi.

candied_orange
la source
5
"les getters et setters ne fournissent pas une véritable encapsulation " - oui!
Boris the Spider le
3
@BoristheSpider - les getters et setters fournissent absolument l'encapsulation, ils ne correspondent tout simplement pas à votre définition étroite de l'encapsulation.
Davor Ždralo
4
@ DavorŽdralo: Ils sont parfois utiles comme solution de contournement, mais de par leur nature même, les getters et setters rompent l'encapsulation. Fournir un moyen d'obtenir et de définir une variable interne est l'opposé d'être responsable de votre propre état et d'agir en conséquence.
cHao
5
@cHao - vous ne comprenez pas ce qu'est un getter. Cela ne signifie pas une méthode qui retourne une valeur d'une propriété d'objet. C'est une implémentation courante, mais elle peut renvoyer une valeur à partir d'une base de données, la demander via http, la calculer à la volée, peu importe. Comme je l'ai dit, les getters et setters ne brisent l'encapsulation que lorsque les gens utilisent leurs propres définitions étroites (et incorrectes).
Davor Ždralo
4
@cHao - l'encapsulation signifie que vous cachez l'implémentation. C'est ce qui est encapsulé. Si vous avez un getter "getSurfaceArea ()" sur une classe Square, vous ne savez pas si la surface est un champ, s'il est calculé à la volée (hauteur de retour * largeur) ou une troisième méthode, vous pouvez donc modifier l'implémentation interne à tout moment, car il est encapsulé.
Davor Ždralo
1

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 ne casse pas forcément la POO 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 tout à fait 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.

Ewan
la source
J'ai ajouté à mon message, copiant essentiellement votre exemple. Désolé ti n'était pas clair pour commencer
MyDaftQuestions
1
ce que je veux dire, c'est que si vous nous disiez ce que fait l'application, il serait plus facile d'écrire des exemples. Au lieu de LogicClass, vous pourriez avoir PaymentProvider ou autre chose
Ewan
1

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.

Bill K
la source
1

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, mis à part que vous 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 d'avoir 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)

gnasher729
la source
0

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 UI, 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 quelques hypothèses de base extrêmement importantes:

  • Une classe / des objets de modèle n'ont aucune connaissance des autres parties (vue, contrôle, ...). 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 prennent en charge cela (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 ce qu'elles font sans instance de contrôle ou de vue 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 de session, la gestion des utilisateurs, etc. se produisent ici (et seulement ici).

Surtout: l'interface utilisateur fait 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 complètement sur la 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).

AnoE
la source