Notação O em design de software

Ao lidar com o SOLID, sempre me deparei com o fato de que não seguir esses princípios pode levar a problemas. Os problemas são conhecidos, mas pouco formalizados. Este artigo foi escrito com o objetivo de formalizar situações típicas que surgem no processo de escrever código, possíveis soluções e as conseqüências decorrentes disso. Falaremos sobre por que enfrentamos códigos ruins e como os problemas crescem com o crescimento do programa. Infelizmente, na maioria dos casos, a avaliação se resume às opções “many” e “one”, que sugerem a insolvência da notação O, mas mesmo essa análise permitirá compreender melhor qual código é realmente perigoso para o desenvolvimento do sistema e qual é tolerante.

Definição de


Dizemos que uma mudança no programa requer “O” de ações f (n) se o programador não precisar fazer mais do que f (n) alterações logicamente separadas no programa para implementar a mudança precisa de um fator constante, em que n significa o tamanho do programa.

Métricas


Considere alguns dos recursos de design de Robert Martin e avalie-os em termos de notação O.
Um projeto é difícil se uma única alteração causar uma cascata de outras alterações nos módulos dependentes. Quanto mais módulos você precisar alterar, mais rígido será o design.
A diferença significativa é O (1) e O (n) mudanças. I.e. nosso design permite um número constante de alterações ou, à medida que o programa cresce, o número de alterações aumenta. Em seguida, devemos considerar as próprias mudanças - elas também podem se mostrar rígidas com algum comportamento assintótico. Assim, a rigidez pode ser complexa até O (nm). O parâmetro m será chamado de profundidade da rigidez. Mesmo uma estimativa aproximada da profundidade da rigidez em um projeto que até permita a rigidez O (n) é muito complicada para uma pessoa, pois cada uma das alterações deve ser verificada para alterações ainda mais profundas.
Fragilidade é propriedade de um programa que pode ser danificado em muitos lugares quando uma única alteração é feita. Freqüentemente, novos problemas surgem em partes que não têm conexão conceitual com a que foi alterada.
Não consideraremos a questão da conexão lógica dos módulos nos quais as alterações ocorrem. Portanto, do ponto de vista da Notação, não há diferença entre fragilidade e rigidez, e os argumentos válidos para rigidez se aplicam à fragilidade.
Um design é inerte se contiver peças que possam ser úteis em outros sistemas, mas os esforços e riscos associados à tentativa de separar essas peças do sistema original são muito grandes.
Os riscos e esforços nessa definição podem ser interpretados como o número de alterações que ocorrem no módulo ao tentar abstraí-lo do sistema original tão constante quanto o tamanho do módulo. No entanto, como mostra a prática, ainda vale a pena abstrair, pois isso traz ordem ao próprio módulo e permite que ele seja transferido para outros projetos. Muitas vezes, após a primeira necessidade de transferir o módulo para outro projeto, outros similares aparecem.

Viscosidade
Diante da necessidade de fazer uma alteração, o desenvolvedor geralmente encontra várias maneiras de fazer isso. Alguns preservam o design, outros não (ou seja, são essencialmente um "hack"). Se as abordagens de preservação do design são mais difíceis de implementar do que um hack, a viscosidade do design é alta. Resolver o problema é errado, fácil, mas o certo é difícil. Queremos projetar nossos programas para que seja fácil fazer alterações que preservem o design.
A viscosidade a seguir pode ser chamada de miopia em termos de notação O. Sim, a princípio o desenvolvedor realmente tem a oportunidade de fazer uma alteração em O (1) em vez de O (n) (devido à rigidez ou fragilidade), mas muitas vezes essas alterações levam a ainda mais rigidez e fragilidade, ou seja, aumentar a profundidade da rigidez. Se você ignorar tal "campainha", as alterações subsequentes podem não ser mais possíveis de resolver com um "hack" e você terá que fazer alterações nas condições de rigidez (talvez mais do que antes da "campainha") ou deixar o sistema em boas condições. Ou seja, quando a viscosidade é detectada, é melhor reescrever o sistema imediatamente.
Acontece assim: Ivan precisa escrever um código que enrole o pézinho. Subindo para diferentes partes do programa, onde, como ele suspeita, o bokryad foi fumado mais de uma vez, ele encontra um fragmento adequado. Ele copia, cola no módulo e faz as alterações necessárias.

Mas Ivan não sabe que o código que ele extraiu com o mouse colocou Peter lá, retirando-o do módulo escrito por Sveta. Sveta foi a primeira a fumar um pouco de meio-fio, mas sabia que esse processo era muito semelhante ao fumo de marmota. Ela encontrou em algum lugar um código que karmyachit karmaglot, copiou em seu módulo e modificou. Quando o mesmo código aparece em formas ligeiramente diferentes repetidas vezes, os desenvolvedores perdem a idéia de abstração.
Nessa situação, torna-se óbvio que, quando houver necessidade de alterar a base da ação escavada, essa alteração deve ser feita em n lugares. Dada a possibilidade de modificações exclusivas em cada pasta de cópia, isso também pode resultar em alterações logicamente não relacionadas. Nesse caso, há uma chance de simplesmente esquecer a mudança em outro local, pois não há proteção no estágio de compilação. Portanto, isso também pode se transformar em O (n) iterações de teste.

Aplicativo sobre a notação SOLID


SRP (princípio de responsabilidade única). Uma entidade de software deve ter apenas um motivo para a mudança (responsabilidade). Em outras palavras, por exemplo, a classe não deve seguir a lógica e o mapeamento de negócios, porque ao alterar uma responsabilidade, devemos garantir que não danificamos outra responsabilidade. Ou seja, a inconsistência com o princípio SRP resulta em rigidez e fragilidade. Seguir esse princípio também ajuda a eliminar a inércia e transferir os módulos de um programa para outro com um número potencialmente menor de dependências.

O comportamento assintótico das alterações permanece basicamente o mesmo que sem seguir o princípio, mas o fator constante diminui significativamente. Nos dois casos, devemos verificar todo o conteúdo da classe e, no caso de alterar a interface da entidade, os locais onde eles interagem com essa entidade. Somente o acompanhamento do SRP ajuda a reduzir a interface e a probabilidade de sua alteração, bem como a quantidade de implementação interna que, após a alteração, pode estar com defeito. Raciocínio semelhante, truncado para a discussão de interfaces, é válido para o ISP (Princípio de Segregação de Interface).

OCP (princípio de fechamento aberto). As entidades de software (classes, módulos, funções etc.) devem estar abertas para expansão e fechadas para modificação. Isso deve ser entendido de tal maneira que possamos modificar o comportamento do módulo sem afetar seu código-fonte. Normalmente, isso é alcançado por herança e polimorfismo. Como violar o princípio do LSP é uma violação do OCP e o DIP é um meio de manter o OCP, o seguinte pode ser aplicado ao LSP e ao DIP. A não conformidade com o princípio de abertura / proximidade força a realização de alterações em todas as entidades que não estão fechadas em relação a essa alteração.

Uma situação bastante trivial é, por exemplo, a presença de uma cadeia de ifs que determina o tipo de variável na lista de classes filho. Tais estruturas podem aparecer no programa mais de uma vez. E sempre que você adicionar uma nova classe filho, faça as alterações apropriadas em cada uma dessas cadeias. Situações semelhantes podem ocorrer não apenas com as classes filho, mas também com a consideração de nem todos os casos particulares possíveis [Isso se refere a casos não no momento da redação, mas em geral. Os casos podem aparecer mais tarde].

Agora considere a situação em que fazemos m mudanças do mesmo tipo que, devido à discrepância com o princípio da abertura-proximidade, exigem n operações de nós. Então, se deixarmos tudo como está, suportando a arquitetura para considerar casos especiais e não generalizar, obteremos a complexidade geral de todas as alterações O (mn). Se fecharmos todos os m lugares em relação a essa alteração, as alterações subsequentes receberão O (1) em vez de O (m). Assim, a complexidade geral é reduzida para O (m + n). Isso significa que iniciar um OCP nunca é tarde demais.

Martin diz sobre essa situação que você não deve adivinhar (se não sabe ao certo, é claro) como fechar desde a primeira alteração, mas após a primeira alteração vale a pena fechar, pois a primeira alteração foi um marcador de que o sistema não permanecerá necessariamente no estado atual. Isso é lógico, uma vez que realizamos O (1 * n) ações devido à falta de proximidade e, em seguida, O (m) ações para nos fechar das alterações subseqüentes. No total, obtemos a complexidade geral O (n + m), mas ao mesmo tempo executamos todas as ações exatamente quando elas se tornam necessárias e não fazemos nada com antecedência, sem saber se será necessário.

Padrões e Notação O


Mais uma analogia pode ser traçada entre a notação O na teoria computacional e a notação O no design. Consiste no fato de reduzirmos o número de cálculos com a ajuda de algoritmos e estruturas de dados, como árvores e montes de pesquisa, que resolvem problemas típicos mais rapidamente do que soluções "diretas" e o número de operações de um programador com um bom design, no qual ele também pode usar boas soluções típicas chamados padrões de design. Você pode avaliar o efeito dos padrões no contexto dos princípios do SOLID e, como resultado, no contexto da notação O.

Por exemplo, o modelo Mediador elimina a possibilidade de quebrar algo no programa ao alterar a lógica da interação entre dois objetos, pois o encapsula completamente e garante a constante complexidade de tal alteração.

O modelo do adaptador nos permite usar (ler e adicionar) entidades com diferentes interfaces, que usaremos para um propósito comum. Usando este modelo, você pode incorporar um novo objeto com uma interface incompatível no sistema para o número de operações que não depende do tamanho do sistema.

Como as estruturas de dados podem suportar algumas operações com bons assintóticos, e algumas com más, os padrões se comportam de maneira flexível em relação a algumas mudanças e rigidamente em relação a outras.

Limites razoáveis


Ao lidar com a notação O, trabalhando em um problema de otimização, devemos lembrar que nem sempre o algoritmo com os melhores assintóticos é mais adequado para resolver o problema. Deve-se entender que a classificação por uma bolha para uma matriz de 3 elementos funcionará mais rápido que a piramidal, apesar do fato de que a classificação piramidal tem melhores assintóticos. Para valores pequenos de n, um fator constante desempenha um papel importante, oculto pela notação O. A notação O no design funciona da mesma maneira. Para pequenos projetos, não faz sentido cercar muitos modelos, já que os custos de sua implementação excedem o número de alterações que devem ser feitas com “design inadequado”.

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


All Articles