Arquitectura para principiantes o por qué no necesita insertar una bandera en el hombre espada



Anotación:

  1. Un ejemplo de la implementación de una nueva funcionalidad en la clase agregando una "bandera".
  2. Efectos.
  3. Enfoque alternativo y comparación de resultados.
  4. ¿Cómo evitar la situación: "exceso arquitectónico"?
  5. El momento en que llega el momento de cambiarlo todo.

Antes de comenzar, un par de notas:

  • Esta es una historia sobre arquitectura de software, en el sentido que usa el tío Bob. Si, ese.
  • Todos los personajes, sus nombres y códigos en el artículo son ficticios, cualquier coincidencia con la realidad es aleatoria.


Supongamos que soy un programador-programador ordinario en un proyecto. El proyecto es un juego en el que un solo Héroe (también conocido como Héroe) va a lo largo de una línea perfectamente horizontal de izquierda a derecha. Los monstruos interfieren con este maravilloso viaje. A la orden del usuario, el héroe los corta con una espada en la col y no sopla el bigote. El proyecto ya tiene 100K líneas, y "¡necesitas más líneas de características!" Miremos a nuestro héroe:

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

Supongamos también que el líder de mi equipo personal, Vasily, también puede irse de vacaciones. Repentinamente. ¿Quien lo hubiera pensado? Además, la situación se desarrolla de manera estándar: un Búho vuela y necesita urgentemente, justo ayer, para poder elegir antes del comienzo del juego: jugar para el Héroe con un palo o una espada.

Debe ser muy rápido. Y sí, por supuesto, él mismo fue un programador durante más de años de lo que puedo caminar, por lo que sabe que la tarea es de cinco minutos. Pero que así sea, estimar por 2 horas. Solo necesita agregar una casilla de verificación y if-chik.

Nota: if-chik en el valor @! ^% $ # @ ^% & @! 11 $ its # $% @ ^% &! @ !!! en &! ^% # $ ^%! 1 if-chic!

Mis pensamientos: “¡Ja! ¡Dos horas! ¡Hecho! ":

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 te enteraste de mi decisión, entonces, por desgracia: tengo dos noticias para ti:

  1. Como si fuera bueno: los dos cumplimos. Entregamos: desde la palabra entregar valor o desde la palabra código divertido (a través de las lágrimas).
  2. Y el malo: sin Vasily, el proyecto de kapets.

¿Entonces qué pasó? Parece que hasta ahora nada. Pero veamos qué sucede después (mientras hacemos todo lo posible para mantener a Vasily de vacaciones). Y luego habrá esto: el departamento de control de calidad prestará atención al peso de nuestro héroe. Y no, esto no se debe a que el Héroe tenga que ponerse a dieta, sino por qué:

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

Olvidé arreglar el cálculo del peso. Bueno, piensas, error, ¿con quién no sucede? Slap-bloop, fuck-tibidoch, listo:

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

Pero rápidamente lo arregló! El héroe ahora está saltando, teniendo en cuenta el peso, pero. Trabajo orientado a resultados! Esto no es para que usted mire los ombligos, los astronautas arquitectónicos.

Bueno, de verdad, luego corrigió un poco más. En la lista de hechizos, tuve que hacer esto:

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

Y luego vino Petia y rompió todo. Bueno, la verdad es que tenemos un junio en el proyecto. Inatento.

Solo necesitaba agregar el concepto de "Nivel de Arma" a las fórmulas para calcular la fuerza del golpe y el peso del Héroe. Y Petya se perdió uno de los cuatro casos. ¡Pero nada! Lo corregí todo:

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

No hace falta decir que cuando (¡de repente!) Fue necesario agregar fórmulas de actualización / arco, nuevamente hubo casos olvidados, errores y eso fue todo.

¿Qué salió mal? ¿Dónde me equivoqué y qué (excepto los compañeros) dijo Vasily cuando regresó de vacaciones?

Aquí no puedes seguir leyendo la historia, pero, por ejemplo, piensa en lo eterno, en la arquitectura.

Y con aquellos que aún decidieron leer, continuamos.

Entonces, echemos un vistazo a los clásicos:
¡La optimización temprana es la raíz de todo mal!
Y ... eh ... eso no es todo. Aquí está la cosa:

en los lenguajes OOP (como Swift, por ejemplo), hay tres formas principales de extender las capacidades de una clase:

  1. La primera forma es "ingenua". Acabamos de verlo. Agregar una casilla de verificación. Agregar responsabilidad. Clase de hinchazón
  2. La segunda forma es la herencia. Todos conocen un mecanismo poderoso para reutilizar el código. Uno podría, por ejemplo:
    • Heredar el nuevo Héroe con el Arco y el Héroe de Dubina del Héroe (que sostiene una espada, pero esto no se refleja en el nombre de la clase Héroe ahora). Y luego, en los herederos, anule los métodos modificados. Este camino es muy malo (solo confía en mí).
    • Convierta a la clase base (o protocolo) en un Héroe, y elimine todas las características asociadas con un tipo específico de arma como herederos
      : Héroe con
      espada: Héroe, Héroe con arco: Héroe, Héroe con
      Dubina: Héroe.
      Esto es mejor, pero los nombres de estas clases nos miran de alguna manera disgustados, feroces y al mismo tiempo tristes y perplejos. Si no miran a alguien así, entonces intentaré escribir otro artículo, donde además del aburrido héroe masculino serán ...
  3. La tercera forma es la separación de responsabilidades a través de la inyección de dependencia. Puede ser una dependencia cerrada por un protocolo o un cierre (como si estuviera cerrado por una firma), lo que sea. Lo principal es que la implementación de nuevas responsabilidades debe abandonar la clase principal.

¿Cómo puede verse esto en nuestro caso? Por ejemplo, así (decisión 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
    }
}

¿Qué necesitas para ser así? Ninjutsu - protocolo:

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

Ejemplo de implementación de protocolo:

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

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

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

Del mismo modo, las clases de espada están escritas para: Club, Bow, Pike, etc. "Es fácil ver" (c) que en la nueva arquitectura todo el código que se aplica a cada tipo específico de arma se agrupa en la clase correspondiente y no se distribuye de acuerdo con el Héroe junto con los otros tipos de armas. Esto facilita la lectura y comprensión del Héroe y cualquier arma en particular. Además, debido a los requisitos del protocolo impuesto, es mucho más fácil rastrear todos los métodos que deben implementarse al agregar un nuevo tipo de arma o al agregar una nueva característica a un arma (por ejemplo, un arma puede tener un método de cálculo de precio).

Aquí puede ver que la inyección de dependencia ha complicado la creación de objetos de clase Hero. Lo que solía hacerse simplemente:

let lastHero = Hero()

Ahora se ha convertido en un conjunto de instrucciones que se duplicarán siempre que sea necesario para crear un héroe. Sin embargo, Vasily se encargó de esto:

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

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

Está claro que Vasily tuvo que sudar para dispersar la pila que se amontonó diseñada por Petya (y yo).

Por supuesto, mirando la última solución, puede surgir el siguiente pensamiento:
Ok, resultó, normas. Es conveniente leer y expandir, pero ¿son todas estas fábricas / protocolos / dependencias un montón de gastos generales? Un código que no da nada en términos de características, pero existe solo para organizar el código. "Código para organizar el código", mgm. ¿Es realmente necesario cercar este jardín siempre y en todas partes?
Una respuesta honesta a la primera pregunta sería:
Sí, esta es una sobrecarga para las características que a las empresas les encantan.
Y a la pregunta "¿cuándo?" sección de respuestas:

La filosofía del hombre espada o "¿cuándo tuviste que gobernar?"


Al principio era un hombre espada. En el sistema de sentido del antiguo código, esto era bastante normal. Mientras hubiera un héroe y un arma, no había necesidad de distinguir entre ellos; de todos modos, no había nada más para el héroe. Y el código monolítico confirmó este hecho con su texto.

Swordman: ni siquiera suena tan mal.

¿Y a qué condujeron las primeras ediciones "ingenuas"? ¿A qué condujo la adición del if-chik?

Agregar un if-chic condujo a ... ¡un mutante! Mutante, es decir objeto mutable que puede mutar entre un "humano" y un "humano Dubin". Al mismo tiempo, si comete un pequeño error en la implementación del mutante, entonces surge el estado de "medulina humana". ¡No hagas así! No hay necesidad de "Dubin humano" en absoluto.

Es necesario:

  1. Hombre + adicción a la espada (necesidad de una espada);
  2. Hombre + adicción a un club (necesidad de un club).

¡No todas las adicciones son malas! Esta adicción al alcohol es malvada, y el protocolo es bueno. ¡Sí, incluso la dependencia del objeto es mejor que la "Dubin humana"!

¿Cuándo ocurrió la mutación? La transformación en un mutante ocurrió cuando se agregó la bandera: el código monolítico permaneció monolítico, pero cuando la bandera se cambió (mutó), el comportamiento del mismo objeto comenzó a cambiar significativamente.

Vasily destacaría dos etapas de mutación:

  1. Agregar una bandera y el primer "si" (o "interruptor" u otro mecanismo de ramificación) a la bandera. La situación es amenazante, pero soportable: el héroe se vierte con desechos radiactivos, pero lo supera.
  2. «if» , . . — . , - - .

Por lo tanto, en la situación considerada, para evitar la aparición de deudas técnicas, tiene sentido construir la arquitectura antes de pasar por la primera etapa, en el peor de los casos, a la segunda.

¿Qué hizo exactamente Vasily para tratar a un mutante?

Desde un punto de vista técnico: se utiliza el protocolo cerrado de dependencia de inyección.

De filosófico - responsabilidad compartida.

Todas las características del trabajo de la clase, que pueden intercambiarse entre sí (lo que significa que son alternativas entre sí), se eliminaron del Héroe dependiendo. Una nueva funcionalidad alternativa , la implementación del trabajo de la espada, la implementación del trabajo del club, a su aparición se volvió diferente entre sí y es diferente del resto como antes.Sin código de héroe alternativo . Entonces, ya en el código "ingenuo" apareció algo nuevo que era diferente en su comportamiento alternativo de la parte incontestada del Héroe. Entonces, en el código "ingenuo" apareció una descripción implícita de nuevas entidades comerciales : una espada y un garrote, distribuidos según el Héroe. Para que sea conveniente operar con nuevas entidades comerciales , se hizo necesario distinguirlas como entidades de código separadascon sus propios nombres . Entonces hubo una división de responsabilidad.

PS TL; DR;

  1. ¿Ves la bandera?
  2. ¡Sé un hombre, maldita sea! ¡Bórralo!
  3. Inyección de adicción
  4. ...
  5. Beneficio! 11

All Articles