Arquitetura para iniciantes ou por que você não precisa inserir uma bandeira no homem-espada



Anotação:

  1. Um exemplo da implementação de novas funcionalidades na classe adicionando um "sinalizador".
  2. Efeitos.
  3. Abordagem alternativa e comparação de resultados.
  4. Como evitar a situação: "Excesso arquitetônico"?
  5. O momento em que chega a hora de mudar tudo.

Antes de começar, algumas notas:

  • Esta é uma história sobre arquitetura de software - no sentido que o tio Bob usa. Sim esse.
  • Todos os personagens, seus nomes e códigos no artigo são fictícios, quaisquer coincidências com a realidade são aleatórias.


Suponha que eu seja um programador-programador comum em um projeto. O projeto é um jogo no qual um único herói (também conhecido como herói) segue uma linha perfeitamente horizontal da esquerda para a direita. Monstros interferem nessa jornada maravilhosa. Sob o comando do usuário, o Herói os corta com uma espada no repolho e não sopra no bigode. O projeto já possui 100 mil linhas e "você precisa de mais linhas de recursos!" Vamos olhar para o nosso herói:

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

Suponha também que o líder da minha equipe pessoal, Vasily, também possa sair de férias. De repente. Quem teria pensado? Além disso, a situação se desenvolve como padrão: uma Coruja chega e precisa urgentemente, ontem, de tornar possível escolher antes do início do jogo: jogar para o Herói com um taco ou com uma espada.

Deve ser muito rápido. E sim, é claro, ele próprio foi programador por mais de anos do que eu posso andar, então ele sabe que a tarefa é de cinco minutos. Mas assim seja, faça uma estimativa de 2 horas. Você só precisa adicionar uma caixa de seleção e if-chik.

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

Meus pensamentos: “Ha! Duas horas! Feito! ":

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

Se você descobriu na minha decisão, então: infelizmente: tenho duas notícias para você:

  1. Como se fosse bom: nós dois entregamos. Nós entregamos - a partir da palavra agregar valor ou da palavra código engraçado (através de lágrimas).
  2. E o ruim: sem Vasily, o projeto kapets.

Então o que aconteceu? Parece até agora nada. Mas vamos ver o que acontece a seguir (enquanto fazemos todo o possível para manter Vasily de férias). E então haverá o seguinte: o departamento de controle de qualidade prestará atenção ao peso do nosso herói. E não, isso não ocorre porque o Herói precisa seguir uma dieta, mas eis o porquê:

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

Eu esqueci de corrigir o cálculo do peso. Bem, você pensa, erro, com quem não acontece ?! Slap-bloop, fuck-tibidoch, pronto:

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

Mas rapidamente consertou! O herói agora está pulando, levando em consideração o peso, mas. Trabalho orientado a resultados! Não é para você olhar para umbigos, astronautas arquitetônicos.

Bem, sério, então ele corrigiu um pouco mais. Na lista de feitiços, eu tinha que fazer isso:

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

E então Petya veio e quebrou tudo. Bem, a verdade é que temos um mês de junho no projeto. Desatento.

Ele só precisava adicionar o conceito de "Nível de Arma" às fórmulas para calcular a força do golpe e o peso do Herói. E Petya perdeu um dos quatro casos. Mas nada! Corrigi tudo:

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

Escusado será dizer que, quando (de repente!) Foi necessário adicionar um arco / atualizar fórmulas, novamente houve casos esquecidos, bugs e isso foi tudo.

O que deu errado? Onde eu estava errado, e o que (exceto os companheiros) Vasily disse quando ele voltou das férias?

Aqui você não pode ler mais a história, mas, por exemplo, pensar sobre o eterno, sobre arquitetura.

E com aqueles que ainda decidiram ler, continuamos.

Então, vamos olhar para os clássicos:
Otimização prematura é a raiz de todo o mal!
E ... não é isso. Aqui está a coisa:

Nas linguagens OOP (como Swift, por exemplo), existem três maneiras principais de estender os recursos de uma classe:

  1. A primeira maneira é "ingênua". Nós apenas o vimos. Adicionando uma caixa de seleção. Adicionando responsabilidade. Aula de inchaço.
  2. A segunda maneira é herança. Todo mundo conhece um mecanismo poderoso para reutilizar código. Pode-se, por exemplo:
    • Herda o novo Herói com o Arco e o Herói de Dubina do Herói (que está segurando uma espada, mas isso não está refletido no nome da classe do Herói agora). E então, nos herdeiros, substitua os métodos alterados. Este caminho é muito ruim (apenas confie em mim).
    • Torne a classe base (ou protocolo) um Herói e remova todos os recursos associados a um tipo específico de arma como herdeiro
      : Herói com uma
      espada: Herói, Herói com um arco: Herói, Herói com
      Dubina: Herói.
      Isso é melhor, mas os próprios nomes dessas classes nos olham de alguma maneira descontentes, ferozmente e ao mesmo tempo tristes e perplexos. Se eles não olharem para alguém assim, tentarei escrever outro artigo, onde, além do chato herói masculino, eles serão ...
  3. A terceira maneira é a separação de responsabilidades através da injeção de dependência. Pode ser uma dependência fechada por um protocolo ou um fechamento (como se estivesse fechada por uma assinatura), qualquer que seja. O principal é que a implementação de novas responsabilidades saia da classe principal.

Como isso pode parecer no nosso caso? Por exemplo, assim (decisão 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
    }
}

O que você precisa ser assim? Ninjutsu - protocolo:

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

Exemplo de implementação de protocolo:

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

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

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

Da mesma forma, as aulas de espada são escritas para: Club, Bow, Pike, etc. “É fácil ver” (c) que na nova arquitetura todo o código que se aplica a cada tipo específico de arma está agrupado na classe correspondente e não é espalhado de acordo com o Herói, juntamente com outros tipos de armas. Isso facilita a leitura e a compreensão do Herói e de qualquer arma específica. Além disso, devido aos requisitos do protocolo imposto, é muito mais fácil rastrear todos os métodos que precisam ser implementados ao adicionar um novo tipo de arma ou ao adicionar um novo recurso a uma arma (por exemplo, uma arma pode ter um método de cálculo de preço).

Aqui você pode ver que a injeção de dependência complicou a criação de objetos da classe Hero. O que costumava ser feito de maneira simples:

let lastHero = Hero()

Agora ele se transformou em um conjunto de instruções que serão duplicadas sempre que for necessário criar um Herói. No entanto, Vasily cuidou disso:

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

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

É claro que Vasily teve que suar para espalhar a pilha que foi empilhada, projetada por Petya (e eu).

Obviamente, olhando para a última solução, o seguinte pensamento pode surgir:
Ok, acabou, normas. É conveniente ler e expandir, mas todas essas fábricas / protocolos / dependências são um monte de despesas gerais? Um código que não fornece nada em termos de recursos, mas existe apenas para organizar o código. "Código para organizar o código", mgm. É realmente necessário cercar este jardim sempre e em qualquer lugar?
Uma resposta honesta para a primeira pergunta seria:
Sim, essa é uma sobrecarga para os recursos que as empresas amam tanto.
E para a pergunta "quando?" seção de respostas:

A filosofia do homem-espada ou "quando você teve que governar?"


No começo era um homem de espada. No sistema dos sentidos do código antigo, isso era bastante normal. Desde que houvesse um herói e uma arma, não havia necessidade de distinguir entre eles - mesmo assim, não havia mais nada para o herói. E o código monolítico confirmou esse fato com seu texto.

Espadachim - nem parece tão ruim.

E a que as primeiras edições “ingênuas” levaram? A que a adição do if-chik levou?

Adicionar um if-chic levou a ... um mutante! Mutante, isto é, objeto mutável que pode alternar entre um "humano" e "humano Dubin". Ao mesmo tempo, se você cometer um pequeno erro na implementação do mutante, o estado de "medulaína humana" surge. Não faça assim! Não há necessidade de "Dubin humano".

É necessário:

  1. Homem + dependência da espada (necessidade de espada);
  2. Homem + dependência de um clube (necessidade de um clube).

Nem todo vício é mau! Esse vício em álcool é mau e o protocolo é bom. Sim, até a dependência do objeto é melhor que o "Dubin humano"!

Quando a mutação aconteceu? A transformação em mutante ocorreu quando a bandeira foi adicionada: o código monolítico permaneceu monolítico, mas quando a bandeira foi alterada (mutada), o comportamento do mesmo objeto começou a mudar significativamente.

Vasily destacaria dois estágios da mutação:

  1. Adicionando uma bandeira e o primeiro "se" (ou "alternar" ou outro mecanismo de ramificação) à bandeira. A situação é ameaçadora, mas suportável: o herói é derramado com lixo radioativo, mas ele o supera.
  2. «if» , . . — . , - - .

Assim, na situação considerada, para evitar a ocorrência de dívida técnica, faz sentido construir a arquitetura antes de passar pela primeira etapa, na pior das hipóteses, para a segunda.

O que exatamente Vasily fez para tratar um mutante?

Do ponto de vista técnico - protocolo fechado de dependência de injeção usado.

Da responsabilidade filosófica - compartilhada.

Todas as características do trabalho da classe, que podem ser trocadas entre si (o que significa que são alternativas umas às outras), foram removidas do Herói, dependendo. Uma nova funcionalidade alternativa - a implementação do trabalho da espada, a implementação do trabalho do clube - após sua aparência se tornou diferente uma da outra e é diferente do restonenhum código de herói alternativo . Então, já no código "ingênuo", algo novopareciadiferente em seu comportamento alternativo da parte não contestada do Herói. Assim, no código "ingênuo", apareceu uma descrição implícita de novas entidades comerciais : uma espada e um taco, espalhados de acordo com o Herói. Para facilitar a operação com novas entidades comerciais , tornou-se necessário distingui-las como entidades de código separadascom seus próprios nomes . Portanto, houve uma divisão de responsabilidade.

PS TL; DR;

  1. Veja a bandeira?
  2. Seja um homem, droga! Apague isso!
  3. Injeção de dependência
  4. ...
  5. Lucro! 11

All Articles