OOP em imagens

OOP (Programação Orientada a Objetos) tornou-se parte integrante do desenvolvimento de muitos projetos modernos, mas, apesar de sua popularidade, esse paradigma está longe de ser o único. Se você já sabe trabalhar com outros paradigmas e gostaria de se familiarizar com o ocultismo da OOP, então está à sua frente um pouco de sol e dois megabytes de fotos e animações. Os transformadores servirão como exemplos.



A primeira coisa a responder é por quê? A ideologia orientada a objetos foi desenvolvida como uma tentativa de conectar o comportamento de uma entidade com seus dados e projetar objetos do mundo real e processos de negócios no código do programa. Pensou-se que esse código é mais fácil de ler e entender por uma pessoa, porque as pessoas tendem a perceber o mundo ao seu redor como uma infinidade de objetos interagindo entre si, passíveis de uma determinada classificação. É possível que os ideólogos alcancem a meta, é difícil responder inequivocamente, mas, de fato, temos muitos projetos nos quais o programador exigirá OOP.

Você não deve pensar que o POO de alguma forma acelerará milagrosamente a redação de programas e espera uma situação em que os moradores de Villaribo já tenham lançado o projeto OOP para trabalhar, e os habitantes de Villabaggio ainda estejam lavando o código de espaguete gordo. Na maioria dos casos, não é assim, e o tempo é economizado não no estágio de desenvolvimento, mas nos estágios de suporte (expansão, modificação, depuração e teste), ou seja, a longo prazo. Se você precisar escrever um script único que não precise de suporte subsequente, o POO nesta tarefa provavelmente não será útil. No entanto, uma parte significativa do ciclo de vida da maioria dos projetos modernos é precisamente de suporte e expansão. Ter OOP sozinho não torna sua arquitetura perfeita e vice-versa pode levar a complicações desnecessárias.

Às vezes, você pode encontrar críticas ao desempenho dos programas de POO. É verdade que uma pequena sobrecarga está presente, mas tão insignificante que, na maioria dos casos, pode ser negligenciada em favor de vantagens. No entanto, nos gargalos em que milhões de objetos por segundo devem ser criados ou processados ​​em um encadeamento, vale a pena revisar a necessidade de POO, pois mesmo a sobrecarga mínima nessas quantidades pode afetar significativamente o desempenho. A criação de perfil ajudará você a capturar a diferença e a tomar uma decisão. Em outros casos, digamos, onde a maior parte da velocidade depende da E / S, o abandono de objetos será uma otimização prematura.

Por sua própria natureza, a programação orientada a objetos é melhor explicada com exemplos. Como prometido, nossos pacientes serão transformadores. Eu não sou um transformador e não li quadrinhos, portanto, nos exemplos serei guiado pela Wikipedia e pela fantasia.

Classes e Objetos


Digressão imediatamente lírica: uma abordagem orientada a objetos é possível sem classes, mas consideraremos: peço desculpas pelo trocadilho, o esquema clássico, onde as classes são tudo.

A explicação mais simples: uma classe é um desenho de um transformador, e as instâncias dessa classe são transformadores específicos, por exemplo, Optimus Prime ou Oleg. E embora sejam montados de acordo com um desenho, eles podem andar, transformar e disparar da mesma maneira, ambos têm seu próprio estado único. Um estado é uma série de propriedades alteradas. Portanto, em dois objetos diferentes da mesma classe, podemos observar um nome diferente, idade, localização, nível de carga, quantidade de munição etc. A própria existência dessas propriedades e seus tipos é descrita na classe.

Assim, uma classe é uma descrição de quais propriedades e comportamentos um objeto possuirá. E um objeto é uma instância com seu próprio estado dessas propriedades.

Dizemos "propriedades e comportamento", mas parece algo abstrato e incompreensível. Será mais familiar para um programador soar assim: “variáveis ​​e funções”. De fato, “propriedades” são as mesmas variáveis ​​comuns, são simplesmente atributos de algum objeto (elas são chamadas de campos de objeto). Da mesma forma, "comportamento" é a função de um objeto (eles são chamados métodos), que também são atributos do objeto. A diferença entre o método do objeto e a função usual é apenas que o método tem acesso ao seu próprio estado através dos campos.

No total, temos métodos e propriedades que são atributos. Como trabalhar com atributos? Na maioria dos PLs, o operador de referência de atributo é o ponto (exceto PHP e Perl). Parece algo assim (pseudo-código):

//       class class Transformer(){ //   x int x //    (     0) function constructor(int x){ //   x // (  0    ) this.x = x } //   run function run(){ //      this this.x += 1 } } //    : //        0 optimus = new Transformer(0) optimus.run() //    print optimus.x //  1 optimus.run() //      print optimus.x //  2 

Nas fotos, usarei a seguinte notação:



Não usei diagramas UML, considerando-os insuficientemente visuais, embora mais flexíveis.


Animação número 1

O que vemos do código?

1. essa é uma variável local especial (métodos internos) que permite que um objeto acesse seus próprios atributos a partir de seus métodos. Chamo a atenção de vocês apenas para os seus, ou seja, quando o transformador chama seu próprio método ou muda seu próprio estado. Se a chamada se parecer com isso de fora: optimus.x , por dentro, se a Optimus quiser acessar seu campo x, no seu método, a chamada será assim.x , ou seja, " I (Optimus) me refiro ao meu atributo x ". Na maioria dos idiomas, essa variável é chamada assim, mas há exceções (por exemplo, auto)

2. construtor é um método especial chamado automaticamente quando um objeto é criado. O construtor pode aceitar qualquer argumento, como qualquer outro método. Em cada idioma, um construtor é indicado por seu nome. Em algum lugar, esses são nomes especialmente reservados, como __construct ou __init__, e em algum lugar o nome do construtor deve corresponder ao nome da classe. O objetivo dos construtores é inicializar o objeto, preencha os campos obrigatórios.

3. new é uma palavra-chave que deve ser usada para criar uma nova instância de uma classe. Neste ponto, um objeto é criado e o construtor é chamado. No nosso exemplo, 0 é passado para o construtor como a posição inicial do transformador (esta é a inicialização acima). A nova palavra-chave está ausente em alguns idiomas e o construtor é chamado automaticamente ao tentar chamar a classe como uma função, por exemplo: Transformer ().

4. O construtor e os métodos de execução trabalham com o estado interno, mas em todos os outros aspectos não diferem das funções comuns . Até a sintaxe da declaração corresponde.

5. As classes podem possuir métodos que não precisam de estado e, como resultado, criar um objeto. Nesse caso, o método é feito estático .

Srp


(Princípio de responsabilidade única / Primeiro princípio do SOLID ). Você provavelmente já está familiarizado com isso de outros paradigmas: “uma função deve executar apenas uma ação concluída”. Este princípio também é válido para as classes: "Uma classe deve ser responsável por qualquer tarefa". Infelizmente com as classes, é mais difícil definir a linha que precisa ser ultrapassada para que o princípio seja violado.

Existem tentativas de formalizar esse princípio descrevendo o objetivo de uma classe com uma frase sem sindicatos, mas essa é uma técnica muito controversa; portanto, confie na sua intuição e não se apresse ao extremo. Você não precisa fazer uma faca suíça de uma classe, mas produzir um milhão de classes com um método dentro também é estúpido.

Associação


Tradicionalmente, nos campos de um objeto, não apenas variáveis ​​comuns de tipos padrão podem ser armazenadas, mas também outros objetos. E esses objetos, por sua vez, podem armazenar outros objetos e assim por diante, formando uma árvore (às vezes um gráfico) de objetos. Esse relacionamento é chamado de associação.

Suponha que nosso transformador esteja equipado com uma arma. Embora não, melhor com duas armas. Em cada mão. As armas são as mesmas (pertencem à mesma classe, ou, se você preferir, fabricadas de acordo com um desenho), ambas podem disparar e recarregar igualmente, mas cada uma tem seu próprio armazenamento de munição (próprio estado). Como agora para descrevê-lo no OOP? Por associação:

 class Gun(){ //    int ammo_count //    function constructor(){ //  this.reload() //    "" } function fire(){ //    "" this.ammo_count -= 1 //      } function reload(){ //   "" this.ammo_count = 10 //     } } class Transformer(){ //    Gun gun_left //   " "   Gun gun_right //   " "    /*            ,    */ function constructor(Gun gun_left, Gun gun_right){ this.gun_left = gun_left //      this.gun_right = gun_right //      } //    "",   ... function fire(){ //  ,    "" this.gun_left.fire() //    ,     "" this.gun_right.fire() } } gun1 = new Gun() //    gun2 = new Gun() //    optimus = new Transformer(gun1, gun2) //  ,     


Animação número 2

this.gun_left.fire () e this.gun_right.fire () são chamadas para objetos filhos que também acontecem através de pontos. No primeiro ponto, voltamos para o atributo de nós mesmos (this.gun_right), obtendo o objeto gun, e no segundo ponto, voltamos para o método do objeto gun (this.gun_right.fire ()).

Conclusão: o robô foi fabricado, a arma de serviço foi lançada, agora vamos descobrir o que está acontecendo aqui. Nesse código, um objeto se tornou parte integrante de outro objeto. Esta é a associação. Por sua vez, é de dois tipos:

1. Composição - um caso em que na fábrica de transformadores, coletando Optimus, as duas armas estão presas firmemente às mãos com unhas e, após a morte de Optimus, as armas morrem com ele. Em outras palavras, o ciclo de vida da criança é o mesmo que o ciclo de vida dos pais.

2. Agregação - o caso em que uma arma é emitida como uma arma em sua mão e, após a morte de Optimus, essa arma pode ser apanhada por seu camarada Oleg, e depois levada para sua mão ou transformada em uma loja de penhores. Ou seja, o ciclo de vida de um objeto filho não depende do ciclo de vida do pai e pode ser usado por outros objetos.

A igreja ortodoxa da OOP nos prega uma trindade fundamental - encapsulamento, polimorfismo e herança , na qual toda a abordagem orientada a objetos se baseia. Vamos ordená-los em ordem.



Herança


A herança é um mecanismo do sistema que permite, por mais paradoxal que possa parecer, herdar as propriedades e o comportamento de outras classes por algumas classes para expansão ou modificação adicional.

E se não quisermos carimbar os mesmos transformadores, mas queremos criar um quadro comum, mas com um kit de carroceria diferente? OOP nos permite essa brincadeira, dividindo a lógica em semelhanças e diferenças, com a remoção subsequente das semelhanças na classe pai e nas diferenças nas classes descendentes. Como é isso?

Optimus Prime e Megatron são ambos transformadores, mas um é um Autobot e o outro é um Decepticon. Suponha que as diferenças entre Autobots e Decepticons consistam apenas no fato de que os Autobots são transformados em carros e os Decepticons - em aviação. Todas as outras propriedades e comportamentos não farão diferença. Nesse caso, o sistema de herança pode ser projetado da seguinte maneira: recursos comuns (corrida, disparo) serão descritos na classe base Transformer e diferenças (transformação) nas duas classes filho Autobot e Decepticon.

 class Transformer(){ //   function run(){ // ,    } function fire(){ // ,    } } class Autobot(Transformer){ //  ,   Transformer function transform(){ // ,      } } class Decepticon(Transformer){ //  ,   Transformer function transform(){ // ,      } } optimus = new Autobot() megatron = new Decepticon() 


Animação número 3

Este exemplo ilustra como a herança se torna uma das maneiras de desduplicar código ( princípio DRY ) usando a classe pai e, ao mesmo tempo, oferece oportunidades para mutação nas classes descendentes.

Sobrecarga


Se você substituir um método existente na classe pai na classe pai, a sobrecarga funcionará. Isso nos permite não complementar o comportamento da classe pai, mas modificá-lo. No momento de chamar o método ou acessar o campo do objeto, a pesquisa pelo atributo ocorre do descendente até a raiz - o pai. Ou seja, se o método fire () for chamado no autobot, o método será pesquisado primeiro na classe descendente - Autobot e, como não existe, a pesquisa aumenta um passo mais - na classe Transformer, onde será detectada e chamada.

Uso inadequado


É curioso que uma hierarquia de herança excessivamente profunda possa levar ao efeito oposto - complicação ao tentar descobrir quem é herdado de quem e qual método é chamado nesse caso. Além disso, nem todos os requisitos de arquitetura podem ser implementados usando herança. Portanto, a herança deve ser aplicada sem fanatismo. Existem recomendações que exigem uma composição preferida sobre herança, quando apropriado. Qualquer crítica à herança que conheci é reforçada por exemplos mal sucedidos quando a herança é usada como um martelo de ouro . Mas isso não significa que a herança seja sempre prejudicial em princípio. Meu narcologista disse que o primeiro passo é admitir que você é dependente de herança.

Ao descrever as relações de duas entidades, como é possível determinar quando a herança é apropriada e quando a composição é apropriada? Você pode usar a folha de dicas populares: pergunte a si mesmo, a entidade A é a entidade B ? Nesse caso, a herança mais provável é apropriada. Se a entidade A fizer parte da entidade B , nossa escolha será a composição.

Em relação à nossa situação, será assim:

  1. O Autobot Transformer? Sim, então escolhemos herança.
  2. A arma faz parte do Transformer? Sim, isso significa composição.

Para um autoteste, tente a combinação inversa, você recebe lixo. Esta folha de dicas ajuda na maioria dos casos, mas há outros fatores em que você deve confiar ao escolher entre composição e herança. Além disso, esses métodos podem ser combinados para resolver diferentes tipos de problemas.

A herança é estática


Outra diferença importante entre herança e composição é que a herança é estática por natureza e estabelece relações de classe apenas no estágio de interpretação / compilação. A composição, como vimos nos exemplos, permite alterar o relacionamento das entidades dinamicamente no tempo de execução - às vezes isso é muito importante, portanto, você deve se lembrar disso ao escolher os relacionamentos (a menos que haja um desejo de usar a metaprogramação ).

Herança múltipla


Examinamos uma situação em que duas classes são herdadas de um descendente comum. Mas em alguns idiomas você pode fazer o oposto - herdar uma classe de dois ou mais pais, combinando suas propriedades e comportamento. A capacidade de herdar de várias classes em vez de uma é herança múltipla.



Em geral, nos círculos Illuminati, existe uma opinião de que a herança múltipla é um pecado, traz consigo um problema em forma de diamante e confusão com os designers. Além disso, tarefas que podem ser resolvidas por herança múltipla podem ser resolvidas por outros mecanismos, por exemplo, o mecanismo de interface (sobre o qual também falaremos). Mas, para ser justo, deve-se notar que a herança múltipla é conveniente para a implementação de impurezas .

Classes abstratas


Além das classes comuns, existem idiomas abstratos em alguns idiomas. Eles diferem das classes comuns, pois você não pode criar um objeto dessa classe. Por que precisamos dessa classe, o leitor perguntará? É necessário para que os descendentes possam ser herdados dele - classes comuns cujos objetos já podem ser criados.

A classe abstrata, juntamente com os métodos usuais, contém métodos abstratos sem implementação (com uma assinatura, mas sem código), que o programador que planeja criar uma classe descendente deve implementar. As classes abstratas não são necessárias, mas ajudam a estabelecer um contrato que exige a implementação de um conjunto específico de métodos para proteger um programador com memória insuficiente de um erro de implementação.

Polimorfismo


Polimorfismo é uma propriedade do sistema que permite que você tenha muitas implementações de uma interface. Nada está claro. Vamos virar para transformadores.

Suponha que tenhamos três transformadores: Optimus, Megatron e Oleg. Os transformadores são de combate, então eles têm o método attack (). O jogador, pressionando o botão "luta" no joystick, diz ao jogo para chamar o método attack () no transformador para o qual está jogando. Mas como os transformadores são diferentes e o jogo é interessante, cada um deles irá atacar de alguma forma. Digamos que o Optimus seja um objeto da classe Autobot, e os Autobots estão equipados com canhões com ogivas de plutônio (sim, os fãs dos transformadores não ficam bravos). Megatron é um Decepticon e dispara de uma arma de plasma. Oleg é um baixista, e ele o chama de nomes. E qual é a utilidade?

A vantagem do polimorfismo neste exemplo é que o código do jogo não sabe nada sobre a implementação de sua solicitação, quem deve atacar como, sua tarefa é simplesmente chamar o método attack (), cuja assinatura é a mesma para todas as classes de personagens. Isso permite adicionar novas classes de personagens ou alterar os métodos existentes sem alterar o código do jogo. Isso é conveniente.

Encapsulamento


Encapsulamento é o controle de acesso aos campos e métodos de um objeto. O controle de acesso implica não apenas possível / inconseqüente, mas também várias validações, cargas, cálculos e outros comportamentos dinâmicos.

Em muitos idiomas, a criptografia de dados faz parte do encapsulamento. Para isso, existem modificadores de acesso (descrevemos aqueles que estão em quase todos os idiomas OOP):

  • publi - qualquer pessoa pode acessar o atributo
  • private - somente métodos desta classe podem acessar o atributo
  • protegido - o mesmo que privado, somente os herdeiros da classe têm acesso, incluindo

 class Transformer(){ public function constructor(){ } protected function setup(){ } private function dance(){ } } 

Como escolher o modificador de acesso? No caso mais simples como este: se o método deve ser acessível ao código externo, selecione público. Caso contrário, privado. Se houver herança, poderá ser necessário proteger se o método não for chamado externamente, mas for chamado pelos descendentes.

Acessores (getters e setters)


Getters e setters são métodos cuja tarefa é controlar o acesso aos campos. O getter lê e retorna o valor do campo, e o setter, ao contrário, pega o valor como argumento e o grava no campo. Isso torna possível fornecer esses métodos com tratamentos adicionais. Por exemplo, um setter, ao gravar um valor em um campo de objeto, pode verificar o tipo ou se o valor está no intervalo de valores válidos (validação). No getter, você pode adicionar inicialização ou cache lento se o valor real estiver no banco de dados. Existem muitas aplicações.

Algumas linguagens possuem açúcar sintático que permite que esses acessadores sejam mascarados como propriedades, o que torna o acesso transparente ao código externo, que não suspeita que ele funcione não com um campo, mas com um método que executa uma consulta SQL ou lê um arquivo sob o capô. É assim que a abstração e a transparência são alcançadas.

Interfaces


A tarefa da interface é reduzir o nível de dependência de entidades entre si, adicionando mais abstração.

Nem todos os idiomas possuem esse mecanismo, mas em idiomas OOP com digitação estática sem eles, seria muito ruim. Acima, consideramos classes abstratas, abordando o tema dos contratos que obrigam a implementar alguns métodos abstratos. Portanto, a interface se parece muito com uma classe abstrata, mas não é uma classe, mas apenas um manequim com uma enumeração de métodos abstratos (sem implementação). Em outras palavras, a interface é de natureza declarativa, ou seja, um contrato limpo, sem um pouco de código.

Normalmente, os idiomas que possuem interfaces não possuem herança de várias classes, mas há herança de várias interfaces. Isso permite que a classe liste as interfaces que se compromete a implementar.

Classes com interfaces consistem em um relacionamento muitos-para-muitos: uma única classe pode implementar várias interfaces e cada interface, por sua vez, pode ser implementada por muitas classes.

A interface possui uso frente e verso:

  1. De um lado da interface, há classes que implementam essa interface.
  2. Por outro lado, estão os consumidores que usam essa interface como uma descrição do tipo de dados com o qual eles (consumidores) trabalham.

Por exemplo, se um objeto diferente do comportamento básico puder ser serializado, deixe-o implementar a interface Serializable. E se o objeto puder ser clonado, deixe-o implementar outra interface - “Clonado”. E se tivermos algum tipo de módulo de transporte que transmita objetos pela rede, ele aceitará qualquer objeto que implemente a interface serializável.

Imagine que a estrutura do transformador esteja equipada com três slots: um slot para armas, um gerador de energia e algum tipo de scanner. Esses slots têm certas interfaces: somente equipamentos adequados podem ser instalados em cada slot. No slot para armas, você pode instalar um lançador de foguetes ou uma pistola a laser, no slot para o gerador de energia - um reator nuclear ou RTG (gerador termoelétrico radioisótopo) e no slot para o scanner - um radar ou lidar. A linha inferior é que cada slot possui uma interface de conexão universal e dispositivos específicos devem corresponder a essa interface. Por exemplo, vários tipos de slots são usados ​​nas placas-mãe: um slot de processador permite conectar vários processadores adequados para esse soquete, e um slot SATA permite conectar qualquer unidade SSD ou HDD ou mesmo um CD / DVD.

Chamo a atenção para o fato de que o sistema resultante de slots para transformadores é um exemplo do uso da composição. Se o equipamento nos slots for substituível durante a vida útil do transformador, isso já é agregação. Para maior clareza, chamaremos as interfaces, como é habitual em alguns idiomas, adicionando a letra “E” antes do nome: IWeapon, IEnergyGenerator, IScanner.

 //  : interface IWeapon{ function fire() {} //    .   } interface IEnergyGenerator{ //    ,     : function generate_energy() {} //  function load_fuel() {} //  } interface IScanner{ function scan() {} } // ,  : class RocketLauncher() : IWeapon { function fire(){ //    } } class LaserGun() : IWeapon { function fire(){ //    } } class NuclearReactor() : IEnergyGenerator { function generate_energy(){ //      } function load_fuel(){ //     } } class RITEG() : IEnergyGenerator { function generate_energy(){ //     } function load_fuel(){ //   - } } class Radar() : IScanner { function scan(){ //    } } class Lidar() : IScanner { function scan(){ //     } } //  - : class Transformer() { // , : IWeapon slot_weapon //      . IEnergyGenerator slot_energy_generator //     , IScanner slot_scanner //     /*         ,      ,   : */ function install_weapon(IWeapon weapon){ this.slot_weapon = weapon } function install_energy_generator(IEnergyGenerator energy_generator){ this.slot_energy_generator = energy_generator } function install_scanner(IScanner scanner){ this.slot_scanner = scanner } } //   class TransformerFactory(){ function build_some_transformer() { transformer = new Transformer() laser_gun = new LaserGun() nuclear_reactor = new NuclearReactor() radar = new Radar() transformer.install_weapon(laser_gun) transformer.install_energy_generator(nuclear_reactor) transformer.install_scanner(radar) return transformer } } //  transformer_factory = new TransformerFactory() oleg = transformer_factory.build_some_transformer() 


Animação nº 4

Infelizmente, a fábrica não se encaixava na imagem, mas ainda é opcional, o transformador também pode ser montado no quintal.

A camada de abstração indicada na figura na forma de interfaces entre a camada de implementação e a camada consumidora torna possível abstrair uma da outra. Você pode observar isso observando cada camada separadamente: na camada de implementação (à esquerda) não há uma palavra sobre a classe Transformer e, na camada do consumidor (à direita), não há uma palavra sobre implementações específicas (não há palavras Radar, RocketLauncher, NuclearReactor etc.) d.)

Neste código, podemos criar novos componentes para transformadores sem afetar os desenhos dos próprios transformadores. Ao mesmo tempo e vice-versa, podemos criar novos transformadores combinando componentes existentes ou adicionar novos componentes sem alterar os existentes.

Digitação de pato


O fenômeno que observamos na arquitetura resultante é chamado de tipagem de pato : se algo grasna como um pato, nada como um pato e se parece com um pato, provavelmente é um pato .

Traduzindo isso para a linguagem dos transformadores, soará assim: se algo disparar como um canhão e recarregar como um canhão, provavelmente é um canhão. Se o dispositivo gera energia, provavelmente é um gerador de energia.

Em contraste com a tipificação hierárquica da herança, com a tipagem do pato, o transformador não se importa com a classe que a arma foi dada a ele e se é uma arma. O principal é que essa coisa pode disparar! Isso não é uma virtude da digitação de patos, mas um compromisso. Pode haver uma situação inversa, como nesta figura abaixo:



ISP

(Princípio de segregação de interface / Quarto princípio de SOLID) incentiva a não criar interfaces universais ousadas. Em vez disso, as interfaces devem ser divididas em menores e especializadas, o que ajudará a combiná-las de maneira mais flexível na implementação de classes, sem forçar a implementação de métodos desnecessários.

Abstração


No POO, tudo gira em torno da abstração. Há fanáticos que afirmam que a abstração deve fazer parte da trindade OOP (encapsulamento, polimorfismo, herança). E meu inspetor para testes de liberdade condicional disse o contrário: a abstração é inerente a qualquer programação, e não apenas à OOP, portanto deve ser separada. Por outro lado, o mesmo pode ser dito sobre o restante dos princípios, mas você não apaga as palavras de uma música. De uma forma ou de outra, a abstração é necessária, e especialmente no OOP.

Nível de abstração


Aqui não se pode deixar de citar uma piada bem conhecida:
- qualquer problema de arquitetura pode ser resolvido adicionando uma camada adicional de abstração, exceto pelo problema de um grande número de abstrações.

Em nosso exemplo com interfaces, implementamos uma camada de abstração entre transformadores e componentes, tornando a arquitetura mais flexível. Mas a que custo? Tivemos que complicar a arquitetura. Meu psicoterapeuta disse que a capacidade de equilibrar entre a simplicidade da arquitetura e a flexibilidade do aplicativo é uma arte. Ao escolher um meio termo, deve-se confiar não apenas na própria experiência e intuição, mas também no contexto do projeto atual. Como a pessoa ainda não aprendeu a ver o futuro, é necessário estimar analiticamente qual nível de abstração e com que grau de probabilidade pode ser útil neste projeto, quanto tempo será necessário para desenvolver uma arquitetura flexível e se o tempo gasto será recompensado no futuro.

A seleção incorreta do nível de abstração leva a um dos dois problemas:

  1. , , , ( )
  2. , , , , . ( )



Também é importante entender que o nível de abstração é determinado não para todo o projeto como um todo, mas separadamente para diferentes componentes. Em alguns lugares, o sistema de abstração pode não ser suficiente, mas em algum lugar pelo contrário - falido. No entanto, a escolha errada do nível de abstração pode ser corrigida mediante refatoração oportuna. A palavra-chave é oportuna . A refatoração atrasada é problemática quando muitos mecanismos já estão implementados nesse nível de abstração. Realizar um ritual de refatoração em sistemas em execução pode envolver dor aguda em pontos de difícil acesso de um programador. É sobre como mudar a base de uma casa - é mais barato construir uma casa ao lado do zero.

Vejamos a definição do nível de abstração a partir das opções possíveis no exemplo de um jogo hipotético "transformers-online". Nesse caso, os níveis de abstração atuarão como camadas, cada camada subseqüente em consideração ficará sobre a anterior, participando do funcional em si.

Primeira camada. O jogo tem uma classe de transformador, todas as propriedades e comportamento estão descritos nele. Este é um nível de abstração completamente de madeira, adequado para brincadeiras casuais, o que não implica em nenhuma flexibilidade especial.

O segundo nível.O jogo possui um transformador básico com habilidades básicas e classes de transformadores com sua própria especialização (como batedor, aeronave de ataque, suporte), descrito por métodos adicionais. Assim, o jogador tem a oportunidade de escolher, e o desenvolvimento das novas classes é simplificado.

Terceiro nível Além da classificação dos transformadores, a agregação é introduzida usando um sistema de slots e componentes (como no exemplo de reatores, pistolas e radares). Agora, parte do comportamento será determinada por qual equipe o jogador instalou em seu transformador. Isso dá ao jogador ainda mais oportunidades para personalizar a mecânica do personagem do jogo e dá aos desenvolvedores a oportunidade de adicionar esses mesmos módulos de expansão, o que simplifica o trabalho dos designers de jogos para liberar novos conteúdos.

Quarto nível. Você também pode incluir sua própria agregação nos componentes, o que permite selecionar os materiais e peças a partir dos quais esses componentes são montados. Essa abordagem dará ao jogador a oportunidade não apenas de encher os transformadores com os componentes necessários, mas também de produzir independentemente esses componentes a partir de várias partes. Francamente, nunca encontrei esse nível de abstração nos jogos, e não sem razão! Afinal, isso é acompanhado por uma complicação significativa da arquitetura, e o ajuste do equilíbrio nesses jogos se transforma em um inferno. Mas não excluo a existência de tais jogos.



Como você pode ver, cada camada descrita, em princípio, tem direito à vida. Tudo depende do tipo de flexibilidade que queremos colocar no projeto. Se os termos de referência não dizem nada sobre isso, ou o próprio autor do projeto não sabe o que uma empresa pode exigir, você pode olhar para projetos semelhantes nessa área e focar neles.

Padrões de design




Décadas de desenvolvimento levaram à formação de uma lista das soluções arquitetônicas mais usadas, que ao longo do tempo foram classificadas pela comunidade e são chamadas de padrões de design . É por isso que, quando li pela primeira vez sobre padrões, fiquei surpreso ao descobrir que já utilizava muitos deles na prática, simplesmente não sabia que essas soluções tinham um nome.

Os padrões de design, como a abstração, são características não apenas do desenvolvimento da OOP, mas também de outros paradigmas. Em geral, o tópico dos padrões está além do escopo deste artigo, mas eu gostaria de alertar um jovem desenvolvedor que apenas pretende se familiarizar com os padrões. Isso é uma armadilha! Agora vou explicar o porquê.

O objetivo dos padrões é ajudar a resolver problemas de arquitetura que já foram descobertos ou com maior probabilidade de serem descobertos durante o desenvolvimento do projeto. Assim, depois de ler sobre padrões, um iniciante pode ter uma tentação irresistível de usar padrões não para resolver problemas, mas para gerá-los. E como o desenvolvedor é desenfreado em seus desejos, ele pode não começar a resolver o problema com a ajuda de padrões, mas pode ajustar quaisquer tarefas às soluções com a ajuda de padrões.

Outro valor dos padrões é a formalização da terminologia. É muito mais fácil para um colega dizer que uma "cadeia de tarefas" é usada neste local do que desenhar o comportamento e as relações dos objetos em um pedaço de papel por meia hora.

Conclusão


Em condições modernas, a presença da palavra classe no seu código não faz de você um programador de POO. Pois, se você não usar os mecanismos descritos no artigo (polimorfismo, composição, herança etc.) e, em vez disso, usar classes apenas para agrupar funções e dados, então isso não é OPO. O mesmo pode ser resolvido por alguns namespaces e estruturas de dados. Não confunda, caso contrário você terá vergonha na entrevista.

Eu quero terminar minha música com palavras importantes. Quaisquer mecanismos, princípios e padrões descritos, bem como a POO como um todo, não devem ser aplicados onde não faz sentido ou podem ser prejudiciais. Isso leva a artigos com títulos estranhos como "A herança é a causa do envelhecimento prematuro" ou "Singleton pode levar ao câncer".

Estou falando serio.Se considerarmos o caso de singleton, seu amplo uso sem o conhecimento do caso causou sérios problemas de arquitetura em muitos projetos. E os amantes de martelar as unhas com um microscópio gentilmente o chamavam de antipadrão. Seja prudente.

Infelizmente, no design, não há receitas inequívocas para todas as ocasiões, onde é apropriado e onde é inapropriado. Isso gradualmente se encaixará na sua cabeça com a experiência.

Source: https://habr.com/ru/post/pt463125/


All Articles