Architecture for beginners or why you do not need to insert a flag in the sword man



Annotation:

  1. An example of the implementation of new functionality in the class by adding a "flag".
  2. Effects.
  3. Alternative approach and comparison of results.
  4. How to avoid the situation: “Architectural overkill”?
  5. The moment when the time comes to change everything.

Before you begin, a couple of notes:

  • This is a story about software architecture - in the sense that Uncle Bob uses. Yes, that one.
  • All characters, their names and code in the article are fictitious, any coincidences with reality are random.


Suppose I am an ordinary programmer-programmer on a project. The project is a game in which a single Hero (aka Hero) goes along a perfectly horizontal line from left to right. Monsters interfere with this wonderful journey. At the user's command, the Hero famously cuts them with a sword into the cabbage and does not blow into the mustache. The project already has 100K lines, and “you need more lines of features!” Let's look at our Hero:

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

Suppose also that my personal team lead, Vasily, can also go on vacation. Suddenly. Who would have thought? Further, the situation develops as standard: an Owl flies in and urgently needs, right yesterday, to make it possible to choose before the start of the game: play for the Hero with a club or with a sword.

It must be very fast. And yes, of course, he himself was a programmer for more than years than I can walk, so he knows that the task is for five minutes. But so be it, estimate for 2 hours. You just need to add a checkbox and if-chik.

Note: if-chik in the value @! ^% $ # @ ^% & @! 11 $ its # $% @ ^% &! @ !!! in &! ^% # $ ^%! 1 if-chic!

My thoughts: “Ha! Two hours! Done! ”:

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

If you found out in my decision, then, alas: I have two news for you:

  1. As if good: we both deliver. We deliver - from the word deliver value or from the word funny (through tears) code.
  2. And the bad one: without Vasily, the kapets project.

So what happened? It would seem so far nothing. But let's see what happens next (while we are doing everything we can to keep Vasily on vacation). And then there will be this: the QA department will pay attention to the weight of our Hero. And no, this is not because the Hero has to go on a diet, but here's why:

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

I forgot to fix the weight calculation. Well, you think, mistake, with whom does not happen ?! Slap-bloop, fuck-tibidoch, ready:

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

But quickly fixed it! The hero is now jumping, taking into account the weight, but. Result-oriented work! This is not for you to look at navels, architectural astronauts.

Well, really, then he corrected a little more. In the spell list, I had to do this:

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

And then Petya came and broke everything. Well, the truth is, we have such a june on the project. Inattentive.

He just needed to add the concept of “Weapon Level” to the formula for calculating the strength of the blow and the weight of the Hero. And Petya missed one of four cases. But nothing! I corrected everything:

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

Needless to say, when (suddenly!) It was necessary to add a bow / update formulas, again there were forgotten cases, bugs and that was all.

What went wrong? Where was I wrong, and what (except for the mates) did Vasily say when he returned from vacation?

Here you can not read the story further, but, for example, think about the eternal, about architecture.

And with those who still decided to read, we continue.

So, let's look at the classics:
Premature optimization is the root of all evil!
And ... uh ... that's not it. Here's the thing:

In OOP languages ​​(like Swift, for example), there are three main ways to extend the capabilities of a class:

  1. The first way is “naive”. We just saw him. Adding a checkbox. Adding Responsibility. Swelling class.
  2. The second way is inheritance. Everyone knows a powerful mechanism for reusing code. One could, for example:
    • To inherit the new Hero with the Bow and Hero from Dubina from the Hero (who is holding a sword, but this is not reflected in the name of the Hero class now). And then in the heirs, override the changed methods. This path is very bad (just trust me).
    • Make the base class (or protocol) a Hero, and remove all the features associated with a particular type of weapon as heirs
      : Hero with a
      sword: Hero, Hero with a bow: Hero, Hero with
      Dubina: Hero.
      This is better, but the names of these classes themselves look at us somehow displeased, fiercely and at the same time sad and perplexed. If they don’t look at someone like that, then I’ll try to write another article, where in addition to the boring masculine Hero they will be ...
  3. The third way is the separation of responsibility through dependency injection. It can be a dependency closed by a protocol or a closure (as if closed by a signature), whatever. The main thing is that the implementation of new responsibilities should leave the main class.

How can this look in our case? For example, like this (decision from 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
    }
}

What do you need to be so? Ninjutsu - protocol:

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

Protocol implementation example:

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

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

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

Similarly, Sword classes are written for: Club, Bow, Pike, etc. “It is easy to see” (c) that in the new architecture all the code that relates to each particular type of weapon is grouped in the corresponding class, and is not spread according to the Hero along with other types of weapons. This makes it easier to read and understand the Hero and any particular weapon. Plus, due to the requirements of the imposed protocol, it is much easier to track all the methods that need to be implemented when adding a new type of weapon or when adding a new feature to a weapon (for example, a weapon may have a price calculation method).

Here you can see that dependency injection has complicated the creation of Hero class objects. What used to be done as simply:

let lastHero = Hero()

Now it has turned into a set of instructions that will be duplicated wherever it is necessary to create a Hero. However, Vasily took care of this:

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

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

It is clear that Vasily had to sweat in order to scatter the pile that was piled up designed by Petya (and I).

Of course, looking at the last solution, the following thought may arise:
Ok, it turned out, norms. It’s convenient to read and expand, but are all these factories / protocols / dependencies a bunch of overheads? A code that does not give anything in terms of features, but exists only to organize the code. "Code for organizing the code", mgm. Is it really necessary to fence this garden always and everywhere?
An honest answer to the first question would be:
Yes, this is an overhead for features that business loves so much.
And to the question “when?” answers section:

The philosophy of the sword-man or “when did you have to rule?”


In the beginning was a sword man. In the sense system of the old code, this was quite normal. As long as there was one Hero and one weapon, there was no need to distinguish between them - all the same, there was nothing else for the hero. And the monolithic code confirmed this fact with its text.

Swordman - it doesn't even sound so bad.

And what did the first “naive" edits lead to? What did the addition of the if-chik lead to?

Adding an if-chic led to ... a mutant! Mutant, i.e. mutable object that can mutate between a “human” and “human Dubin”. At the same time, if you make a little mistake in the implementation of the mutant, then the state of “human-medullin” arises. Do not do like this! There is no need for “human Dubin” at all.

It is necessary:

  1. Man + addiction to the sword (need for a sword);
  2. Man + addiction to a club (need for a club).

Not every addiction is evil! This addiction to alcohol is evil, and protocol is good. Yes, even dependence on the object is better than “human Dubin”!

When did the mutation happen? The transformation into a mutant occurred when the flag was added: the monolithic code remained monolithic, but when the flag was changed (mutated), the behavior of the same object began to change significantly.

Vasily would single out two stages of mutation:

  1. Adding a flag and the very first “if” (or “switch”, or other branching mechanism) to the flag. The situation is threatening, but bearable: the hero is poured with radioactive waste, but he overcomes it.
  2. «if» , . . — . , - - .

Thus, in the situation considered, in order to prevent the occurrence of technical debt, it makes sense to build the architecture before passing through the first stage, in the worst case, to the second.

What exactly did Vasily do to treat a mutant?

From a technical point of view - used injection dependency closed protocol.

From philosophical - shared responsibility.

All the features of the work of the class, which can be interchanged with each other (which means they are alternative to each other), were removed from the Hero depending. A new, alternative functionality - the implementation of the work of the sword, the implementation of the work of the club - upon its appearance became different from each other and is different from the rest as beforeno alternative Hero code. So already in the "naive" code something newappeared that was different in its alternative behavior from the uncontested part of the Hero. So in the “naive" code there appeared an implicit description of new business entities : a sword and a club, spread out according to the Hero. In order to make it convenient to operate with new business entities , it became necessary to distinguish them as separate code entities with their own names . So there was a division of responsibility.

PS TL; DR;

  1. See the flag?
  2. Be a man, damn it! Erase it!
  3. Addiction injection
  4. ...
  5. Profit! 11

All Articles