Quebrando classe grande em classes menores quando elas precisam de um estado comum?

5

Estou escrevendo um analisador para uma linguagem bastante complicada em C ++. A classe Parser recebe uma lista de tokens e cria o AST. Embora apenas uma parte do analisador seja concluída, o arquivo Parser.cpp já tem mais de 1.5k linhas e a classe possui cerca de 25 funções. Portanto, planejo dividir a grande classe Parser em classes menores, de modo que eu possa ter classes separadas para analisar diferentes construções de linguagem.

Por exemplo, desejo ter a classe ExprParser que analisa expressões, uma classe TypeParser que analisa os tipos. Parece ser muito mais limpo. O problema é que as funções de análise devem ter acesso a um estado que inclua a posição do token atual e várias funções auxiliares de análise. Em C #, é possível implementar funções relacionadas em classes diferentes usando classes parciais. Existe algum padrão de design específico ou uma maneira recomendada para isso?

    
por Fish 18.05.2016 / 17:37
fonte

3 respostas

4

Crie uma classe Scanner ou Tokenizer, que recebe os dados de entrada (o texto a ser analisado) e mantém a posição do token atual ou estado semelhante. Também pode fornecer algumas funções auxiliares compartilhadas. Em seguida, forneça uma referência (ou um ponteiro compartilhado) ao objeto Scanner para todos os seus objetos xyzParser individuais, para que todos possam acessar o mesmo scanner. O "scanner" será responsável apenas por acessar os dados por funções básicas de tokenização, os analisadores individuais serão responsáveis pela lógica de análise real.

Isso funcionará mais facilmente, desde que seu scanner não precise saber quais analisadores individuais existem. Se o scanner realmente precisa saber disso, você pode considerar resolver a dependência cíclica introduzindo classes base abstratas de "interface", ou implementando algum tipo de callback ou mecanismo de evento, onde o scanner pode notificar qualquer tipo de observador.

    
por 18.05.2016 / 18:27
fonte
1

Padrão de design do estado, talvez? É basicamente uma herança direta, com a classe pai-abstrata contendo uma referência ao objeto "estado" atual, ou seja, analisador.

O padrão, juntamente com delegados, métodos de extensão, etc. deve dar bastante flexibilidade.

Desconfie de separar uma classe arbitrariamente. Essas classes menores também precisam de integridade OO. Não estou me referindo a classes parciais aqui.

Eu particularmente gosto deste vídeo de demonstração simples e limpo

    
por 18.05.2016 / 18:06
fonte
1

Muito provavelmente, implementar sua gramática como vários analisadores interdependentes só tornará seu código mais complicado. O fluxo de dados se tornará menos óbvio e você duplicará algum comportamento. Está tudo bem se uma turma é grande.

No entanto, muitos idiomas podem ser facilmente divididos em diferentes níveis, e lidar com eles separadamente pode ser sensato. Por exemplo:

  • você pode extrair a tokenização do analisador principal. C tem uma fase separada de tokenização e pré-processamento.
  • Você poderia fazer um pós-processamento em uma fase separada que cria o AST final. Isto é particularmente sensato se o seu analisador também verificar a semântica, por ex. resolver definições de símbolos ou fazer verificações de tipo. Essas devem ser separadas da análise.
  • Se o seu idioma tiver uma declaração strong - expressão dichtomy, você poderá ter analisadores separados para cada um, com o analisador de instrução chamando o analisador de expressões conforme necessário. Markdown é um exemplo de uma linguagem com uma gramática baseada em linhas (recuo) sobre uma gramática em nível de bloco (parágrafos, títulos, listas) sobre uma gramática inline (ênfase, links). Alguns analisadores usam uma abordagem de descendência recursiva simples para a sintaxe de nível de instrução, como construções de fluxo de controle ou definições de nível superior, mas alternam para um algoritmo LR para expressões para tratar apropriadamente precedência e associatividade.

Descobri que é bastante vantajoso extrair operações de análise de baixo nível em uma classe separada: manipular o buffer de entrada, verificar as verificações, extrair os tokens, manipular erros, é melhor feito por uma classe personalizada em vez de depender facilidades fornecidas pelo idioma (em particular, std::istream é inadequado para a maioria dos problemas). Se você estiver usando um algoritmo de análise diferente de Descida Recursiva, você também deve manipular essas operações em uma classe separada.

    
por 18.05.2016 / 18:34
fonte