Suponha que você tenha uma árvore de análise, uma árvore de sintaxe abstrata e um gráfico de fluxo de controle, cada um deles logicamente derivado do anterior. Em princípio, é fácil construir cada gráfico, dada a árvore de análise, mas como podemos gerenciar a complexidade de atualizar os gráficos quando a árvore de análise é modificada? Sabemos exatamente como a árvore foi modificada, mas como a mudança pode ser propagada para as outras árvores de uma maneira que não se torna difícil de gerenciar?
Naturalmente, o gráfico dependente pode ser atualizado simplesmente reconstruindo-o do zero toda vez que o primeiro gráfico for alterado, mas não haveria como saber os detalhes das alterações no gráfico dependente.
Atualmente, tenho quatro maneiras de tentar resolver esse problema, mas cada uma delas tem dificuldades.
- Cada um dos nós da árvore dependente observa os nós relevantes da árvore original, atualizando a si mesmos e as listas de observadores dos nós da árvore original, conforme necessário. A complexidade conceitual disso pode se tornar assustadora.
- Cada nó da árvore original possui uma lista dos nós da árvore dependente que dependem especificamente dela e, quando o nó é alterado, define um sinalizador nos nós dependentes para marcá-los como sujos, incluindo os pais dos nós dependentes até o fim para a raiz. Após cada alteração, executamos um algoritmo que é muito parecido com o algoritmo para construir o gráfico dependente do zero, mas pula qualquer nó limpo e reconstrói cada nó sujo, mantendo o controle de se o nó reconstruído é realmente diferente do nó sujo. Isso também pode ser complicado.
- Podemos representar a conexão lógica entre o gráfico original e o gráfico dependente como uma estrutura de dados, como uma lista de restrições, talvez projetada usando uma linguagem declarativa. Quando o gráfico original muda, precisamos apenas varrer a lista para descobrir quais restrições são violadas e como a árvore dependente precisa ser alterada para corrigir a violação, todas codificadas como dados.
- Podemos reconstruir o gráfico dependente do zero como se não houvesse um gráfico dependente existente e, em seguida, comparar o gráfico existente e o novo gráfico para descobrir como ele foi alterado. Tenho certeza de que essa é a maneira mais fácil, porque sei que existem algoritmos disponíveis para detectar diferenças, mas todos são bastante computacionais e, em princípio, parece desnecessário, por isso evito deliberadamente essa opção.
Qual é a maneira correta de lidar com esse tipo de problema? Certamente deve haver um padrão de design que torne tudo isso quase fácil. Seria bom ter uma boa solução para todos os problemas desta descrição geral. Essa classe de problemas tem um nome?
Deixe-me elaborar os problemas que esse problema causa. Esse problema aparece em vários lugares, sempre que duas partes de um projeto operam em gráficos, com cada gráfico sendo uma representação diferente da mesma coisa que muda enquanto o software está em execução. É como fazer um adaptador para uma interface, mas em vez de agrupar um único objeto ou um número fixo de objetos, precisamos agrupar um gráfico inteiro de tamanho arbitrário.
Toda vez que tento isso, acabo com uma confusão confusa e inatingível. Pode ser difícil acompanhar o fluxo de controle dos observadores quando se torna complicado, e o algoritmo para converter um gráfico em outro é geralmente bastante complicado de seguir quando é definido claramente e não se espalha por várias classes. O problema é que parece não haver maneira de usar apenas um algoritmo de conversão de gráfico simples e direto quando o gráfico original está sendo alterado.
Naturalmente, não podemos simplesmente usar um algoritmo comum de conversão de gráficos diretamente, porque isso não pode responder a mudanças de nenhuma outra maneira que não seja começar do zero, então quais são as alternativas? Talvez o algoritmo possa ser escrito em um estilo de passagem contínua, onde cada etapa do algoritmo é representada como um objeto com um método para cada tipo de nó no gráfico original, como um visitante. Em seguida, o algoritmo pode ser montado compondo vários visitantes simples juntos.
Outro exemplo: suponha que você tenha uma GUI projetada como no Java Swing, usando JPanels e gerenciadores de layout. Você pode simplificar esse processo usando JPanels aninhados no lugar de gerenciadores de layout complexos, para terminar com uma árvore de vários contêineres que inclui nós que existem apenas para fins de layout e, de outra forma, não têm sentido. Agora, suponha que a mesma árvore usada para gerar sua GUI também seja usada em outra parte do seu aplicativo, mas em vez de distribuir a árvore graficamente, ela está trabalhando com uma biblioteca que gerará uma árvore de representação abstrata como um sistema de pastas. Para usar esta biblioteca, precisamos ter uma versão da árvore que não tenha os nós de layout; os nós de layout precisam ser achatados em seus nós pais,
Outra maneira de analisar: o próprio conceito de trabalhar com árvores mutáveis viola a Lei de Deméter . Realmente não seria uma violação da lei se a árvore tivesse um valor como as árvores de análise e as árvores de sintaxe normalmente são, mas nesse caso não haveria nenhum problema, pois nada precisaria ser atualizado. Portanto, esse problema existe como resultado direto da violação da Lei de Demeter, mas como você evita isso em geral quando seu domínio parece ser sobre a manipulação de árvores ou gráficos?
O padrão Composite é uma ferramenta maravilhosa para transformar um gráfico em um único objeto e obedecer à Lei de Demeter. É possível usar o padrão Composite para transformar efetivamente um tipo de árvore em outro? Você pode criar uma árvore de análise composta para que ela funcione como uma árvore de sintaxe abstrata e até mesmo um gráfico de fluxo de controle? Existe uma maneira de fazer isso sem violar o princípio da responsabilidade única ? O padrão Composite tende a fazer com que as classes absorvam todas as responsabilidades que exercem, mas talvez possa ser combinado com o padrão Strategy de alguma forma.