Architecture pour débutants ou pourquoi vous n'avez pas besoin d'insérer un drapeau dans l'épée



Annotation:

  1. Un exemple de l'implémentation de nouvelles fonctionnalités dans la classe en ajoutant un "flag".
  2. Effets.
  3. Approche alternative et comparaison des résultats.
  4. Comment éviter la situation: "Overkill architectural"?
  5. Le moment où vient le temps de tout changer.

Avant de commencer, quelques notes:

  • Il s'agit d'une histoire sur l'architecture logicielle - au sens où l'oncle Bob l'utilise. Oui, celui-là.
  • Tous les personnages, leurs noms et code dans l'article sont fictifs, toutes les coïncidences avec la réalité sont aléatoires.


Supposons que je sois un programmeur-programmeur ordinaire sur un projet. Le projet est un jeu dans lequel un seul héros (alias Hero) suit une ligne parfaitement horizontale de gauche à droite. Les monstres interfèrent avec ce merveilleux voyage. À la demande de l'utilisateur, le héros les coupe avec une épée dans le chou et ne souffle pas dans la moustache. Le projet compte déjà 100 000 lignes et «vous avez besoin de plus de lignes de fonctionnalités!» Regardons notre héros:

class Hero {
    func strike() {
        //   1
    }
    //   
}

Supposons également que mon chef d'équipe personnel, Vasily, puisse également partir en vacances. Tout à coup. Qui aurait pensé? De plus, la situation évolue en standard: une Chouette s'envole et a urgemment besoin, hier même, de pouvoir choisir avant le début du jeu: jouer pour le Héros avec un club ou avec une épée.

Ça doit être très rapide. Et oui, bien sûr, lui-même a été programmeur pendant plus de ans que je ne peux marcher, il sait donc que la tâche est de cinq minutes. Mais tant pis, estimez pour 2 heures. Vous avez juste besoin d'ajouter une case à cocher et if-chik.

Remarque: if-chik dans la valeur @! ^% $ # @ ^% & @! 11 $ its # $% @ ^% &! @ !!! dans &! ^% # $ ^%! 1 if-chic!

Mes pensées: «Ha! Deux heures! Terminé! ":

class Hero {
    enum WeaponTypes {
        case sword:
        case club:
    }
	
    var weaponType: WeaponTypes?

    func strike() {
        guard let weaponType = weaponType else {
            assertionFailure()
            return	
        }
        //  : switch  Swift  if- -    !
        switch (weaponType) {
            case .sword: //     
            case .club:  //     
        }
    }
    //  
}

Si vous avez découvert ma décision, hélas: j'ai deux nouvelles pour vous:

  1. Comme si c'était bon: nous livrons tous les deux. Nous livrons - du mot livrer la valeur ou du mot code drôle (à travers les larmes).
  2. Et le mauvais: sans Vasily, le projet des kapets.

Alors, qu'est-ce-qu'il s'est passé? Il semblerait pour l'instant rien. Mais voyons ce qui se passe ensuite (pendant que nous faisons tout notre possible pour garder Vasily en vacances). Et puis il y aura ceci: le département QA fera attention au poids de notre héros. Et non, ce n'est pas parce que le héros doit suivre un régime, mais voici pourquoi:

var weight: Stone {
    return meatbagWeight + pantsWeight + helmetWeight + swordWeight
}

J'ai oublié de fixer le calcul du poids. Eh bien, vous pensez, erreur, avec qui ne se produit pas?! Slap-bloop, fuck-tibidoch, prêt:

var weight: Stone {
    //   guard let weaponType
    let weightWithoutWeapon = meatbagWeight + pantsWeight + helmetWeight
    switch (weaponType) {
        case .sword: return weightWithoutWeapon + swordWeight
        case .club:  return weightWithoutWeapon + clubWeight
    }
}

Mais rapidement corrigé! Le héros saute maintenant, en tenant compte du poids, mais. Un travail axé sur les résultats! Ce n'est pas à vous de regarder les nombrils, les astronautes architecturaux.

Eh bien, vraiment, alors il a corrigé un peu plus. Dans la liste des sorts, je devais faire ceci:

var spells: [Spells] {
    //   guard let weaponType 
    // ,   let spellsWithoutWeapon: [Spells]
    switch (weaponType) {
        case .sword:
            //  let swordSpells: [Spells]
            return spellsWithoutWeapon + swordSpells
	case .club:
            //  let clubSpells: [Spells]
            return spellsWithoutWeapon + clubSpells
    }
}

Et puis Petya est venu et a tout cassé. Eh bien, la vérité est que nous avons un tel juin sur le projet. Inattentif.

Il avait juste besoin d'ajouter le concept de "niveau d'arme" aux formules pour calculer la force du coup et le poids du héros. Et Petya a raté l'un des quatre cas. Mais rien! J'ai tout corrigé:

func strike() {
    //guard let weaponType
    switch (weaponType) {
        case .sword: //        weaponGrade
        case .club:  //        weaponGrade
    }
}

var weight: Stone {
    // guard let weaponType
    let weightWithoutWeapon = meatbagWeight + pantsWeight + helmetWeight
    switch (weaponType) {
        case .sword: return weightWithoutWeapon + swordWeight / grade
	case .club:  return weightWithoutWeapon + pow(clubWeight, 1 / grade)
    }
}

var spells: [Spells] {
    //     , ! 
    // ,     .    ,    ,   .   !
}

Inutile de dire que quand (du coup!) Il fallait ajouter un arc / mettre à jour les formules, là encore il y avait des cas oubliés, des bugs et c'était tout.

Qu'est ce qui ne s'est pas bien passé? Où avais-je tort et qu'est-ce que (sauf pour les camarades) Vasily a dit à son retour de vacances?

Ici, vous ne pouvez pas lire l'histoire plus loin, mais, par exemple, pensez à l'éternel, à l'architecture.

Et avec ceux qui ont encore décidé de lire, nous continuons.

Alors, regardons les classiques:
L'optimisation prématurée est la racine de tout Mal!
Et ... euh ... ce n'est pas ça. Voici la chose:

dans les langages OOP (comme Swift, par exemple), il existe trois façons principales d'étendre les capacités d'une classe:

  1. La première façon est «naïve». Nous venons de le voir. Ajout d'une case à cocher. Ajout de responsabilité. Cours de gonflement.
  2. La deuxième voie est l'héritage. Tout le monde connaît un mécanisme puissant pour réutiliser le code. On pourrait par exemple:
    • Héritez le nouveau héros avec l'arc et le héros de Dubina du héros (qui tient une épée, mais cela ne se reflète pas dans le nom de la classe de héros maintenant). Et puis dans les héritiers, remplacez les méthodes modifiées. Ce chemin est très mauvais (croyez-moi).
    • Faites de la classe de base (ou protocole) un héros et supprimez toutes les fonctionnalités associées à un type d'arme spécifique en tant qu'héritiers
      : héros avec une
      épée: héros, héros avec un arc: héros, héros avec
      Dubina: héros.
      C'est mieux, mais les noms de ces classes nous regardent en quelque sorte mécontents, farouchement et à la fois tristes et perplexes. S'ils ne regardent pas quelqu'un comme ça, alors j'essaierai d'écrire un autre article, où en plus du héros masculin ennuyeux, ils seront ...
  3. La troisième voie est la séparation des responsabilités par l'injection de dépendance. Il peut s'agir d'une dépendance fermée par un protocole ou d'une fermeture (comme si elle était fermée par une signature), peu importe. L'essentiel est que la mise en œuvre de nouvelles responsabilités quitte la classe principale.

À quoi cela peut-il ressembler dans notre cas? Par exemple, comme ceci (décision de Vasily):

class Hero {
    let weapon: Weapon //   , ..    

    init (_ weapon: Weapon) { //     
        self.weapon = weapon
    }
	
    func strike() {
        weapon.strike()
    }

    var weight: Stone {
        return meatbagWeight + pantsWeight + helmetWeight + weapon.weight
    }

    var spells: [Spells] {
        //   
        return spellsWithoutWeapon + weapon.spells
    }
}

De quoi avez-vous besoin pour être ainsi? Ninjutsu - protocole:

protocol Weapon {
    func strike()
    var weight: Stone {get}
    var spells: [Spells] {get}
}

Exemple de mise en œuvre du protocole:

class Sword: Weapon {
    func strike() {
        // ,     Hero  switch   .sword
    }

    var weight: Stone {
        // ,     Hero  switch   .sword
    }

    var spells: [Spells] {
        // ,     Hero  switch   .sword
    }
}

De même, les classes Sword sont écrites pour: Club, Bow, Pike, etc. «Il est facile de voir» (c) que dans la nouvelle architecture tout le code qui se rapporte à chaque type particulier d'arme est groupé dans la classe correspondante, et n'est pas réparti selon le Héros avec d'autres types d'armes. Cela facilite la lecture et la compréhension du héros et de toute arme particulière. De plus, en raison des exigences du protocole imposé, il est beaucoup plus facile de suivre toutes les méthodes qui doivent être mises en œuvre lors de l'ajout d'un nouveau type d'arme ou lors de l'ajout d'une nouvelle fonctionnalité à une arme (par exemple, une arme peut avoir une méthode de calcul de prix).

Ici, vous pouvez voir que l'injection de dépendances a compliqué la création d'objets de classe Hero. Ce qui était auparavant fait simplement:

let lastHero = Hero()

Maintenant, il s'est transformé en un ensemble d'instructions qui seront dupliquées partout où il est nécessaire de créer un héros. Cependant, Vasily s'est occupé de cela:

class HeroFactory {
    static func makeSwordsman() -> Hero { // ,  static -  
        let weapon = Sword(/*  */)
        return Hero(weapon)
    }

    static func makeClubman() -> Hero {
        let weapon = Club(/*  */)
        return Hero(weapon)
    }
}

Il est clair que Vasily a dû transpirer pour disperser le tas qui a été empilé conçu par Petya (et moi).

Bien sûr, en regardant la dernière solution, la pensée suivante peut se poser:
Ok, ça s'est avéré, des normes. Il est pratique de lire et d'étendre, mais toutes ces usines / protocoles / dépendances sont-ils un tas de frais généraux? Un code qui ne donne rien en termes de fonctionnalités, mais n'existe que pour organiser le code. "Code pour organiser le code", mgm. Faut-il vraiment clôturer ce jardin toujours et partout?
Une réponse honnête à la première question serait:
Oui, c'est une surcharge pour les fonctionnalités que les entreprises aiment tant.
Et à la question "quand?" section réponses:

La philosophie de l'épéiste ou «quand avez-vous dû régner?»


Au début, c'était un épéiste. Dans le système de sens de l'ancien code, c'était tout à fait normal. Tant qu'il y avait un héros et une arme, il n'était pas nécessaire de les distinguer - tout de même, il n'y avait rien d'autre pour le héros. Et le code monolithique a confirmé ce fait avec son texte.

Swordman - ça ne sonne même pas si mal.

Et à quoi ont conduit les premières modifications «naïves»? À quoi l'ajout de l'if-chik?

L'ajout d'un if-chic a conduit à ... un mutant! Mutant, c'est-à-dire objet mutable qui peut muter entre un «humain» et un «Dubin humain». Dans le même temps, si vous faites une petite erreur dans la mise en œuvre du mutant, alors l'état de «médulline humaine» apparaît. Ne fais pas ça! Il n'y a aucun besoin de «Dubin humain».

Il est nécessaire:

  1. Homme + addiction à l'épée (besoin d'une épée);
  2. Homme + addiction à un club (besoin d'un club).

Toutes les dépendances ne sont pas mauvaises! Cette dépendance à l'alcool est mauvaise et le protocole est bon. Oui, même la dépendance à l'objet est meilleure que «Dubin humain»!

Quand la mutation s'est-elle produite? La transformation en mutant s'est produite au moment où le drapeau a été ajouté: le code monolithique est resté monolithique, mais lorsque le drapeau a été changé (muté), le comportement du même objet a commencé à changer de manière significative.

Vasily distinguerait deux étapes de mutation:

  1. Ajout d'un drapeau et du tout premier «si» (ou «commutateur» ou autre mécanisme de branchement) au drapeau. La situation est menaçante, mais supportable: le héros est déversé de déchets radioactifs, mais il le surmonte.
  2. «if» , . . — . , - - .

Ainsi, dans la situation considérée, afin d'éviter l'apparition de dettes techniques, il est logique de construire l'architecture avant de passer par la première étape, dans le pire des cas, par la seconde.

Qu'a fait Vasily exactement pour traiter un mutant?

D'un point de vue technique - protocole fermé de dépendance d'injection utilisé.

De la responsabilité philosophique - partagée.

Toutes les caractéristiques du travail de la classe, qui peuvent être échangées entre elles (ce qui signifie qu'elles sont alternatives les unes aux autres), ont été supprimées du héros en fonction. La nouvelle fonctionnalité alternative - la mise en œuvre du travail de l'épée, la mise en œuvre du travail du club - dès son apparition, est devenue différente les unes des autres et différente des autres comme auparavantpas de code alternatif de héros. Donc déjà dans le code "naïf" quelque chose de nouveauest apparu qui était différent dans son comportement alternatif de la partie incontestée du Héros. Ainsi, dans le code «naïf», une description implicite de nouvelles entités commerciales : une épée et un club, répartis selon le Héros, a surgi. Afin de faciliter le fonctionnement avec de nouvelles entités commerciales , il est devenu nécessaire de les distinguer en tant qu'entités de code distinctesavec leurs propres noms . Il y avait donc une division des responsabilités.

PS TL; DR;

  1. Tu vois le drapeau?
  2. Soyez un homme, bon sang! Efface le!
  3. Injection de toxicomanie
  4. ...
  5. Bénéfice! 11

All Articles