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.
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;
}
}
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.
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.
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.
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)
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.
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:
Il y a ici excessivement hypothèses de base importantes:
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).