Como facilitar a manutenção do código orientado a eventos?


16

Ao usar um componente baseado em eventos, muitas vezes sinto alguma dor na fase de manutenção.

Como o código executado está todo dividido, pode ser bastante difícil descobrir qual será a parte do código que estará envolvida no tempo de execução.

Isso pode levar a problemas sutis e difíceis de depurar quando alguém adiciona alguns novos manipuladores de eventos.

Editar a partir dos comentários: mesmo com algumas boas práticas a bordo, como ter um barramento de eventos para aplicativos e delegar negócios a outras partes do aplicativo, há um momento em que o código começa a ficar difícil de ler, porque há muitas manipuladores registrados de muitos lugares diferentes (especialmente quando existe um ônibus).

Em seguida, o diagrama de sequência começa a parecer complexo, o tempo gasto para descobrir o que está acontecendo está aumentando e a sessão de depuração se torna confusa (ponto de interrupção no gerenciador de manipuladores enquanto itera nos manipuladores, especialmente alegre com o manipulador assíncrono e com alguma filtragem no topo).

//////////////
Example

Eu tenho um serviço que está recuperando alguns dados no servidor. No cliente, temos um componente básico que está chamando este serviço usando um retorno de chamada. Para fornecer ponto de extensão aos usuários do componente e evitar o acoplamento entre componentes diferentes, estamos disparando alguns eventos: um antes da consulta ser enviada, um quando a resposta está voltando e outro em caso de falha. Temos um conjunto básico de manipuladores pré-registrados que fornecem o comportamento padrão do componente.

Agora, os usuários do componente (e também somos usuários do componente) podem adicionar alguns manipuladores para realizar alguma alteração no comportamento (modificar a consulta, logs, análise de dados, filtragem de dados, massagem de dados, animação sofisticada da interface do usuário, encadear várias consultas sequenciais , tanto faz). Portanto, alguns manipuladores devem ser executados antes / depois de outros e são registrados a partir de vários pontos de entrada diferentes no aplicativo.

Depois de um tempo, pode acontecer que uma dúzia ou mais de manipuladores estejam registrados, e trabalhar com isso pode ser entediante e perigoso.

Esse design surgiu porque o uso da herança estava começando a ser uma bagunça completa. O sistema de eventos é usado em um tipo de composição em que você ainda não sabe quais serão seus compostos.

Fim do exemplo
//////////////

Então, eu estou me perguntando como outras pessoas estão lidando com esse tipo de código. Tanto ao escrever quanto ao ler.

Você tem algum método ou ferramenta que permita escrever e manter esse código sem muita dificuldade?


Você quer dizer, além de refatorar a lógica dos manipuladores de eventos?
Telastyn

Documente o que está acontecendo.

@ Telastyn, não sei ao certo o que você quer dizer com 'além de refatorar a lógica dos manipuladores de eventos'.
Guillaume

@ Thorbjoern: veja minha atualização.
Guillaume

3
Parece que você não está usando a ferramenta correta para o trabalho? Quero dizer, se o pedido é importante, você não deve usar eventos simples para essas partes do aplicativo em primeiro lugar. Basicamente, se houver limitações na ordem dos eventos em um sistema de barramento típico, ele não é mais um sistema de barramento típico, mas você ainda o está usando como tal. O que significa que você precisa resolver esse problema adicionando uma lógica extra para lidar com o pedido. Pelo menos, se eu entender o seu problema corretamente ..
stijn

Respostas:


7

Descobri que o processamento de eventos usando uma pilha de eventos internos (mais especificamente, uma fila LIFO com remoção arbitrária) simplifica bastante a programação orientada a eventos. Permite dividir o processamento de um "evento externo" em vários "eventos internos" menores, com um estado bem definido no meio. Para mais informações, consulte minha resposta a esta pergunta .

Apresento aqui um exemplo simples que é resolvido por esse padrão.

Suponha que você esteja usando o objeto A para executar algum serviço e dê um retorno de chamada para informá-lo quando terminar. No entanto, A é tal que, depois de ligar para o retorno de chamada, talvez seja necessário mais trabalho. Um risco surge quando, dentro desse retorno de chamada, você decide que não precisa mais de A e o destrói de uma maneira ou de outra. Mas você está sendo chamado de A - se A, depois que seu retorno de chamada retornar, não puder descobrir com segurança que foi destruído, poderá ocorrer uma falha ao tentar executar o trabalho restante.

NOTA: É verdade que você poderia fazer a "destruição" de alguma outra maneira, como diminuir um refcount, mas isso apenas leva a estados intermediários, além de códigos e bugs extras ao lidar com eles; é melhor que A pare de funcionar completamente depois que você não precisar mais dele, além de continuar em algum estado intermediário.

No meu padrão, A simplesmente agendaria o trabalho adicional necessário empurrando um evento interno (trabalho) para a fila LIFO do loop de eventos, depois prosseguir para chamar o retorno de chamada e retornar ao loop de eventos imediatamente . Esse trecho de código não é mais um perigo, pois A apenas retorna. Agora, se o retorno de chamada não destruir A, o trabalho enviado será executado pelo loop de eventos para realizar seu trabalho extra (após a conclusão do retorno e todos os trabalhos enviados, recursivamente). Por outro lado, se o retorno de chamada destruir a função destruidora ou deinit de A, A poderá remover o trabalho enviado da pilha de eventos, impedindo implicitamente a execução do trabalho enviado.


7

Eu acho que o registro adequado pode ajudar bastante. Verifique se todos os eventos lançados / manipulados estão registrados em algum lugar (você pode usar estruturas de registro para isso). Quando você está depurando, pode consultar os logs para ver a ordem exata de execução do seu código quando o bug ocorreu. Muitas vezes, isso realmente ajuda a diminuir a causa do problema.


Sim, isso é um conselho útil. A quantidade de toros produzidos pode em seguida ser assustador (I podem lembrar de ter um tempo difícil para encontrar a causa de um ciclo de processamento no caso de uma forma muito complexa.)
Guillaume

Talvez uma solução seja registrar as informações interessantes em um arquivo de log separado. Além disso, você pode atribuir um UUID a cada evento, para poder acompanhar cada evento mais facilmente. Você também pode escrever alguma ferramenta de relatório que permita extrair informações específicas dos arquivos de log. (Ou, alternativamente, use opções no código para registrar informações diferentes em arquivos de log diferentes).
Giorgio

3

Então, eu estou me perguntando como outras pessoas estão lidando com esse tipo de código. Tanto ao escrever quanto ao ler.

O modelo de programação orientada a eventos simplifica até certo ponto a codificação. Provavelmente evoluiu como uma substituição das grandes instruções Select (ou maiúsculas) usadas em idiomas mais antigos e ganhou popularidade nos primeiros ambientes de desenvolvimento visual, como o VB 3 (não me cite no histórico, eu não verifiquei)!

O modelo se tornará problemático se a sequência de eventos for importante e quando 1 ação de negócios for dividida em vários eventos. Esse estilo de processo viola os benefícios dessa abordagem. A todo custo, tente tornar o código de ação encapsulado no evento correspondente e não crie eventos de dentro dos eventos. Isso então se torna muito pior do que o espaguete resultante do GoTo.

Às vezes, os desenvolvedores estão ansiosos para fornecer a funcionalidade da GUI que requer essa dependência de eventos, mas, na verdade, não há alternativa real que seja significativamente mais simples.

Bottom line aqui é que a técnica não é ruim se usada com sabedoria.


Existe algum projeto alternativo para evitar o 'pior que o espaguete'?
Guillaume

Não tenho certeza de que exista uma maneira fácil sem simplificar o comportamento esperado da GUI, mas se você listar todas as suas interações do usuário com uma janela / página e para cada determinar exatamente o que seu programa fará, poderá começar a agrupar o código em um único local e direcionar vários eventos para um grupo de subs / métodos responsáveis ​​pelo processamento real da solicitação. Também pode ajudar a separar o código que serve apenas a GUI do código que processa o back-end.
NoChance

A necessidade de postar essa mensagem veio de uma refatoração quando mudei de um inferno de herança para algo baseado em evento. Em algum ponto, é realmente melhor, mas em outro, é muito ruim ... Como eu já tive alguns problemas com 'eventos enlouquecidos' em alguma GUI, estou pensando no que pode ser feito para melhorar a manutenção de tais código fatiado do evento.
Guillaume

A programação orientada a eventos é muito mais antiga que o VB; estava presente na GUI do SunTools e, antes disso, me lembro que ele foi incorporado à linguagem Simula.
Kevin cline

@ Guillaume, eu acho que você está superando o serviço. Minha descrição acima foi de fato baseada principalmente em eventos da GUI. Você (realmente) precisa desse tipo de processamento?
NoChance

3

Eu queria atualizar esta resposta desde que obtive alguns momentos eureka desde depois dos fluxos de controle "achatamento" e "achatamento" e formulado alguns pensamentos novos sobre o assunto.

Efeitos colaterais complexos vs. fluxos de controle complexos

O que eu descobri é que meu cérebro pode tolerar efeitos colaterais complexos ou fluxos de controle semelhantes a gráficos, como você normalmente encontra na manipulação de eventos, mas não na combinação de ambos.

Posso facilmente raciocinar sobre código que causa 4 efeitos colaterais diferentes se eles estão sendo aplicados com um fluxo de controle muito simples, como o de um forloop seqüencial . Meu cérebro pode tolerar um loop seqüencial que redimensiona e reposiciona elementos, os anima, redesenha e atualiza algum tipo de status auxiliar. Isso é fácil de entender.

Da mesma forma, posso compreender um fluxo de controle complexo, como você pode obter com eventos em cascata ou percorrer uma estrutura de dados complexa, semelhante a um gráfico, se houver apenas um efeito colateral muito simples acontecendo no processo em que a ordem não importa nem um pouco, como elementos de marcação para ser processado de maneira diferida em um loop seqüencial simples.

Onde me perco, confuso e oprimido é quando você tem fluxos de controle complexos, causando efeitos colaterais complexos. Nesse caso, o fluxo de controle complexo dificulta a previsão antecipada de onde você vai acabar, enquanto os efeitos colaterais complexos dificultam a previsão exata do que acontecerá como resultado e em que ordem. Portanto, é a combinação dessas duas coisas que a torna tão desconfortável onde, mesmo que o código funcione perfeitamente bem no momento, é tão assustador alterá-lo sem medo de causar efeitos colaterais indesejados.

Fluxos de controle complexos tendem a dificultar o raciocínio sobre quando / onde as coisas vão acontecer. Isso só se torna realmente indutor de dor de cabeça se esses complexos fluxos de controle estiverem desencadeando uma combinação complexa de efeitos colaterais, onde é importante entender quando / onde as coisas acontecem, como efeitos colaterais que têm algum tipo de dependência de ordem, onde uma coisa deve acontecer antes da próxima.

Simplifique o fluxo de controle ou os efeitos colaterais

Então, o que você faz quando encontra o cenário acima, que é tão difícil de entender? A estratégia é simplificar o fluxo de controle ou os efeitos colaterais.

Uma estratégia amplamente aplicável para simplificar os efeitos colaterais é favorecer o processamento diferido. Usando um evento de redimensionamento da GUI como exemplo, a tentação normal pode ser reaplicar o layout da GUI, reposicionar e redimensionar os widgets filhos, acionando outra cascata de aplicativos de layout e redimensionando e reposicionando a hierarquia, além de repintar os controles, possivelmente acionando alguns eventos exclusivos para widgets com comportamento de redimensionamento personalizado que acionam mais eventos que levam a quem sabe onde etc. etc. Em vez de tentar fazer isso tudo de uma só vez ou enviar spam para a fila de eventos, uma solução possível é descer a hierarquia do widget e marque quais widgets precisam que seus layouts sejam atualizados. Em uma passagem adiada posterior, que possui um fluxo de controle seqüencial simples, aplique novamente todos os layouts para os widgets que precisam. Você pode marcar quais widgets precisam ser redesenhados. Novamente, em um passe diferido seqüencial com um fluxo de controle direto, repinte os widgets marcados como precisando ser redesenhados.

Isso tem o efeito de simplificar o fluxo de controle e os efeitos colaterais, uma vez que o fluxo de controle se torna simplificado, pois não ocorre eventos recursivos em cascata durante a passagem do gráfico. Em vez disso, as cascatas ocorrem no loop sequencial diferido que pode ser tratado em outro loop sequencial diferido. Os efeitos colaterais se tornam simples onde é importante, pois, durante os fluxos de controle mais complexos do tipo gráfico, tudo o que estamos fazendo é simplesmente marcar o que precisa ser processado pelos loops sequenciais adiados que acionam os efeitos colaterais mais complexos.

Isso vem com alguma sobrecarga de processamento, mas pode abrir portas para, digamos, fazer essas passagens diferidas em paralelo, potencialmente permitindo que você obtenha uma solução ainda mais eficiente do que a iniciada, se o desempenho for uma preocupação. Geralmente, o desempenho não deve ser muito preocupante na maioria dos casos. O mais importante é que, embora isso possa parecer uma grande diferença, achei muito mais fácil argumentar. Torna muito mais fácil prever o que está acontecendo e quando, e não posso superestimar o valor que pode ter para compreender mais facilmente o que está acontecendo.


2

O que funcionou para mim é tornar cada evento independente, sem referência a outros eventos. Se eles estão chegando de forma assíncrona, você não tem uma sequência, então tente descobrir o que acontece em que ordem é inútil, além de impossível.

Você termina com um monte de estruturas de dados que são lidas e modificadas, criadas e removidas por uma dúzia de threads em nenhuma ordem específica. Você precisa fazer uma programação multithread extremamente adequada, o que não é fácil. Você também precisa pensar em vários segmentos, como em "Com este evento, examinarei os dados que tenho em um determinado instante, sem considerar o que era um microssegundo antes, sem considerar o que acabou de mudar isso e sem levar em consideração o que os 100 threads que estão esperando que eu libere o bloqueio farão com ele. Depois, farei minhas alterações com base nisso e no que vejo. Depois, terminei. "

Uma coisa que me vejo fazendo é procurar uma coleção específica e garantir que a referência e a coleção em si (se não for segura para threads) estão bloqueadas corretamente e sincronizadas corretamente com outros dados. À medida que mais eventos são adicionados, essa tarefa aumenta. Mas se eu estivesse rastreando as relações entre os eventos, essa tarefa aumentaria muito mais rápido. Além disso, às vezes, grande parte do bloqueio pode ser isolado em seu próprio método, tornando o código mais simples.

Tratar cada thread como uma entidade completamente independente é difícil (por causa do multi-threading do núcleo duro), mas é possível. "Escalável" pode ser a palavra que estou procurando. O dobro de eventos exige apenas o dobro de trabalho e talvez apenas 1,5 vezes. Tentar coordenar eventos mais assíncronos o enterrará rapidamente.


Eu atualizei minha pergunta. Você está totalmente certo sobre coisas sequenciais e assíncronas. Como você lidaria com um caso em que o evento X deve ser executado após o processamento de todos os outros eventos? Parece que tenho que criar um gerenciador de manipuladores mais complexo, mas também quero evitar expor muita complexidade aos usuários.
Guillaume

No seu conjunto de dados, tenha uma opção e alguns campos definidos quando o evento X for lido. Quando todos os outros são processados, você verifica a opção e sabe que precisa lidar com o X e os dados. O switch e os dados devem ficar por conta própria, na verdade. Quando definido, você deve pensar: "Eu tenho que fazer esse trabalho", não "Eu tenho que entregar o X". Próximo problema: como você sabe que os eventos são realizados? e se você receber 2 ou mais eventos X? Na pior das hipóteses, você pode executar um encadeamento de manutenção em loop que verifica a situação e pode agir por sua própria iniciativa. ? (Sem entrada para 3 segundos conjunto interruptor X código de desligamento Em seguida, execute.
RalphChapin

2

Parece que você está procurando por máquinas estaduais e atividades orientadas a eventos .

No entanto, você também pode querer examinar Amostra de fluxo de trabalho de marcação de máquina de estado .

Aqui está uma breve visão geral da implementação da máquina de estado. Um fluxo de trabalho da máquina de estados consiste em estados. Cada estado é composto de um ou mais manipuladores de eventos. Cada manipulador de eventos deve conter um atraso ou um IEventActivity como a primeira atividade. Cada manipulador de eventos também pode conter uma atividade SetStateActivity usada para fazer a transição de um estado para outro.

Cada fluxo de trabalho da máquina de estado possui duas propriedades: InitialStateName e CompletedStateName. Quando uma instância do fluxo de trabalho da máquina de estado é criada, ela é colocada na propriedade InitialStateName. Quando a máquina de estado atinge a propriedade CompletedStateName, termina a execução.


2
Embora isso possa teoricamente responder à pergunta, seria preferível incluir aqui as partes essenciais da resposta e fornecer o link para referência.
Thomas Owens

Mas se você tiver dezenas de manipuladores anexados a cada estado, não ficará muito bem ao tentar entender o que está acontecendo ... E todo componente baseado em evento pode não ser descrito como uma máquina de estado.
Guillaume

1

Código orientado a eventos não é o problema real. Na verdade, não tenho nenhum problema em seguir a lógica, mesmo em código orientado, em que o retorno de chamada é explicitamente definido ou o retorno de chamada em linha é usado. Por exemplo , retornos de chamada no estilo gerador no Tornado são muito fáceis de seguir.

O que é realmente difícil de depurar são chamadas de função geradas dinamicamente. O padrão (anti?) Que eu chamaria de Fábrica de Retorno do Inferno. No entanto, esse tipo de fábrica de funções é igualmente difícil de depurar no fluxo tradicional.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.