Architektur für Anfänger oder warum Sie keine Flagge in den Schwertkämpfer einfügen müssen



Anmerkung:

  1. Ein Beispiel für die Implementierung neuer Funktionen in der Klasse durch Hinzufügen eines "Flags".
  2. Auswirkungen.
  3. Alternativer Ansatz und Vergleich der Ergebnisse.
  4. Wie vermeide ich die Situation: „Architektonischer Overkill“?
  5. Der Moment, in dem die Zeit kommt, alles zu ändern.

Bevor Sie beginnen, ein paar Anmerkungen:

  • Dies ist eine Geschichte über Softwarearchitektur - in dem Sinne, wie Onkel Bob sie verwendet. Ja, dieses.
  • Alle Zeichen, ihre Namen und ihr Code im Artikel sind fiktiv, alle Übereinstimmungen mit der Realität sind zufällig.


Angenommen, ich bin ein gewöhnlicher Programmierer-Programmierer in einem Projekt. Das Projekt ist ein Spiel, in dem ein einzelner Held (auch bekannt als Held) entlang einer perfekt horizontalen Linie von links nach rechts verläuft. Monster stören diese wunderbare Reise. Auf Befehl des Benutzers schneidet der Held sie mit einem Schwert in den Kohl und bläst nicht in den Schnurrbart. Das Projekt hat bereits 100.000 Zeilen und "Sie benötigen mehr Zeilen mit Funktionen!" Schauen wir uns unseren Helden an:

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

Angenommen, mein persönlicher Teamleiter Vasily kann auch in den Urlaub fahren. Plötzlich. Wer hätte das gedacht? Weiter entwickelt sich die Situation auf normale Weise: Eine Eule fliegt ein und fordert erst gestern dringend auf, vor Spielbeginn eine Wahl treffen zu können: mit einem Schläger oder einem Schwert für den Helden zu spielen.

Es muss sehr schnell sein. Und ja, natürlich war er selbst mehr als Jahre lang Programmierer, als ich laufen kann, also weiß er, dass die Aufgabe fünf Minuten dauert. Aber sei es, schätze für 2 Stunden. Sie müssen nur ein Kontrollkästchen und if-chik hinzufügen.

Hinweis: if-chik im Wert @! ^% $ # @ ^% & @! 11 $ ist # $% @ ^% &! @ !!! in &! ^% # $ ^%! 1 if-chic!

Meine Gedanken: „Ha! Zwei Stunden! Erledigt! ":

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:  //     
        }
    }
    //  
}

Wenn Sie in meiner Entscheidung herausgefunden haben, dann leider: Ich habe zwei Neuigkeiten für Sie:

  1. Als ob gut: Wir liefern beide. Wir liefern - vom Wort liefern Wert oder vom Wort lustiger (durch Tränen) Code.
  2. Und der schlechte: ohne Vasily das Kapets-Projekt.

Also was ist passiert? Es scheint bisher nichts zu sein. Aber mal sehen, was als nächstes passiert (während wir alles tun, um Vasily im Urlaub zu halten). Und dann wird es Folgendes geben: Die QS-Abteilung wird auf das Gewicht unseres Helden achten. Und nein, das liegt nicht daran, dass der Held eine Diät machen muss, sondern hier ist der Grund:

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

Ich habe vergessen, die Gewichtsberechnung zu korrigieren. Na denkst du, Fehler, mit wem passiert das nicht ?! Slap-Bloop, Fick-Tibidoch, fertig:

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

Aber schnell behoben! Der Held springt jetzt unter Berücksichtigung des Gewichts, aber. Ergebnisorientierte Arbeit! Dies ist nicht für Sie, um Navels, architektonische Astronauten zu betrachten.

Nun, wirklich, dann hat er ein bisschen mehr korrigiert. In der Zauberliste musste ich Folgendes tun:

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
    }
}

Und dann kam Petja und brach alles. Nun, die Wahrheit ist, wir haben so einen Juni im Projekt. Unaufmerksam.

Er musste nur das Konzept der „Waffenstufe“ zu den Formeln hinzufügen, um die Stärke des Schlags und das Gewicht des Helden zu berechnen. Und Petja hat einen von vier Fällen verpasst. Aber nichts! Ich habe alles korrigiert:

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] {
    //     , ! 
    // ,     .    ,    ,   .   !
}

Unnötig zu sagen, wann (plötzlich!) Es notwendig war, eine Bogen- / Aktualisierungsformel hinzuzufügen, gab es wieder vergessene Fälle, Fehler und das war alles.

Was schief gelaufen ist? Wo habe ich mich geirrt und was (außer den Freunden) hat Wassili gesagt, als er aus dem Urlaub zurückkam?

Hier kann man die Geschichte nicht weiter lesen, sondern zum Beispiel an das Ewige denken, an die Architektur.

Und mit denen, die sich noch zum Lesen entschlossen haben, fahren wir fort.

Schauen wir uns also die Klassiker an:
Vorzeitige Optimierung ist die Wurzel allen Übels!
Und ... äh ... das ist es nicht. Hier ist die Sache:

In OOP-Sprachen (wie zum Beispiel Swift) gibt es drei Hauptmöglichkeiten, um die Fähigkeiten einer Klasse zu erweitern:

  1. Der erste Weg ist "naiv". Wir haben ihn gerade gesehen. Kontrollkästchen hinzufügen. Verantwortung hinzufügen. Schwellungsklasse.
  2. Der zweite Weg ist die Vererbung. Jeder kennt einen leistungsstarken Mechanismus zur Wiederverwendung von Code. Man könnte zum Beispiel:
    • Erbe den neuen Helden mit dem Bogen und den Helden von Dubina vom Helden (der ein Schwert hält, aber dies spiegelt sich jetzt nicht im Namen der Heldenklasse wider). Und dann überschreiben Sie in den Erben die geänderten Methoden. Dieser Weg ist sehr schlecht (vertrau mir einfach).
    • Machen Sie die Basisklasse (oder das Protokoll) zu einem Helden und entfernen Sie alle Merkmale, die mit einem bestimmten Waffentyp verbunden sind, als Erben
      : Held mit
      Schwert: Held, Held mit Bogen: Held, Held mit
      Dubina: Held.
      Das ist besser, aber die Namen dieser Klassen selbst sehen uns irgendwie unzufrieden, heftig und gleichzeitig traurig und ratlos an. Wenn sie jemanden so nicht ansehen, werde ich versuchen, einen weiteren Artikel zu schreiben, in dem sie zusätzlich zu dem langweiligen männlichen Helden ...
  3. Der dritte Weg ist die Trennung der Verantwortung durch Abhängigkeitsinjektion. Es kann sich um eine Abhängigkeit handeln, die durch ein Protokoll geschlossen wird, oder um eine Schließung (als ob sie durch eine Signatur geschlossen wäre). Die Hauptsache ist, dass die Umsetzung neuer Verantwortlichkeiten die Hauptklasse verlassen sollte.

Wie kann das in unserem Fall aussehen? Zum Beispiel so (Entscheidung von 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
    }
}

Was musst du so sein? Ninjutsu - Protokoll:

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

Beispiel für die Protokollimplementierung:

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

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

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

Ebenso sind Schwertklassen geschrieben für: Club, Bow, Pike usw. "Es ist leicht zu erkennen" (c), dass in der neuen Architektur der gesamte Code, der für jeden bestimmten Waffentyp gilt, in der entsprechenden Klasse zusammengefasst ist und nicht gemäß dem Helden zusammen mit anderen Waffentypen verbreitet wird. Dies erleichtert das Lesen und Verstehen des Helden und einer bestimmten Waffe. Aufgrund der Anforderungen des auferlegten Protokolls ist es außerdem viel einfacher, alle Methoden zu verfolgen, die beim Hinzufügen eines neuen Waffentyps oder beim Hinzufügen einer neuen Funktion zu einer Waffe implementiert werden müssen (z. B. kann eine Waffe eine Preisberechnungsmethode haben).

Hier können Sie sehen, dass die Abhängigkeitsinjektion die Erstellung von Hero-Klassenobjekten erschwert hat. Was früher einfach gemacht wurde:

let lastHero = Hero()

Jetzt hat es sich in eine Reihe von Anweisungen verwandelt, die überall dort dupliziert werden, wo es notwendig ist, einen Helden zu erstellen. Vasily kümmerte sich jedoch darum:

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

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

Es ist klar , dass Vasily , um zu schwitzen hatte den Haufen zu zerstreuen , das wurde aufgetürmt von Petya entworfen (und mir).

Wenn man sich die letzte Lösung ansieht, kann natürlich der folgende Gedanke auftauchen:
Ok, es stellte sich heraus, Normen. Es ist bequem zu lesen und zu erweitern, aber sind all diese Fabriken / Protokolle / Abhängigkeiten eine Menge Overhead? Ein Code, der keine Funktionen enthält, sondern nur zum Organisieren des Codes vorhanden ist. "Code zum Organisieren des Codes", mgm. Ist es wirklich notwendig, diesen Garten immer und überall einzuzäunen?
Eine ehrliche Antwort auf die erste Frage wäre:
Ja, dies ist ein Overhead für Funktionen, die Unternehmen so sehr lieben.
Und zur Frage „wann?“ Antwortbereich:

Die Philosophie des Schwertkämpfers oder "Wann mussten Sie regieren?"


Am Anfang war ein Schwertkämpfer. Im Sinne des alten Codes war dies ganz normal. Solange es einen Helden und eine Waffe gab, musste man nicht zwischen ihnen unterscheiden - trotzdem gab es nichts anderes für den Helden. Und der monolithische Code bestätigte diese Tatsache mit seinem Text.

Schwertkämpfer - es klingt nicht einmal so schlecht.

Und wozu führten die ersten „naiven“ Änderungen? Was führte die Hinzufügung des if-chik dazu?

Das Hinzufügen eines If-Chic führte zu ... einer Mutante! Mutante, d.h. veränderliches Objekt, das zwischen einem „Menschen“ und einem „menschlichen Dubin“ mutieren kann. Wenn Sie gleichzeitig einen kleinen Fehler bei der Implementierung der Mutante machen, entsteht der Zustand von „Human-Medullin“. Nicht so! Es besteht überhaupt keine Notwendigkeit für "menschliches Dubin".

Es ist notwendig:

  1. Mann + Schwertabhängigkeit (Notwendigkeit eines Schwertes);
  2. Mann + Sucht nach einem Verein (Notwendigkeit eines Vereins).

Nicht jede Sucht ist böse! Diese Alkoholsucht ist böse und das Protokoll ist gut. Ja, sogar die Abhängigkeit vom Objekt ist besser als „menschlicher Dubin“!

Wann ist die Mutation aufgetreten? Die Umwandlung in eine Mutante erfolgte zum Zeitpunkt des Hinzufügens der Flagge: Der monolithische Code blieb monolithisch, aber als die Flagge geändert (mutiert) wurde, begann sich das Verhalten desselben Objekts signifikant zu ändern.

Vasily würde zwei Stadien der Mutation herausgreifen:

  1. Hinzufügen eines Flags und des allerersten "if" (oder "switch" oder eines anderen Verzweigungsmechanismus) zum Flag. Die Situation ist bedrohlich, aber erträglich: Der Held wird mit radioaktivem Abfall übergossen, aber er überwindet ihn.
  2. «if» , . . — . , - - .

In der betrachteten Situation ist es daher sinnvoll, die Architektur zu erstellen, um das Auftreten technischer Schulden zu verhindern, bevor die erste Stufe im schlimmsten Fall die zweite durchlaufen wird.

Was genau hat Vasily getan, um eine Mutante zu behandeln?

Aus technischer Sicht - verwendetes Injection Dependency Closed Protocol.

Aus philosophisch geteilter Verantwortung.

Alle Merkmale der Arbeit der Klasse, die untereinander ausgetauscht werden können (was bedeutet, dass sie alternativ zueinander sind), wurden abhängig vom Helden entfernt. Eine neue, alternative Funktionalität - die Umsetzung der Arbeit des Schwertes, die Umsetzung der Arbeit des Clubs - unterschied sich nach ihrem Erscheinen voneinander und unterscheidet sich von den anderen nach wie vorKein alternativer Heldencode. So erschien bereits im "naiven" Code etwas Neues , das sich in seinem alternativen Verhalten von dem unbestrittenen Teil des Helden unterschied. So erschien im „naiven“ Code eine implizite Beschreibung neuer Geschäftseinheiten : ein Schwert und ein Knüppel, die nach Angaben des Helden verteilt waren. Um die Arbeit mit neuen Geschäftsentitäten zu vereinfachen , wurde es erforderlich, sie als separate Codeentitäten mit eigenen Namen zu unterscheiden . Es gab also eine Aufteilung der Verantwortung.

PS TL; DR;

  1. Sehen Sie die Flagge?
  2. Sei ein Mann, verdammt! Lösche es!
  3. Suchtinjektion
  4. ...
  5. Gewinn! 11

All Articles