OOP en images

La programmation orientée objet (POO) est devenue une partie intégrante du développement de nombreux projets modernes, mais malgré sa popularité, ce paradigme est loin d'être le seul. Si vous savez déjà comment travailler avec d'autres paradigmes et que vous souhaitez vous familiariser avec l'occultisme de la POO, alors devant vous est un petit longrid et deux mégaoctets d'images et d'animations. Les transformateurs serviront d'exemples.



La première chose à répondre est pourquoi? L'idéologie orientée objet a été développée comme une tentative de connecter le comportement d'une entité à ses données et de projeter des objets et des processus métier du monde réel dans du code de programme. On pensait qu'un tel code est plus facile à lire et à comprendre par une personne, car il est courant que les gens perçoivent le monde environnant comme une multitude d'objets interagissant les uns avec les autres, se prêtant à une certaine classification. Est-il possible pour les idéologues d'atteindre l'objectif, il est difficile de répondre sans équivoque, mais de facto, nous avons beaucoup de projets dans lesquels le programmeur aura besoin de POO.

Vous ne devez pas penser que l'OOP accélérera miraculeusement la rédaction des programmes et s'attendra à une situation où les habitants de Villaribo ont déjà mis en œuvre le projet OOP et où les habitants de Villabaggio blanchissent toujours le code des spaghettis en gras. Dans la plupart des cas, ce n'est pas le cas et le temps est gagné non pas au stade du développement, mais aux étapes de support (expansion, modification, débogage et test), c'est-à-dire à long terme. Si vous devez écrire un script à usage unique qui ne nécessite pas de prise en charge ultérieure, la POO dans cette tâche n'est probablement pas utile. Cependant, une partie importante du cycle de vie de la plupart des projets modernes est précisément le soutien et l'expansion. Le fait d'avoir la POO seule ne rend pas votre architecture parfaite et vice versa peut entraîner des complications inutiles.

Parfois, vous pouvez rencontrer des critiques sur la performance des programmes POO. Il est vrai qu'un léger surcoût est présent, mais si insignifiant que dans la plupart des cas il peut être négligé au profit d'avantages. Néanmoins, dans les goulots d'étranglement où des millions d'objets par seconde doivent être créés ou traités dans un seul thread, il vaut au moins revoir la nécessité de la POO, car même une surcharge minimale en de telles quantités peut affecter considérablement les performances. Le profilage vous aidera à saisir la différence et à prendre une décision. Dans d'autres cas, par exemple, où la part du lion de la vitesse repose sur les E / S, l'abandon d'objets sera une optimisation prématurée.

De par sa nature même, la programmation orientée objet est mieux expliquée par des exemples. Comme promis, nos patients seront des transformateurs. Je ne suis pas un transformateur, et je n'ai pas lu de bande dessinée, par conséquent, dans les exemples, je serai guidé par Wikipedia et la fantaisie.

Classes et objets


Digression immédiatement lyrique: une approche orientée objet est possible sans classes, mais nous considérerons, je m'excuse pour le jeu de mots, le schéma classique, où les classes sont notre tout.

L'explication la plus simple: une classe est un dessin d'un transformateur, et les instances de cette classe sont des transformateurs spécifiques, par exemple, Optimus Prime ou Oleg. Et bien qu'ils soient assemblés selon un dessin, ils peuvent marcher, se transformer et tirer de la même manière, ils ont tous deux leur propre état. Un état est une série de propriétés changeantes. Par conséquent, dans deux objets différents de la même classe, nous pouvons observer un nom, un âge, un emplacement, un niveau de charge, une quantité de munitions différents, etc. L'existence même de ces propriétés et de leurs types est décrite dans la classe.

Ainsi, une classe est une description des propriétés et du comportement d'un objet. Et un objet est une instance avec son propre état de ces propriétés.

Nous disons «propriétés et comportement», mais cela semble en quelque sorte abstrait et incompréhensible. Il sera plus familier pour un programmeur de sonner comme ceci: «variables et fonctions». En fait, les «propriétés» sont les mêmes variables ordinaires, ce sont simplement des attributs d'un objet (on les appelle des champs d'objet). De même, le «comportement» est la fonction d'un objet (on les appelle des méthodes), qui sont également des attributs de l'objet. La différence entre la méthode de l'objet et la fonction habituelle est seulement que la méthode a accès à son propre état via les champs.

Au total, nous avons des méthodes et des propriétés qui sont des attributs. Comment travailler avec des attributs? Dans la plupart des PL, l'opérateur de référence d'attribut est le point (sauf pour PHP et Perl). Cela ressemble à ceci (pseudo-code):

//       class class Transformer(){ //   x int x //    (     0) function constructor(int x){ //   x // (  0    ) this.x = x } //   run function run(){ //      this this.x += 1 } } //    : //        0 optimus = new Transformer(0) optimus.run() //    print optimus.x //  1 optimus.run() //      print optimus.x //  2 

Dans les images, j'utiliserai la notation suivante:



Je n'ai pas utilisé de diagrammes UML, les considérant insuffisamment visuels, bien que plus flexibles.


Animation numéro 1

Que voyons-nous du code?

1. il s'agit d'une variable locale spéciale (à l'intérieur des méthodes) qui permet à un objet d'accéder à ses propres attributs à partir de ses méthodes. J'attire votre attention uniquement sur la vôtre, c'est-à-dire lorsque le transformateur appelle sa propre méthode ou change son propre état. Si l'appel ressemble à ceci à l'extérieur: optimus.x , alors de l'intérieur, si Optimus veut accéder à son champ x lui-même, dans sa méthode, l'appel ressemblera à this.x , c'est-à-dire " I (Optimus) fait référence à mon attribut x ". Dans la plupart des langues, cette variable est appelée ceci, mais il existe des exceptions (par exemple, self)

2. constructeur est une méthode spéciale qui est automatiquement appelée lors de la création d'un objet. Le constructeur peut accepter n'importe quel argument, comme toute autre méthode. Dans chaque langue, un constructeur est indiqué par son nom. Quelque part, ce sont des noms spécialement réservés comme __construct ou __init__, et quelque part le nom du constructeur doit correspondre au nom de la classe. Le but des constructeurs est d'initialiser l'objet, de remplir les champs obligatoires.

3. new est un mot-clé qui doit être utilisé pour créer une nouvelle instance d'une classe. À ce stade, un objet est créé et le constructeur est appelé. Dans notre exemple, 0 est passé au constructeur comme position de départ du transformateur (c'est l'initialisation ci-dessus). Le nouveau mot-clé manque dans certains langages et le constructeur est appelé automatiquement lors de la tentative d'appeler la classe en tant que fonction, par exemple: Transformer ().

4. Le constructeur et les méthodes d' exécution fonctionnent avec l'état interne, mais à tous autres égards ne diffèrent pas des fonctions ordinaires . Même la syntaxe de la déclaration correspond.

5. Les classes peuvent posséder des méthodes qui n'ont pas besoin d'état et, par conséquent, créer un objet. Dans ce cas, la méthode est rendue statique .

Srp


(Principe de responsabilité unique / Premier principe SOLIDE ). Vous le connaissez probablement déjà dans d'autres paradigmes: «une fonction ne doit exécuter qu'une seule action terminée». Ce principe est également valable pour les classes: "Une classe doit être responsable de n'importe quelle tâche." Malheureusement avec les classes, il est plus difficile de définir la ligne qui doit être franchie pour que le principe soit violé.

Il y a des tentatives pour formaliser ce principe en décrivant le but d'une classe avec une phrase sans unions, mais c'est une technique très controversée, alors faites confiance à votre intuition et ne vous précipitez pas à l'extrême. Vous n'avez pas besoin de fabriquer un couteau suisse à partir d'une classe, mais produire un million de classes avec une méthode à l'intérieur est également stupide.

L'association


Traditionnellement, dans les champs d'un objet, non seulement les variables ordinaires de types standard peuvent être stockées, mais aussi d'autres objets. Et ces objets peuvent à leur tour stocker d'autres objets et ainsi de suite, formant un arbre (parfois un graphique) d'objets. Cette relation est appelée association.

Supposons que notre transformateur soit équipé d'un pistolet. Bien que non, mieux avec deux pistolets. Dans chaque main. Les pistolets sont les mêmes (ils appartiennent à la même classe ou, si vous voulez, fabriqués selon un dessin), les deux peuvent tirer et recharger de manière égale, mais chacun a son propre stockage de munitions (propre état). Comment le décrire maintenant dans la POO? Par association:

 class Gun(){ //    int ammo_count //    function constructor(){ //  this.reload() //    "" } function fire(){ //    "" this.ammo_count -= 1 //      } function reload(){ //   "" this.ammo_count = 10 //     } } class Transformer(){ //    Gun gun_left //   " "   Gun gun_right //   " "    /*            ,    */ function constructor(Gun gun_left, Gun gun_right){ this.gun_left = gun_left //      this.gun_right = gun_right //      } //    "",   ... function fire(){ //  ,    "" this.gun_left.fire() //    ,     "" this.gun_right.fire() } } gun1 = new Gun() //    gun2 = new Gun() //    optimus = new Transformer(gun1, gun2) //  ,     


Animation numéro 2

this.gun_left.fire () et this.gun_right.fire () sont des appels à des objets enfants qui se produisent également via des points. Au premier point, nous nous tournons vers l'attribut de nous-mêmes (this.gun_right), obtenant l'objet pistolet, et au deuxième point, nous nous tournons vers la méthode de l'objet pistolet (this.gun_right.fire ()).

En résumé: le robot a été fabriqué, l'arme de service a été émise, nous allons maintenant comprendre ce qui se passe ici. Dans ce code, un objet est devenu partie intégrante d'un autre objet. Voici l'association. Il est à son tour de deux types:

1. Composition - le cas où, à l'usine de transformateurs, récupérant Optimus, les deux pistolets sont fermement attachés à ses mains avec des clous, et après la mort d'Optimus, les pistolets meurent avec lui. En d'autres termes, le cycle de vie de l'enfant est le même que le cycle de vie du parent.

2. Agrégation - le cas où le pistolet est délivré comme un pistolet dans la main et après la mort d'Optimus, ce pistolet peut être ramassé par son camarade Oleg, puis pris en main ou transformé en prêteur sur gages. Autrement dit, le cycle de vie d'un objet enfant ne dépend pas du cycle de vie du parent et peut être utilisé par d'autres objets.

L'église OOP orthodoxe nous prêche une trinité fondamentale - encapsulation, polymorphisme et héritage , sur laquelle repose toute l'approche orientée objet. Trions-les dans l'ordre.



Héritage


L'héritage est un mécanisme système qui permet, aussi paradoxal que cela puisse paraître, d'hériter des propriétés et du comportement d'autres classes par certaines classes pour une expansion ou une modification ultérieure.

Et si nous ne voulons pas emboutir les mêmes transformateurs, mais que nous voulons créer un cadre commun, mais avec un kit carrosserie différent? La POO nous permet une telle farce en divisant la logique en similitudes et différences, avec suppression ultérieure des similitudes dans la classe parente et des différences dans les classes descendantes. À quoi ça ressemble?

Optimus Prime et Megatron sont tous deux des transformateurs, mais l'un est un Autobot et l'autre est un Decepticon. Supposons que les différences entre les Autobots et les Decepticons consistent uniquement dans le fait que les Autobots sont transformés en voitures et les Decepticons - en aviation. Toutes les autres propriétés et comportements ne feront aucune différence. Dans ce cas, le système d'héritage peut être conçu comme suit: les caractéristiques communes (course à pied, prise de vue) seront décrites dans la classe de base Transformer et les différences (transformation) dans les deux classes enfants Autobot et Decepticon.

 class Transformer(){ //   function run(){ // ,    } function fire(){ // ,    } } class Autobot(Transformer){ //  ,   Transformer function transform(){ // ,      } } class Decepticon(Transformer){ //  ,   Transformer function transform(){ // ,      } } optimus = new Autobot() megatron = new Decepticon() 


Animation numéro 3

Cet exemple illustre comment l'héritage devient l'un des moyens de dédupliquer du code ( principe DRY ) à l'aide de la classe parente, tout en offrant des opportunités de mutation dans les classes descendantes.

Surcharge


Si vous remplacez une méthode existante dans la classe parent dans la classe parent, la surcharge fonctionnera. Cela nous permet de ne pas compléter le comportement de la classe parente, mais de le modifier. Au moment d'appeler la méthode ou d'accéder au champ de l'objet, la recherche de l'attribut se produit du descendant à la racine même - le parent. Autrement dit, si la méthode fire () est appelée sur l'autobot, la méthode est d'abord recherchée dans la classe descendante - Autobot, et puisqu'elle n'est pas là, la recherche monte d'un cran plus haut - vers la classe Transformer, où elle sera détectée et appelée.

Utilisation inappropriée


Il est curieux qu'une hiérarchie excessivement profonde de l'héritage puisse conduire à l'effet inverse - complication lorsque l'on cherche à savoir qui est hérité de qui, et quelle méthode est appelée dans quel cas. De plus, toutes les exigences architecturales ne peuvent pas être mises en œuvre à l'aide de l'héritage. Par conséquent, l'héritage doit être appliqué sans fanatisme. Il existe des recommandations appelant à une composition préférée à l'héritage, le cas échéant. Toute critique de l'héritage que j'ai rencontrée est renforcée par des exemples infructueux lorsque l'héritage est utilisé comme un marteau d'or . Mais cela ne signifie pas du tout que l'héritage est toujours nuisible en principe. Mon narcologue a dit que la première étape consiste à admettre que vous dépendez de l'héritage.

En décrivant les relations de deux entités, comment déterminer quand l'héritage est approprié et quand la composition est-elle appropriée? Vous pouvez utiliser la feuille de triche populaire: demandez-vous, l' entité A est l'entité B ? Si c'est le cas, l'héritage le plus probable est approprié. Si l' entité A fait partie de l'entité B , alors notre choix est la composition.

Par rapport à notre situation, cela ressemblera à ceci:

  1. Est Autobot Transformer? Oui, nous choisissons l'héritage.
  2. Le pistolet fait-il partie du Transformer? Oui, cela signifie la composition.

Pour un auto-test, essayez la combinaison inverse, vous obtenez des ordures. Cette feuille de triche aide dans la plupart des cas, mais il existe d'autres facteurs sur lesquels vous devez vous fier lors du choix entre la composition et l'héritage. De plus, ces méthodes peuvent être combinées pour résoudre différents types de problèmes.

L'héritage est statique


Une autre différence importante entre l'héritage et la composition est que l'héritage est de nature statique et établit des relations de classe uniquement au stade de l'interprétation / compilation. La composition, comme nous l'avons vu dans les exemples, vous permet de modifier la relation des entités à la volée à droite lors de l'exécution - parfois, cela est très important, vous devez donc vous en souvenir lors du choix des relations (à moins bien sûr que vous souhaitiez utiliser la méta-programmation ).

Héritage multiple


Nous avons examiné une situation où deux classes sont héritées d'un descendant commun. Mais dans certaines langues, vous pouvez faire le contraire - hériter d'une classe de deux parents ou plus, en combinant leurs propriétés et leur comportement. La possibilité d'hériter de plusieurs classes au lieu d'une est l'héritage multiple.



En général, dans les cercles Illuminati, il existe une opinion selon laquelle l'héritage multiple est un péché, il entraîne un problème en forme de diamant et une confusion avec les concepteurs. De plus, les tâches qui peuvent être résolues par héritage multiple peuvent être résolues par d'autres mécanismes, par exemple le mécanisme d'interface (dont nous parlerons également). Mais en toute équité, il convient de noter que l'héritage multiple est pratique à utiliser pour la mise en œuvre des impuretés .

Classes abstraites


En plus des classes ordinaires, des langues abstraites existent dans certaines langues. Ils diffèrent des classes ordinaires en ce que vous ne pouvez pas créer un objet d'une telle classe. Pourquoi avons-nous besoin d'une telle classe, demandera le lecteur? Il est nécessaire pour que les descendants puissent en être hérités - des classes ordinaires dont les objets peuvent déjà être créés.

La classe abstraite, avec les méthodes habituelles, contient des méthodes abstraites sans implémentation (avec une signature, mais sans code), que le programmeur qui prévoit de créer une classe descendante doit implémenter. Les classes abstraites ne sont pas requises, mais elles aident à établir un contrat qui nécessite l'implémentation d'un ensemble spécifique de méthodes afin de protéger un programmeur avec une mémoire insuffisante d'une erreur d'implémentation.

Polymorphisme


Le polymorphisme est une propriété système qui vous permet d'avoir plusieurs implémentations d'une même interface. Rien n'est clair. Passons aux transformateurs.

Supposons que nous ayons trois transformateurs: Optimus, Megatron et Oleg. Les transformateurs sont des combats, ils ont donc la méthode attack (). Le joueur, en appuyant sur le bouton «Fight» de son joystick, dit au jeu d'appeler la méthode attack () sur le transformateur pour lequel le joueur joue. Mais comme les transformateurs sont différents et que le jeu est intéressant, chacun d'eux attaquera d'une manière ou d'une autre. Disons qu'Optimus est un objet de la classe Autobot, et que les Autobots sont équipés de canons avec des ogives en plutonium (oui, les fans de transformateurs ne sont pas en colère). Megatron est un Decepticon et tire à partir d'un pistolet à plasma. Oleg est un bassiste, et il l'appelle des noms. Et à quoi ça sert?

L'avantage du polymorphisme dans cet exemple est que le code du jeu ne sait rien de l'implémentation de sa requête, qui devrait attaquer comment, sa tâche est simplement d'appeler la méthode attack (), dont la signature est la même pour toutes les classes de personnages. Cela vous permet d'ajouter de nouvelles classes de personnages ou de changer les méthodes existantes sans changer le code du jeu. C'est pratique.

Encapsulation


L'encapsulation est le contrôle de l'accès aux champs et aux méthodes d'un objet. Le contrôle d'accès implique non seulement possible / sans conséquence, mais également diverses validations, chargements, calculs et autres comportements dynamiques.

Dans de nombreuses langues, le chiffrement des données fait partie de l'encapsulation. Pour cela, il existe des modificateurs d'accès (nous décrivons ceux qui sont dans presque tous les langages OOP):

  • publi - n'importe qui peut accéder à l'attribut
  • private - seules les méthodes de cette classe peuvent accéder à l'attribut
  • protégé - le même que privé, seuls les héritiers de la classe ont accès, y compris

 class Transformer(){ public function constructor(){ } protected function setup(){ } private function dance(){ } } 

Comment choisir le modificateur d'accès? Dans le cas le plus simple comme celui-ci: si la méthode doit être accessible au code externe, sélectionnez public. Sinon, privé. S'il existe un héritage, alors protected peut être nécessaire si la méthode ne doit pas être appelée en externe, mais doit être appelée par des descendants.

Accesseurs (getters et setters)


Les getters et setters sont des méthodes dont la tâche est de contrôler l'accès aux champs. Le getter lit et renvoie la valeur du champ, et le setter, au contraire, prend la valeur comme argument et l'écrit dans le champ. Cela permet de fournir à ces méthodes des traitements supplémentaires. Par exemple, un setter, lors de l'écriture d'une valeur dans un champ d'objet, peut vérifier le type ou si la valeur est dans la plage de valeurs valides (validation). Dans le getter, vous pouvez ajouter une initialisation ou une mise en cache paresseuse si la valeur réelle se trouve réellement dans la base de données. Il y a beaucoup d'applications.

Certaines langues ont du sucre syntaxique qui permet à ces accesseurs d'être masqués en tant que propriétés, ce qui rend l'accès transparent au code externe, qui ne soupçonne pas qu'il ne fonctionne pas avec un champ, mais avec une méthode qui exécute une requête SQL ou la lecture d'un fichier sous le capot. C'est ainsi que l'abstraction et la transparence sont réalisées.

Interfaces


La tâche de l'interface est de réduire le niveau de dépendance des entités les unes par rapport aux autres, en ajoutant plus d'abstraction.

Toutes les langues n'ont pas ce mécanisme, mais dans les langues OOP avec typage statique sans elles, ce serait vraiment mauvais. Ci-dessus, nous avons considéré les classes abstraites, abordant le sujet des contrats obligeant à implémenter certaines méthodes abstraites. L'interface ressemble donc beaucoup à une classe abstraite, mais ce n'est pas une classe, mais juste un mannequin avec une énumération de méthodes abstraites (sans implémentation). En d'autres termes, l'interface est de nature déclarative, c'est-à-dire un contrat propre sans un peu de code.

En règle générale, les langages dotés d'interfaces n'ont pas d'héritage de classes multiples, mais il existe un héritage d'interfaces multiples. Cela permet à la classe de répertorier les interfaces qu'elle s'engage à implémenter.

Les classes avec interfaces sont constituées d'une relation plusieurs-à-plusieurs: une seule classe peut implémenter plusieurs interfaces, et chaque interface, à son tour, peut être implémentée par plusieurs classes.

L'interface a une utilisation bilatérale:

  1. D'un côté de l'interface se trouvent des classes implémentant cette interface.
  2. De l'autre côté, il y a les consommateurs qui utilisent cette interface comme description du type de données avec lesquels ils (les consommateurs) travaillent.

Par exemple, si un objet autre que le comportement de base peut être sérialisé, laissez-le implémenter l'interface Serializable. Et si l'objet peut être cloné, laissez-le implémenter une autre interface - «Cloné». Et si nous avons une sorte de module de transport qui transfère des objets sur le réseau, il acceptera tous les objets qui implémentent l'interface Serializable.

Imaginez que le châssis du transformateur soit équipé de trois emplacements: un emplacement pour les armes, un générateur d'énergie et une sorte de scanner. Ces emplacements ont certaines interfaces: seuls des équipements adaptés peuvent être installés dans chaque emplacement. Vous pouvez installer un lance-roquettes ou un canon laser dans la fente pour les armes, un réacteur nucléaire ou RTG (générateur thermoélectrique radio-isotope) dans la fente pour le générateur de puissance, et un radar ou lidar dans la fente pour le scanner. L'essentiel est que chaque emplacement possède une interface de connexion universelle, et des périphériques spécifiques doivent correspondre à cette interface. Par exemple, plusieurs types d'emplacements sont utilisés sur les cartes mères: un emplacement processeur vous permet de connecter différents processeurs adaptés à ce socket, et un emplacement SATA vous permet de connecter n'importe quel lecteur SSD ou HDD ou même un CD / DVD.

J'attire votre attention sur le fait que le système de fentes pour transformateurs qui en résulte est un exemple d'utilisation de la composition. Si l'équipement dans les emplacements sera remplaçable pendant la durée de vie du transformateur, c'est déjà une agrégation. Pour plus de clarté, nous appellerons les interfaces, comme c'est la coutume dans certaines langues, en ajoutant le «et» majuscule avant le nom: IWeapon, IEnergyGenerator, IScanner.

 //  : interface IWeapon{ function fire() {} //    .   } interface IEnergyGenerator{ //    ,     : function generate_energy() {} //  function load_fuel() {} //  } interface IScanner{ function scan() {} } // ,  : class RocketLauncher() : IWeapon { function fire(){ //    } } class LaserGun() : IWeapon { function fire(){ //    } } class NuclearReactor() : IEnergyGenerator { function generate_energy(){ //      } function load_fuel(){ //     } } class RITEG() : IEnergyGenerator { function generate_energy(){ //     } function load_fuel(){ //   - } } class Radar() : IScanner { function scan(){ //    } } class Lidar() : IScanner { function scan(){ //     } } //  - : class Transformer() { // , : IWeapon slot_weapon //      . IEnergyGenerator slot_energy_generator //     , IScanner slot_scanner //     /*         ,      ,   : */ function install_weapon(IWeapon weapon){ this.slot_weapon = weapon } function install_energy_generator(IEnergyGenerator energy_generator){ this.slot_energy_generator = energy_generator } function install_scanner(IScanner scanner){ this.slot_scanner = scanner } } //   class TransformerFactory(){ function build_some_transformer() { transformer = new Transformer() laser_gun = new LaserGun() nuclear_reactor = new NuclearReactor() radar = new Radar() transformer.install_weapon(laser_gun) transformer.install_energy_generator(nuclear_reactor) transformer.install_scanner(radar) return transformer } } //  transformer_factory = new TransformerFactory() oleg = transformer_factory.build_some_transformer() 


Animation n ° 4

Malheureusement, l'usine ne rentre pas dans l'image, mais elle est toujours facultative, le transformateur peut également être assemblé dans la cour.

La couche d'abstraction indiquée dans l'image sous forme d'interfaces entre la couche d'implémentation et la couche consommateur permet d'abstraire l'une de l'autre. Vous pouvez l'observer en regardant chaque couche séparément: dans la couche d'implémentation (à gauche) il n'y a pas un mot sur la classe Transformer, et dans la couche consommateur (à droite) il n'y a pas un mot sur les implémentations spécifiques (il n'y a pas de mots Radar, RocketLauncher, NuclearReactor, etc. . d.)

Dans ce code, nous pouvons créer de nouveaux composants pour les transformateurs sans affecter les dessins des transformateurs eux-mêmes. En même temps et vice versa, nous pouvons créer de nouveaux transformateurs en combinant des composants existants, ou ajouter de nouveaux composants sans modifier ceux qui existent.

Dactylographie de canard


Le phénomène que nous observons dans l'architecture résultante est appelé typage du canard : si quelque chose craque comme un canard, nage comme un canard et ressemble à un canard, alors il s'agit très probablement d'un canard .

En traduisant cela dans le langage des transformateurs, cela ressemblera à ceci: si quelque chose tire comme un canon et se recharge comme un canon, il s'agit très probablement d'un canon. Si l'appareil génère de l'énergie, il s'agit très probablement d'un générateur électrique.

Contrairement à la typification hiérarchique de l'héritage, avec le typage de canard, le transformateur ne se soucie pas de la classe que le pistolet lui a été donnée, ni du fait qu'il s'agisse d'un pistolet. L'essentiel est que cette chose puisse tirer! Ce n'est pas une vertu de la frappe de canard, mais plutôt un compromis. Il peut y avoir une situation inverse, comme dans cette image ci-dessous:



FAI

(Principe de ségrégation des interfaces / Quatrième principe SOLID) encourage à ne pas créer d'interfaces audacieuses et universelles. Au lieu de cela, les interfaces doivent être divisées en interfaces plus petites et spécialisées, cela aidera à les combiner de manière plus flexible dans l'implémentation de classes, sans forcer à implémenter des méthodes inutiles.

Abstraction


En POO, tout tourne autour de l'abstraction. Il y a des fanatiques qui prétendent que l'abstraction devrait faire partie de la trinité OOP (encapsulation, polymorphisme, héritage). Et mon inspecteur des tests de libération conditionnelle a dit le contraire: l'abstraction est inhérente à toute programmation, et pas seulement à la POO, elle devrait donc être distincte. D'un autre côté, on peut en dire autant du reste des principes, mais vous n'effacerez pas les mots d'une chanson. D'une manière ou d'une autre, l'abstraction est nécessaire, et particulièrement en POO.

Niveau d'abstraction


Ici, on ne peut manquer de citer une blague bien connue:
- tout problème architectural peut être résolu en ajoutant une couche d'abstraction supplémentaire, à l'exception du problème d'un grand nombre d'abstractions.

Dans notre exemple avec les interfaces, nous avons implémenté une couche d'abstraction entre les transformateurs et les composants, rendant l'architecture plus flexible. Mais à quel prix? Nous avons dû compliquer l'architecture. Mon psychothérapeute a dit que la capacité d'équilibrer entre la simplicité de l'architecture et la flexibilité de l'application est un art. En choisissant un terrain d'entente, il faut se fier non seulement à sa propre expérience et intuition, mais aussi au contexte du projet en cours. Étant donné que la personne n'a pas encore appris à voir l'avenir, il est nécessaire d'estimer analytiquement quel niveau d'abstraction et avec quel degré de probabilité peut être utile dans ce projet, combien de temps sera nécessaire pour développer une architecture flexible et si le temps passé sera rentable à l'avenir.

Une sélection incorrecte du niveau d'abstraction entraîne l'un des deux problèmes suivants:

  1. , , , ( )
  2. , , , , . ( )



Il est également important de comprendre que le niveau d'abstraction n'est pas déterminé pour l'ensemble du projet dans son ensemble, mais séparément pour différents composants. Dans certains endroits, le système d'abstraction peut ne pas être suffisant, mais quelque part au contraire - buste. Cependant, le mauvais choix du niveau d'abstraction peut être corrigé par une refactorisation opportune. Le mot-clé arrive à point nommé . Le refactoring retardé est problématique lorsque de nombreux mécanismes sont déjà implémentés à ce niveau d'abstraction. Effectuer un rituel de refactorisation dans les systèmes en cours d'exécution peut entraîner une douleur aiguë dans les endroits difficiles à atteindre d'un programmeur. Il s'agit de savoir comment changer les fondations d'une maison - il est moins cher de construire une maison à côté de zéro.

Regardons la définition du niveau d'abstraction à partir des options possibles sur l'exemple d'un jeu hypothétique "transformers-online". Dans ce cas, les niveaux d'abstraction agiront comme des couches, chaque couche suivante à l'étude se placera au-dessus de la précédente, en prenant une partie de la fonctionnelle d'elle en elle-même.

Première couche. Le jeu a une classe de transformateur, toutes les propriétés et le comportement y sont décrits. Il s'agit d'un niveau d'abstraction entièrement en bois, adapté au jeu occasionnel, ce qui n'implique aucune flexibilité particulière.

Le deuxième niveau.Le jeu a un transformateur de base avec des capacités de base et des classes de transformateurs avec leur propre spécialisation (comme un éclaireur, un avion d'attaque, un support), qui est décrit par des méthodes supplémentaires. Ainsi, le joueur a la possibilité de choisir et le développement de nouvelles classes est simplifié pour les développeurs.

Troisième niveau. En plus de la classification des transformateurs, l'agrégation est introduite à l'aide d'un système de fentes et de composants (comme dans notre exemple avec des réacteurs, des canons et des radars). Maintenant, une partie du comportement sera déterminée par le personnel que le joueur a installé dans son transformateur. Cela donne au joueur encore plus de possibilités de personnaliser la mécanique du jeu du personnage et donne aux développeurs la possibilité d'ajouter ces mêmes modules d'extension, ce qui simplifie à son tour le travail des concepteurs de jeux pour publier de nouveaux contenus.

Quatrième niveau. Vous pouvez également inclure votre propre agrégation dans les composants, ce qui vous permet de sélectionner les matériaux et les pièces à partir desquels ces composants sont assemblés. Cette approche donnera au joueur la possibilité non seulement de garnir les transformateurs avec les composants nécessaires, mais aussi de produire indépendamment ces composants à partir de diverses pièces. Franchement, je n'ai jamais rencontré un tel niveau d'abstraction dans les jeux, et non sans raison! Après tout, cela s'accompagne d'une complication importante de l'architecture, et l'ajustement de l'équilibre dans de tels jeux se transforme en enfer. Mais je n'exclus pas que de tels jeux existent.



Comme vous pouvez le voir, chaque couche décrite, en principe, a droit à la vie. Tout dépend du type de flexibilité que nous voulons apporter au projet. Si les termes de référence ne disent rien à ce sujet, ou si l'auteur du projet lui-même ne sait pas ce que l'entreprise peut exiger, vous pouvez regarder des projets similaires dans ce domaine et vous concentrer sur eux.

Modèles de conception




Des décennies de développement ont conduit à la formation d'une liste des solutions architecturales les plus couramment utilisées, qui au fil du temps ont été classées par la communauté, et sont appelées modèles de conception . C'est pourquoi lorsque j'ai lu pour la première fois sur les modèles, j'ai été surpris de constater qu'il s'avère que j'en utilise déjà beaucoup dans la pratique, je ne savais tout simplement pas que ces solutions avaient un nom.

Les modèles de conception, comme l'abstraction, sont caractéristiques non seulement du développement de la POO, mais aussi d'autres paradigmes. En général, le sujet des modèles dépasse le cadre de cet article, mais je voudrais avertir un jeune développeur qui a seulement l'intention de se familiariser avec les modèles. Ceci est un piège! Je vais maintenant expliquer pourquoi.

Le but des modèles est d'aider à résoudre les problèmes architecturaux qui sont déjà découverts ou très susceptibles d'être découverts pendant le développement du projet. Ainsi, après avoir lu sur les modèles, un débutant peut être irrésistiblement tenté d'utiliser des modèles non pas pour résoudre des problèmes, mais pour les générer. Et puisque le développeur est débridé dans ses désirs, il ne peut pas commencer à résoudre le problème à l'aide de modèles, mais peut ajuster toutes les tâches aux solutions à l'aide de modèles.

Une autre valeur des modèles est la formalisation de la terminologie. Il est beaucoup plus facile pour un collègue de dire qu’une «chaîne de devoirs» est utilisée à cet endroit que de dessiner le comportement et les relations des objets sur une feuille de papier pendant une demi-heure.

Conclusion


Dans les conditions modernes, la présence de la classe de mots dans votre code ne fait pas de vous un programmeur OOP. Car si vous n'utilisez pas les mécanismes décrits dans l'article (polymorphisme, composition, héritage, etc.), et que vous n'utilisez à la place des classes que pour regrouper des fonctions et des données, ce n'est pas de la POO. Le même problème peut être résolu par certains espaces de noms et structures de données. Ne confondez pas, sinon vous aurez honte lors de l'entretien.

Je veux terminer ma chanson avec des mots importants. Les mécanismes, principes et schémas décrits, ainsi que la POO dans son ensemble, ne devraient pas être appliqués là où ils sont inutiles ou pourraient être nocifs. Cela conduit à des articles avec des titres étranges comme «L'hérédité est la cause du vieillissement prématuré» ou «Singleton peut conduire au cancer».

Je suis sérieux.Si nous considérons le cas du singleton, son utilisation généralisée à l'insu du cas a causé de graves problèmes architecturaux dans de nombreux projets. Et les amateurs de martelage des ongles avec un microscope l'ont gentiment appelé un contre-modèle. Soyez prudent.

Malheureusement, dans la conception, il n'y a pas de recettes sans ambiguïté pour toutes les occasions, où ce qui est approprié et où est inapproprié. Cela s'intégrera progressivement dans votre tête avec l'expérience.

Source: https://habr.com/ru/post/fr463125/


All Articles