J'ai un peu de mal à concevoir des cours d'une manière oo. J'ai lu que les objets exposent leur comportement, pas leurs données; par conséquent, plutôt que d'utiliser des getter/setters pour modifier les données, les méthodes d'une classe donnée devraient être des "verbes" ou des actions opérant sur l'objet. Par exemple, dans un objet 'Account', nous aurions les méthodes Withdraw()
et Deposit()
plutôt que setAmount()
etc. Voir: Pourquoi getter et les méthodes de setter sont mauvaises .
Ainsi, par exemple, étant donné une classe Client qui conserve beaucoup d'informations sur le client, par exemple Nom, date de naissance, numéro de téléphone, adresse, etc., comment éviterait-on aux getter/setters d'obtenir et de définir tous ces attributs? Quelle méthode de type "Comportement" peut-on écrire pour remplir toutes ces données?
Comme indiqué dans de nombreuses réponses et commentaires, les DTO sont appropriés et utiles dans certaines situations, en particulier pour le transfert de données au-delà des frontières (par exemple, la sérialisation vers JSON pour envoyer via un service Web). Pour le reste de cette réponse, je vais plus ou moins ignorer cela et parler des classes de domaine, et comment elles peuvent être conçues pour minimiser (sinon éliminer) les getters et setters, et être toujours utiles dans un grand projet. Je ne parlerai pas non plus de pourquoi supprimer les getters ou setters, ou quand pour le faire, car ce sont des questions qui leur sont propres.
Par exemple, imaginez que votre projet est un jeu de société comme Chess ou Battleship. Vous pouvez avoir différentes manières de représenter cela dans une couche de présentation (application console, service Web, interface graphique, etc.), mais vous avez également un domaine principal. Une classe que vous pourriez avoir est Coordinate
, représentant une position sur le tableau. La façon "diabolique" de l'écrire serait:
public class Coordinate
{
public int X {get; set;}
public int Y {get; set;}
}
(Je vais écrire des exemples de code en C # plutôt qu'en Java, par souci de concision et parce que je le connais mieux. J'espère que ce n'est pas un problème. Les concepts sont les mêmes et la traduction devrait être simple.)
Alors que les getters et les setters publics sont tous deux potentiellement problématiques, les setters sont beaucoup plus "mal" des deux. Ils sont également généralement les plus faciles à éliminer. Le processus est simple: définissez la valeur à partir du constructeur. Toutes les méthodes qui ont précédemment muté l'objet doivent plutôt renvoyer un nouveau résultat. Donc:
public class Coordinate
{
public int X {get; private set;}
public int Y {get; private set;}
public Coordinate(int x, int y)
{
X = x;
Y = y;
}
}
Notez que cela ne protège pas contre d'autres méthodes de la classe mutant X et Y. Pour être plus strictement immuable, vous pouvez utiliser readonly
(final
en Java). Mais dans tous les cas - que vous rendiez vos propriétés vraiment immuables ou que vous empêchiez simplement une mutation publique directe par le biais de setters - cela fait l'affaire de retirer vos setters publics. Dans la grande majorité des situations, cela fonctionne très bien.
Ce qui précède est très bien pour les setters, mais en termes de getters, nous nous sommes en fait tiré une balle dans le pied avant même de commencer. Notre processus consistait à penser à ce qu'est une coordonnée - les données qu'elles représentent - et à créer une classe autour de cela. Au lieu de cela, nous aurions dû commencer avec ce comportement dont nous avons besoin à partir d'une coordonnée. Soit dit en passant, ce processus est aidé par TDD, où nous n'extrayons des classes comme celle-ci qu'une fois que nous en avons besoin, nous commençons donc par le comportement souhaité et travaillons à partir de là.
Disons donc que le premier endroit où vous vous êtes retrouvé ayant besoin d'un Coordinate
était pour la détection de collision: vous vouliez vérifier si deux pièces occupaient le même espace sur la planche. Voici la voie "diabolique" (constructeurs omis pour des raisons de brièveté):
public class Piece
{
public Coordinate Position {get; private set;}
}
public class Coordinate
{
public int X {get; private set;}
public int Y {get; private set;}
}
//...And then, inside some class
public bool DoPiecesCollide(Piece one, Piece two)
{
return one.X == two.X && one.Y == two.Y;
}
Et voici le bon moyen:
public class Piece
{
private Coordinate _position;
public bool CollidesWith(Piece other)
{
return _position.Equals(other._position);
}
}
public class Coordinate
{
private readonly int _x;
private readonly int _y;
public bool Equals(Coordinate other)
{
return _x == other._x && _y == other._y;
}
}
(IEquatable
implémentation abrégée pour simplifier). En concevant pour le comportement plutôt que pour modéliser les données, nous avons réussi à supprimer nos getters.
Notez que cela s'applique également à votre exemple. Vous utilisez peut-être un ORM, ou affichez des informations client sur un site Web ou quelque chose, auquel cas une sorte de DTO Customer
aurait probablement du sens. Mais ce n'est pas parce que votre système comprend des clients et qu'ils sont représentés dans le modèle de données que vous devez automatiquement avoir une classe Customer
dans votre domaine. Peut-être que lorsque vous concevez le comportement, un émergera, mais si vous voulez éviter les getters, n'en créez pas de manière préventive.
Donc, ce qui précède est un bon début, mais tôt ou tard vous rencontrerez probablement une situation où vous avez un comportement associé à une classe, qui dépend en quelque sorte de l'état de la classe, mais qui n'appartient pas on la classe. Ce type de comportement est généralement présent dans la couche de service de votre application.
En prenant notre exemple Coordinate
, vous souhaiterez éventuellement représenter votre jeu à l'utilisateur, ce qui pourrait signifier dessiner à l'écran. Vous pouvez, par exemple, avoir un projet d'interface utilisateur qui utilise Vector2
Pour représenter un point à l'écran. Mais il serait inapproprié que la classe Coordinate
se charge de la conversion d'une coordonnée en un point sur l'écran - ce qui apporterait toutes sortes de problèmes de présentation dans votre domaine principal. Malheureusement, ce type de situation est inhérent à la conception OO.
La première option , qui est très couramment choisie, consiste simplement à exposer les fichus getters et à dire au diable avec elle. Cela présente l'avantage de la simplicité. Mais étant donné que nous parlons d'éviter les getters, disons pour le bien de l'argument que nous rejetons celui-ci et voyons quelles autres options il y a.
Une deuxième option consiste à ajouter une sorte de méthode .ToDTO()
à votre classe. Cela - ou similaire - pourrait bien être nécessaire de toute façon, par exemple lorsque vous souhaitez enregistrer le jeu, vous devez capturer à peu près tout votre état. Mais la différence entre le faire pour vos services et accéder directement au getter est plus ou moins esthétique. Il a toujours autant de "mal".
Une troisième option - que j'ai vue préconisée par Zoran Horvat dans quelques vidéos Pluralsight - consiste à utiliser une version modifiée version du modèle de visiteur. C'est une utilisation et une variation assez inhabituelles du modèle et je pense que le kilométrage des gens variera massivement selon qu'il ajoute de la complexité sans gain réel ou s'il s'agit d'un bon compromis pour la situation. L'idée est essentiellement d'utiliser le modèle de visiteur standard, mais de faire en sorte que les méthodes Visit
prennent l'état dont elles ont besoin comme paramètres, au lieu de la classe qu'elles visitent. Des exemples peuvent être trouvés ici .
Pour notre problème, une solution utilisant ce modèle serait:
public class Coordinate
{
private readonly int _x;
private readonly int _y;
public T Transform<T>(IPositionTransformer<T> transformer)
{
return transformer.Transform(_x,_y);
}
}
public interface IPositionTransformer<T>
{
T Transform(int x, int y);
}
//This one lives in the presentation layer
public class CoordinateToVectorTransformer : IPositionTransformer<Vector2>
{
private readonly float _tileWidth;
private readonly float _tileHeight;
private readonly Vector2 _topLeft;
Vector2 Transform(int x, int y)
{
return _topLeft + new Vector2(_tileWidth*x + _tileHeight*y);
}
}
Comme vous pouvez probablement le constater, _x
Et _y
Ne sont plus vraiment encapsulés. Nous pourrions les extraire en créant un IPositionTransformer<Tuple<int,int>>
Qui les renvoie directement. Selon le goût, vous pouvez penser que cela rend tout l'exercice inutile.
Cependant, avec les getters publics, il est très facile de faire les choses dans le mauvais sens, en extrayant directement les données et en les utilisant en violation de Tell, Don't Ask . Alors qu'en utilisant ce modèle, c'est en fait plus simple pour le faire de la bonne façon: lorsque vous voulez créer un comportement, vous commencerez automatiquement par créer un type qui lui est associé. Les violations du TDA seront très manifestement malodorantes et nécessiteront probablement de contourner une solution plus simple et meilleure. Dans la pratique, ces points permettent de le faire de la bonne façon, OO, de la manière "mal" que les getters encouragent.
Enfin , même si ce n'est pas évident au départ, il peut en fait y avoir des moyens d'exposer assez de ce que vous besoin comme comportement pour éviter d'exposer l'état. Par exemple, en utilisant notre version précédente de Coordinate
dont le seul membre public est Equals()
(en pratique, il faudrait une implémentation complète de IEquatable
), vous pourriez écrire la classe suivante dans votre couche de présentation:
public class CoordinateToVectorTransformer
{
private Dictionary<Coordinate,Vector2> _coordinatePositions;
public CoordinateToVectorTransformer(int boardWidth, int boardHeight)
{
for(int x=0; x<boardWidth; x++)
{
for(int y=0; y<boardWidth; y++)
{
_coordinatePositions[new Coordinate(x,y)] = GetPosition(x,y);
}
}
}
private static Vector2 GetPosition(int x, int y)
{
//Some implementation goes here...
}
public Vector2 Transform(Coordinate coordinate)
{
return _coordinatePositions[coordinate];
}
}
Il s'avère, peut-être de manière surprenante, que tout le comportement dont nous vraiment avions besoin à partir d'une coordonnée pour atteindre notre objectif était la vérification de l'égalité! Bien sûr, cette solution est adaptée à ce problème et fait des hypothèses sur l'utilisation/les performances de la mémoire acceptables. C'est juste un exemple qui correspond à ce domaine de problème particulier, plutôt qu'un plan pour une solution générale.
Et encore une fois, les opinions varieront selon que, dans la pratique, cette complexité est inutile. Dans certains cas, aucune solution de ce type ne peut exister, ou elle peut être trop étrange ou complexe, auquel cas vous pouvez revenir aux trois ci-dessus.
Le moyen le plus simple d'éviter les setters est de remettre les valeurs à la méthode constructeur lorsque vous new
placez l'objet. C'est aussi la méthode habituelle modèle lorsque vous voulez faire un objet immuable. Cela dit, les choses ne sont pas toujours aussi claires dans le monde réel.
Il est vrai que les méthodes devraient porter sur le comportement. Cependant, certains objets, comme Customer, existent principalement pour conserver les informations. Ce sont les types d'objets qui bénéficient le plus des getters et setters; s'il n'y avait aucun besoin de telles méthodes, nous les éliminerions tout simplement.
Lectures complémentaires
Quand les Getters et Setters sont-ils justifiés
C'est parfaitement bien d'avoir un objet qui expose des données plutôt qu'un comportement. Nous l'appelons simplement un "objet de données". Le modèle existe sous des noms comme Data Transfer Object ou Value Object. Si l'objet de l'objet est de conserver des données, les getters et setters sont valides pour accéder aux données.
Alors pourquoi quelqu'un dirait-il que "les méthodes getter et setter sont mauvaises"? Vous le verrez souvent - quelqu'un prend une directive qui est parfaitement valide dans un contexte spécifique, puis supprime le contexte afin d'obtenir un titre plus percutant. Par exemple, " privilégier la composition à l'héritage " est un bon principe, mais bientôt quelqu'un va supprimer le contexte et écrire " Pourquoi les extensions sont mauvaises " (hé, même auteur , quelle coïncidence!) ou " l'héritage est mauvais et doit être détruit ".
Si vous regardez le contenu de l'article, il contient en fait des points valides, il étend simplement le point pour faire un titre de clics. Par exemple, l'article indique que les détails de l'implémentation ne doivent pas être exposés. Ce sont les principes d'encapsulation et de masquage des données qui sont fondamentaux en OO. Cependant, une méthode getter n'expose pas par définition les détails d'implémentation. Dans le cas d'un objet de données Client , les propriétés de Nom , L'adresse etc. ne sont pas des détails d'implémentation mais plutôt le but de l'objet et doivent faire partie de l'interface publique.
Lisez le suite de l'article auquel vous vous connectez, pour voir comment il suggère de définir des propriétés telles que 'nom' et 'salaire' sur un objet 'Employé' sans utiliser les mauvais régleurs. Il s'avère qu'il utilise un modèle avec un "exportateur" qui est rempli de méthodes appelées add Name, add Salaire qui définit à son tour des champs du même nom ... Donc, finalement, il finit par utiliser exactement le modèle de définition, juste avec une convention de dénomination différente.
Cela revient à penser que vous évitez les pièges des singletons en les renommant en tant que seules choses tout en conservant la même implémentation.
Pour transformer la classe Customer
- à partir d'un objet de données, nous pouvons nous poser les questions suivantes sur les champs de données:
Comment voulons-nous utiliser {champ de données}? Où est utilisé {champ de données}? Peut-on et doit-on déplacer l'utilisation de {champ de données} dans la classe?
Par exemple.:
Quel est le but de Customer.Name
?
Réponses possibles, affichez le nom dans une page Web de connexion, utilisez le nom dans les mailings au client.
Ce qui conduit à des méthodes:
Quel est le but de Customer.DOB
?
Validation de l'âge du client. Remises sur l'anniversaire du client. Mailings.
Compte tenu des commentaires, l'exemple d'objet Customer
- à la fois comme objet de données et comme objet "réel" avec ses propres responsabilités - est trop large; c'est-à-dire qu'il a trop de propriétés/responsabilités. Ce qui conduit soit à beaucoup de composants en fonction de Customer
(en lisant ses propriétés) soit à Customer
en fonction de beaucoup de composants. Peut-être existe-t-il différentes vues du client, peut-être que chacune devrait avoir sa propre classe distincte1:
Le client dans le cadre de Account
et de transactions monétaires est probablement uniquement utilisé pour:
Account
s.Ce client n'a pas besoin de champs comme DOB
, FavouriteColour
, Tel
, et peut-être même pas Address
.
Le client dans le cadre d'un utilisateur se connectant à un site bancaire.
Les champs pertinents sont:
FavouriteColour
, qui pourrait prendre la forme d'un thème personnalisé;LanguagePreferences
, etGreetingName
Au lieu de propriétés avec des getters et setters, celles-ci peuvent être capturées dans une seule méthode:
Le client dans le cadre du marketing et du mailing personnalisé.
Ici, non pas en s'appuyant sur les propriétés d'un objet de données, mais plutôt à partir des responsabilités de l'objet; par exemple.:
Le fait que cet objet client possède une propriété FavouriteColour
et/ou une propriété Address
devient sans importance: l'implémentation utilise peut-être ces propriétés; mais il peut également utiliser certaines techniques d'apprentissage automatique et utiliser les interactions précédentes avec le client pour découvrir les produits qui pourraient l'intéresser.
1. Bien sûr, les classes Customer
et Account
étaient des exemples, et pour un exemple simple ou un exercice de devoirs, le fractionnement de ce client pourrait être exagéré, mais avec l'exemple du fractionnement, j'espère démontrer que la méthode pour transformer un objet de données en un objet avec des responsabilités fonctionnera.
TL; DR
La modélisation du comportement est bonne.
Il est préférable de modéliser pour de bonnes (!) Abstractions.
Parfois, des objets de données sont requis.
Comportement et abstraction
Il y a plusieurs raisons d'éviter les getters et les setters. L'une, comme vous l'avez noté, est d'éviter la modélisation des données. C'est en fait la raison mineure. La raison principale est de fournir l'abstraction.
Dans votre exemple avec le compte bancaire qui est clair: Une méthode setBalance()
serait vraiment mauvaise car la définition d'un solde n'est pas celle pour laquelle un compte doit être utilisé. Le comportement du compte doit autant que possible faire abstraction de son solde actuel. Il peut prendre en compte le solde pour décider d'échouer un retrait, il peut donner accès au solde actuel, mais la modification de l'interaction avec un compte bancaire ne devrait pas obliger l'utilisateur à calculer le nouveau solde. C'est ce que le compte doit faire lui-même.
Même une paire de méthodes deposit()
et withdraw()
n'est pas idéale pour modéliser un compte bancaire. Une meilleure façon serait de ne fournir qu'une seule méthode transfer()
qui prend un autre compte et un montant comme arguments. Cela permettrait à la classe de compte de s'assurer trivialement que vous ne créez/détruisez pas accidentellement de l'argent dans votre système, cela fournirait une abstraction très utilisable et fournirait en fait aux utilisateurs plus d'informations car cela forcerait l'utilisation de comptes spéciaux pour l'argent gagné/investi/perdu (voir double comptabilité ). Bien sûr, toutes les utilisations d'un compte n'ont pas besoin de ce niveau d'abstraction, mais cela vaut vraiment la peine de considérer la quantité d'abstraction que vos classes peuvent fournir.
Notez que fournir l'abstraction et masquer les données internes n'est pas toujours la même chose. Presque toutes les applications contiennent des classes qui ne sont en fait que des données. Les tuples, les dictionnaires et les tableaux sont des exemples fréquents. Vous ne voulez pas cacher la coordonnée x d'un point à l'utilisateur. Il y a très peu d'abstraction que vous pouvez/devriez faire avec un point.
La classe client
Un client est certainement une entité de votre système qui devrait essayer de fournir des abstractions utiles. Par exemple, il devrait probablement être associé à un panier d'achat, et la combinaison du panier et du client devrait permettre de valider un achat, ce qui pourrait déclencher des actions telles que lui envoyer les produits demandés, lui facturer de l'argent (en tenant compte du paiement sélectionné). méthode), etc.
Le hic, c'est que toutes les données que vous avez mentionnées ne sont pas seulement associées à un client, toutes ces données sont également modifiables. Le client peut déménager. Ils peuvent changer de compagnie de carte de crédit. Ils peuvent changer leur adresse e-mail et leur numéro de téléphone. Heck, ils peuvent même changer de nom et/ou de sexe! Ainsi, une classe de clients complète doit en effet fournir un accès de modification complet à tous ces éléments de données.
Pourtant, les setters peuvent/devraient fournir des services non triviaux: ils peuvent assurer le format correct des adresses e-mail, la vérification des adresses postales, etc. De même, les "getters" peuvent fournir des services de haut niveau comme fournir des adresses e-mail dans le Name <[email protected]>
format en utilisant les champs de nom et l'adresse e-mail déposée, ou fournir une adresse postale correctement formatée, etc. Bien sûr, ce que cette fonctionnalité de haut niveau a de sens dépend fortement de votre cas d'utilisation. Ce pourrait être une surpuissance totale, ou cela pourrait demander à une autre classe de le faire correctement. Le choix du niveau d'abstraction n'est pas facile.
En essayant de développer la réponse de Kasper, il est plus facile de s'énerver et d'éliminer les setters. Dans un argument plutôt vague, ondulant (et si tout va bien humoristique):
Quand Customer.Name changerait-il jamais?
Rarement. Ils se sont peut-être mariés. Ou est entré dans la protection des témoins. Mais dans ce cas, vous voudrez également vérifier et éventuellement modifier leur résidence, leurs proches et d'autres informations.
Quand le DOB changerait-il un jour?
Uniquement lors de la création initiale, ou sur une erreur de saisie de données. Ou s'ils sont un joueur de baseball Domincan. :-)
Ces champs ne doivent pas être accessibles avec des setters normaux de routine. Vous disposez peut-être d'une méthode Customer.initialEntry()
ou d'une méthode Customer.screwedUpHaveToChange()
qui nécessite des autorisations spéciales. Mais ne disposez pas d'une méthode publique Customer.setDOB()
.
Habituellement, un client est lu à partir d'une base de données, une API REST, du XML, peu importe. Avoir une méthode Customer.readFromDB()
, ou, si vous êtes plus strict sur SRP/séparation des préoccupations , vous auriez un générateur séparé, par exemple un objet CustomerPersister
avec une méthode read()
. En interne, ils définissent en quelque sorte les champs (je préfère utiliser l'accès au package ou une classe interne, YMMV). Mais encore une fois, évitez les colons publics.
(Addendum car la question a quelque peu changé ...)
Disons que votre application fait un usage intensif des bases de données relationnelles. Il serait insensé d'avoir des méthodes Customer.saveToMYSQL()
ou Customer.readFromMYSQL()
. Cela crée un couplage indésirable avec une entité concrète, non standard et susceptible de changer. Par exemple, lorsque vous modifiez le schéma ou passez à Postgress ou Oracle.
Cependant, OMI, il est parfaitement acceptable de coupler le client à un standard abstrait, ResultSet
. Un objet d'assistance séparé (je l'appellerai CustomerDBHelper
, qui est probablement une sous-classe de AbstractMySQLHelper
) connaît toutes les connexions complexes à votre base de données, connaît les détails d'optimisation délicats, connaît les tables, requête, jointures, etc ... (ou utilise un ORM comme Hibernate) pour générer le ResultSet. Votre objet parle au ResultSet
, qui est un standard abstrait, peu susceptible de changer. Lorsque vous modifiez la base de données sous-jacente ou modifiez le schéma, le client ne change pas, mais CustomerDBHelper le fait. Si vous avez de la chance, c'est uniquement AbstractMySQLHelper qui change qui effectue automatiquement les changements pour le client, le marchand, l'expédition, etc.
De cette façon, vous pouvez (peut-être) éviter ou réduire le besoin de getters et setters.
Et, le point principal de l'article Holub, comparez et comparez ce qui précède à ce que ce serait si vous utilisiez des getters et des setters pour tout et changiez la base de données.
De même, disons que vous utilisez beaucoup de XML. OMI, c'est bien de coupler votre client à une norme abstraite, comme un Python xml.etree.ElementTree ou un Java org.w3c.dom.Element. Le client obtient et se définit à partir de cela. Encore une fois, vous pouvez (peut-être) réduire le besoin de getters et setters.
Le problème d'avoir des getters et des setters peut être dû au fait qu'une classe peut être utilisée dans la logique métier d'une manière, mais vous pouvez également avoir des classes auxiliaires pour sérialiser/désérialiser les données d'une base de données ou d'un fichier ou d'un autre stockage persistant.
En raison du fait qu'il existe de nombreuses façons de stocker/récupérer vos données et que vous souhaitez dissocier les objets de données de la façon dont ils sont stockés, l'encapsulation peut être "compromise" soit en rendant ces membres publics, soit en les rendant accessibles via des getters et setters qui est presque aussi mauvais que de les rendre publics.
Il existe différentes manières de contourner ce problème. Une façon consiste à mettre les données à la disposition d'un "ami". Bien que l'amitié ne soit pas héritée, cela peut être surmonté par le sérialiseur qui demande les informations à l'ami, c'est-à-dire le sérialiseur de base "transmettant" les informations.
Votre classe peut avoir une méthode générique "fromMetadata" ou "toMetadata". À partir des métadonnées, un objet est construit et peut donc être un constructeur. S'il s'agit d'un langage typé dynamiquement, les métadonnées sont assez standard pour un tel langage et sont probablement le principal moyen de construire de tels objets.
Si votre langage est spécifiquement C++, une solution consiste à avoir une "structure" publique de données, puis à votre classe d'avoir une instance de cette "structure" en tant que membre et en fait toutes les données que vous allez stocker/récupérer pour y être stocké. Vous pouvez ensuite facilement écrire des "wrappers" pour lire/écrire vos données dans plusieurs formats.
Si votre langage est C # ou Java qui n'a pas de "structs" alors vous pouvez faire de même mais votre struct est maintenant une classe secondaire. Il n'y a pas de véritable concept de "propriété" des données ou const-ness donc si vous donnez une instance de la classe contenant vos données et que tout est public, tout ce qui est mis en attente peut le modifier. Vous pouvez le "cloner" bien que cela puisse être coûteux. Sinon, vous pouvez faire en sorte que cette classe ait des données privées mais utilisez des accesseurs. Cela donne aux utilisateurs de votre classe un moyen détourné d'accéder aux données, mais ce n'est pas l'interface directe avec votre classe et c'est vraiment un détail dans le stockage des données de la classe qui est également un cas d'utilisation.
J'ajoute ici mes 2 cents en mentionnant objets parlant SQL approche.
Cette approche est basée sur la notion d'objet autonome. Il dispose de toutes les ressources nécessaires pour mettre en œuvre son comportement. Il n'est pas nécessaire de lui dire comment faire son travail - une requête déclarative suffit. Et un objet n'a certainement pas à contenir toutes ses données en tant que propriétés de classe. Peu importe - et ne devrait pas - importe d'où ils viennent.
Parler d'un agrégat , l'immuabilité n'est pas un problème non plus. Disons, vous avez une séquence d'états que l'agrégat peut contenir: Il est tout à fait correct d'implémenter chaque état en tant qu'objet autonome. Vous pourriez probablement aller encore plus loin: discutez avec votre expert en domaine. Il est probable qu'il ne considère pas cet agrégat comme une entité unifiée. Probablement chaque état a sa propre signification, mérite son propre objet.
Enfin, je voudrais noter que le processus de recherche d'objets est très similaire avec décomposition du système en sous-systèmes . Les deux sont basés sur le comportement, pas autre chose.
La POO consiste à encapsuler et à masquer le comportement à l'intérieur des objets. Les objets sont des boîtes noires. C'est une façon de concevoir quelque chose. L'atout est dans de nombreux cas, on n'a pas besoin de connaître l'état interne d'un autre composant et il vaut mieux ne pas avoir à le savoir. Vous pouvez appliquer cette idée avec principalement des interfaces ou à l'intérieur d'un objet avec visibilité et en prenant soin que seuls les verbes/actions autorisés sont disponibles pour l'appelant.
Cela fonctionne bien pour une sorte de problème. Par exemple, dans les interfaces utilisateur pour modéliser un composant d'interface utilisateur individuel. Lorsque vous interagissez avec une zone de texte, vous êtes uniquement intéressé à définir le texte, à l'obtenir ou à écouter l'événement de modification de texte. Vous n'êtes généralement pas intéressé par la position du curseur, la police utilisée pour dessiner le texte ou la façon dont le clavier est utilisé. L'encapsulation offre beaucoup ici.
Au contraire, lorsque vous appelez un service réseau, vous fournissez une entrée explicite. Il y a généralement une grammaire (comme en JSON ou XML) et toutes les options d'appeler le service n'ont aucune raison d'être cachées. L'idée est que vous pouvez appeler le service comme vous le souhaitez et que le format des données est public et publié.
Dans ce cas, ou dans bien d'autres (comme l'accès à une base de données), vous travaillez vraiment avec des données partagées. En tant que tel, il n'y a aucune raison de le cacher, au contraire, vous voulez le rendre disponible. Il peut y avoir des problèmes d'accès en lecture/écriture ou de cohérence de la vérification des données, mais à ce cœur, le concept de base s'il est public.
Pour une telle exigence de conception où vous voulez éviter l'encapsulation et rendre les choses publiques et en clair, vous voulez éviter les objets. Ce dont vous avez vraiment besoin, ce sont des tuples, des structures C ou leurs équivalents, pas des objets.
Mais cela se produit également dans des langages comme Java, les seules choses que vous pouvez modéliser sont des objets ou des tableaux d'objets. Les objets eux-mêmes peuvent contenir quelques types natifs (int, float ...) mais c'est tout. Mais les objets peuvent également se comporter comme une simple structure avec uniquement des champs publics et tout cela.
Donc, si vous modélisez des données, vous pouvez le faire avec uniquement des champs publics à l'intérieur des objets, car vous n'en avez pas besoin de plus. Vous n'utilisez pas l'encapsulation parce que vous n'en avez pas besoin. Cela se fait de cette façon dans de nombreuses langues. En Java, historiquement, une rose standard où avec getter/setter vous pouviez au moins avoir un contrôle en lecture/écriture (en n'ajoutant pas setter par exemple) et que les outils et le framework en utilisant l'API instrospection rechercheraient des méthodes getter/setter et les utiliseraient pour remplir automatiquement le contenu ou afficher ces thèses sous forme de champs modifiables dans l'interface utilisateur générée automatiquement.
Il y a aussi l'argument que vous pourriez ajouter de la logique/vérification dans la méthode setter.
En réalité, il n'y a presque aucune justification pour les getter/setters car ils sont le plus souvent utilisés pour modéliser des données pures. Les cadres et les développeurs utilisant vos objets s'attendent à ce que le getter/setter ne fasse rien de plus que la définition/l'obtention des champs de toute façon. En fait, vous ne faites pas plus avec getter/setter que ce qui pourrait être fait avec les champs publics.
Mais ce sont de vieilles habitudes et les vieilles habitudes sont difficiles à éliminer ... Vous pouvez même être menacé par vos collègues ou votre professeur si vous ne mettez pas aveuglément les accesseurs/poseurs partout s'ils n'ont pas le fond pour mieux comprendre ce qu'ils sont et ce qu'ils sont ne pas.
Vous auriez probablement besoin de changer la langue pour obtenir le code de passe-partout de ces getters/setters. (Comme C # ou LISP). Pour moi, les getters/setter ne sont qu'une autre erreur d'un milliard de dollars ...
Ainsi, par exemple, étant donné une classe Client qui garde beaucoup d'informations sur le client, par exemple Nom, date de naissance, numéro de téléphone, adresse, etc., comment éviterait-on aux getter/setters d'obtenir et de définir tous ces attributs? Quelle méthode de type "Comportement" peut-on écrire pour remplir toutes ces données?
Je pense que cette question est épineuse parce que vous vous inquiétez des méthodes de comportement pour le remplissage des données, mais je ne vois aucune indication sur le comportement que la classe d'objets Customer
est destinée à encapsuler.
Ne confondez pas Customer
en tant que classe d'objets avec 'Customer' en tant qu'utilisateur / acteur qui effectue différentes tâches en utilisant votre logiciel.
Lorsque vous dites étant donné une classe Customer qui garde beaucoup d'informations sur le client alors en ce qui concerne le comportement, il semble que votre classe Customer ne la distingue guère d'une roche. Un Rock
peut avoir une couleur, vous pouvez lui donner un nom, vous pouvez avoir un champ pour stocker son adresse actuelle mais nous n'attendons aucune sorte de comportement intelligent d'un rocher.
De l'article lié sur les getters/setters étant mauvais:
Le processus de conception OO se concentre sur les cas d'utilisation: un utilisateur exécute des tâches autonomes qui ont des résultats utiles. (La connexion n'est pas un cas d'utilisation car il manque un résultat utile dans le domaine de problème. chèque de paie est un cas d'utilisation.) Un système OO, alors, implémente les activités nécessaires pour jouer les différents scénarios qui composent un cas d'utilisation.
Sans aucun comportement défini, faire référence à un rocher comme Customer
ne change pas le fait qu'il ne s'agit que d'un objet avec certaines propriétés que vous souhaitez suivre et peu importe les tours que vous souhaitez jouer pour s'éloigner des getters et des setters. Un rocher ne se soucie pas d'avoir un nom valide et un rocher ne devrait pas savoir si une adresse est valide ou non.
Votre système de commande peut associer un Rock
à un bon de commande et tant que Rock
a une adresse définie, une partie du système peut s'assurer qu'un article est livré à un rocher.
Dans tous ces cas, le Rock
n'est qu'un objet de données et continuera de l'être jusqu'à ce que nous définissions des comportements spécifiques avec des résultats utiles au lieu d'hypothétiques.
Essayez ceci:
Lorsque vous évitez de surcharger le mot "client" avec 2 significations potentiellement différentes, cela devrait rendre les choses plus faciles à conceptualiser.
Un objet Rock
place-t-il une commande ou est-ce quelque chose qu'un être humain fait en cliquant sur les éléments de l'interface utilisateur pour déclencher des actions dans votre système?