"Eu amo westerns de espaguete, odeio código de espaguete"O "código do espaguete" é uma expressão ideal para descrever software que é um caos fumegante do ponto de vista cognitivo e estético. Neste artigo, falarei sobre um plano de três pontos para destruir um código de espaguete:
- Discutimos por que o código do espaguete não é tão saboroso.
- Apresentando uma nova visão do que o código realmente faz.
- Estamos discutindo a FMN (Frame Machine Notation) , que ajuda os desenvolvedores a desvendar uma bola de cola.
Todos sabemos o quão difícil é ler o código de outra pessoa. Isso pode ser devido ao fato de que a tarefa em si é difícil ou porque a estrutura do código é muito ... "criativa". Muitas vezes, esses dois problemas andam de mãos dadas.
Desafios são tarefas difíceis, e geralmente nada além de uma descoberta revolucionária pode simplificá-los. No entanto, acontece que a própria estrutura de software adiciona complexidade desnecessária, e
vale a pena solucionar esse problema.
A feiúra do código do espaguete está em sua lógica condicional complexa. E embora a vida possa ser difícil de imaginar sem as muitas construções complicadas de "se-então-outro", este artigo mostrará uma solução melhor.
Para ilustrar a situação com o código espaguete, precisamos primeiro transformar isso:
Massa crocanteNeste:
Al dente!Vamos começar a cozinhar.
Estado implícito
Para fazer macarrão, definitivamente precisamos de água para cozinhar. No entanto, mesmo um elemento aparentemente simples envolvendo código de espaguete pode ser muito confuso.
Aqui está um exemplo simples:
(temp < 32)
O que essa verificação realmente faz? Obviamente, ele divide a linha numérica em duas partes, mas o
que essas partes
significam ? Eu acho que você pode fazer uma suposição lógica, mas o problema é que o código não comunica isso
explicitamente .
Se eu realmente confirmar que ela verifica se a água é SÓLIDA
[aprox. pista: de acordo com a escala de Fahrenheit, a água congela a +32 graus] , o que logicamente significa o retorno falso?
if (temp < 32) {
Embora o cheque tenha dividido os números em dois grupos, na verdade existem três estados lógicos - sólido, líquido e gás (SÓLIDO, LÍQUIDO, GÁS)!
Ou seja, esta linha numérica:
dividir por verificação de condição da seguinte maneira:
if (temp < 32) {
} else {
}
Observe o que aconteceu porque é muito importante para entender a natureza do código do espaguete. Uma verificação booleana dividiu o espaço numérico em duas partes, mas NÃO categorizou o sistema como uma estrutura lógica real de (SOLID, LIQUID, GAS). Em vez disso, a verificação dividiu o espaço em (SOLID, tudo o resto).
Aqui está uma verificação semelhante:
if (temp > 212) {
Visualmente, ficará assim:
if (temp > 212) {
} else {
}
Note que:
- o conjunto completo de estados possíveis não é anunciado em nenhum lugar
- em nenhum lugar nas construções condicionais são estados lógicos verificáveis ou grupos de estados declarados
- alguns estados são indiretamente agrupados pela estrutura da lógica condicional e ramificação
Esse código é frágil, mas muito comum e não é tão grande que cause problemas com seu suporte. Então, vamos piorar a situação.
Eu nunca gostei do seu código de qualquer maneiraO código mostrado acima implica a existência de três estados da matéria - SÓLIDO, LÍQUIDO, GÁS. No entanto, de acordo com dados científicos, de fato, existem
quatro estados observáveis nos quais o plasma (PLASMA) está incluído (de fato, existem muitos outros, mas isso será suficiente para nós). Embora ninguém esteja preparando uma pasta a partir do plasma, se esse código for publicado no Github, e algum aluno de pós-graduação em física de alta energia forçá-lo, teremos que manter esse estado também.
No entanto, quando o plasma é adicionado, o código mostrado acima fará ingenuamente o seguinte:
if (temp < 32) {
É provável que o código antigo, quando adicionado a muitos estados do plasma, seja interrompido nos outros ramos. Infelizmente, nada na estrutura de código ajuda a relatar a existência de um novo estado ou a influenciar mudanças. Além disso, é provável que quaisquer erros sejam imperceptíveis, ou seja, encontrá-los será o mais difícil. Apenas diga não aos insetos no espaguete.
Em resumo, o problema é este: as verificações booleanas são usadas para determinar os estados
indiretamente . Os estados lógicos geralmente não são declarados e não são visíveis no código. Como vimos acima, quando o sistema adiciona novos estados lógicos, o código existente pode quebrar. Para evitar isso, os
desenvolvedores devem reexaminar cada verificação e ramificação condicional individual para garantir que os caminhos de código ainda sejam válidos para
todos os seus estados lógicos! Esse é o principal motivo da degradação de grandes fragmentos de código à medida que se tornam mais complexos.
Embora não haja maneiras de se livrar completamente das verificações de dados condicionais, qualquer técnica que as minimize reduz a complexidade do código.
Vamos agora dar uma olhada em uma implementação típica orientada a objetos de uma classe que cria um modelo
muito simples do volume de água. A turma gerenciará mudanças no estado da substância da água. Depois de estudar os problemas da solução clássica para esse problema, discutimos uma nova notação chamada
Frame e mostramos como ela pode lidar com as dificuldades que descobrimos.
Primeiro, deixe a água ferver ...
A ciência deu nomes a todas as transições possíveis que uma substância pode fazer quando a temperatura muda.
Nossa classe é muito simples (e não é particularmente útil). Ele responde aos desafios de realizar transições entre estados e altera a temperatura até que se torne adequado ao estado de destino desejado:
(Nota: escrevi esse pseudocódigo. Use-o no seu trabalho apenas por sua própria conta e risco.)
class WaterSample { temp:int Water(temp:int) { this.temp = temp }
Comparado ao primeiro exemplo, esse código possui certas melhorias. Primeiro, os números "mágicos" codificados (32, 212) são substituídos pelas constantes dos limites de temperatura do estado (WATER_SOLID_TEMP, WATER_GAS_TEMP). Essa mudança começa a tornar os estados mais explícitos, embora indiretamente.
As verificações de “programação defensiva” também aparecem neste código, que restringem a chamada do método se estiver em um estado inadequado para a operação. Por exemplo, a água não pode congelar se não for um líquido - isso viola a lei (da natureza). Mas a adição de condições de vigilância complica o entendimento do objetivo do código. Por exemplo:
Essa verificação condicional faz o seguinte:
- Verifica se a
temp
inferior à temperatura limite do GAS - Verifica se a
temp
excede a temperatura limite do SOLID - Retorna um erro se uma dessas verificações não for verdadeira
Essa lógica é confusa. Em primeiro lugar, o estado líquido é determinado pelo que a substância
não é - um sólido ou gás.
(temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP)
Em segundo lugar, o código verifica se a água é líquida para descobrir se é necessário retornar um erro.
!(temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP)
A primeira vez que se entende essa dupla negação de estados não é fácil. Aqui está uma simplificação que reduz um pouco a complexidade da expressão:
bool isLiquidWater = (temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP) if (!isLiquidWater) throw new IllegalStateError()
Esse código é mais fácil de entender porque o estado
isLiquidWater é
explícito .
Agora estamos explorando técnicas que corrigem um
estado explícito como a melhor maneira de resolver problemas. Com essa abordagem, os estados lógicos do sistema tornam-se a estrutura física do software, o que melhora o código e simplifica seu entendimento.
Notação de máquina de estrutura
A FMN (Frame Machine Notation) é uma linguagem específica de domínio (Domain Specific Language, DSL) que define uma abordagem categórica, metodológica e simples para definir e implementar vários tipos de
máquinas . Por simplicidade, chamarei os autômatos de quadros simplesmente de “máquinas”, porque essa notação pode definir critérios teóricos para quaisquer tipos diferentes (máquinas de estado, autômatos de loja e a evolução principal dos autômatos - máquinas de Turing). Para conhecer os diferentes tipos de máquinas e suas aplicações, recomendo estudar a página na
Wikipedia .
Embora a teoria dos autômatos possa ser interessante (uma afirmação MUITO duvidosa), neste artigo, focaremos na aplicação prática desses poderosos conceitos para construir sistemas e escrever código.
Para resolver esse problema, o Frame apresenta uma notação padronizada que funciona em três níveis integrados:
- DSL de texto para definir controladores de quadro com sintaxe elegante e concisa
- Um conjunto de padrões de codificação de referência para implementar classes orientadas a objetos na forma de máquinas que o Frame chama de "controladores"
- Notação visual em que o FMN é usado para expressar operações complexas difíceis de representar graficamente - Notação Visual de Quadro (FVN)
Neste artigo, considerarei os dois primeiros pontos: FMN e padrões de referência e deixarei a discussão da FVN para artigos futuros.
Frame é uma notação que possui vários aspectos importantes:
- O FMN possui objetos de primeiro nível relacionados ao conceito de autômatos, que não estão disponíveis em linguagens orientadas a objetos.
- A especificação FMN define padrões de implementação padrão em pseudo-código que demonstram como a notação FMN pode ser implementada.
- Em breve, o FMN poderá compilar (trabalho em andamento) em qualquer linguagem orientada a objetos
Nota: a implementação de referência é usada para demonstrar a equivalência absoluta da notação FMN e uma maneira simples de implementá-la em qualquer linguagem orientada a objetos. Você pode escolher qualquer método.
Agora, apresentarei os dois objetos de primeiro nível mais importantes em Frame -
Frame Events e
Frame Controllers .
Eventos de quadro
Os FrameEvents são parte integrante da simplicidade da notação FMN. Um FrameEvent é implementado como uma estrutura ou classe que possui pelo menos as seguintes variáveis de membro:
- ID da mensagem
- dicionário ou lista de parâmetros
- retornar objeto
Aqui está o pseudocódigo da classe FrameEvent:
class FrameEvent { var _msg:String var _params:Object var _return:Object FrameEvent(msg:String, params:Object = null) { _msg = msg _params = params } }
A notação de quadro usa o símbolo
@ , que identifica o objeto FrameEvent. Cada um dos atributos FrameEvent necessários possui um token especial para acessá-lo:
@|message| : - _msg @[param1] : [] @^ : _return
Frequentemente, não precisamos especificar com o que o FrameEvent funciona. Como a maioria dos contextos trabalha com apenas um FrameEvent de cada vez, a notação pode definitivamente ser simplificada, de forma que use apenas seletores de atributos. Portanto, podemos simplificar o acesso:
|buttonClick|
Essa notação pode parecer estranha a princípio, mas em breve veremos como uma sintaxe tão simples para eventos simplifica bastante o entendimento do código FMN.
Controladores de quadro
Um Frame Controller é uma classe orientada a objetos, ordenada de uma maneira bem definida para implementar uma máquina Frame. Os tipos de controlador são identificados pelo prefixo
# :
#MyController
isso é equivalente ao seguinte pseudocódigo orientado a objeto:
class MyController {}
Obviamente, essa classe não é particularmente útil. Para que ele possa fazer algo, o controlador precisa de pelo menos um estado para responder aos eventos.
Os controladores são estruturados de forma a conter blocos de vários tipos, identificados por um traço ao redor do nome do tipo de bloco:
#MyController<br> -block 1- -block 2- -block 3-
Um controlador não pode ter mais de uma instância de cada bloco e os tipos de bloco podem conter apenas certos tipos de subcomponentes. Neste artigo, examinamos apenas o bloco
-máquina- , que pode conter apenas estados. Os estados são identificados pelo token de prefixo
$ .
Aqui vemos o FMN para um controlador contendo uma máquina com apenas um estado:
#MyController
Aqui está a implementação do código FMN acima:
class MyController {
A implementação do bloco da máquina consiste nos seguintes elementos:
- variável _state , que se refere a uma função do estado atual. É inicializado com a primeira função de estado no controlador.
- um ou mais métodos de estado
O método Frame state é definido como uma função com a seguinte assinatura:
func MyState(e:FrameEvent);
Depois de definir esses fundamentos da implementação do bloco da máquina, podemos ver como o objeto FrameEvent interage com a máquina.
Unidade de interface
A interação dos FrameEvents que controlam a operação da máquina é a própria essência da simplicidade e poder da notação de quadros. No entanto, ainda não respondemos à pergunta, de onde vêm os FrameEvents - como eles entram no controlador para controlá-lo? Uma opção: os próprios clientes externos podem criar e inicializar FrameEvents e, em seguida, chamar diretamente o método apontado pela variável de membro _state:
myController._state(new FrameEvent("buttonClick"))
Uma alternativa muito melhor seria criar uma interface comum que envolva uma chamada direta para a variável de membro _state:
myController.sendEvent(new FrameEvent("buttonClick"))
No entanto, a maneira mais simples e sem complicações, correspondente à maneira usual de criar software orientado a objetos, é criar métodos comuns que enviam um evento em nome do cliente para a máquina interna:
class MyController { func buttonClick() { FrameEvent e = new FrameEvent("buttonClick") _state(e) return e._return } }
O quadro define a sintaxe para
um bloco de interface que contém métodos que transformam chamadas em uma interface comum para FrameEvents.
#MyController -interface- buttonClick ...
O bloco de
interface
possui muitos outros recursos, mas este exemplo nos dá uma idéia geral de como isso funciona. Darei mais explicações nos seguintes artigos da série.
Agora vamos continuar estudando a operação do autômato Frame.
Manipuladores de eventos
Embora tenhamos mostrado como definir um carro, ainda não temos uma notação com a qual
fazer alguma coisa. Para processar eventos, precisamos 1) poder selecionar o evento que precisa ser processado e 2) anexá-lo ao comportamento que está sendo executado.
Aqui está um controlador de quadro simples que fornece a infraestrutura para manipular eventos:
#MyController
Como mencionado acima, para acessar o atributo
_msg
evento
_msg
, a notação FMN usa colchetes de linhas verticais:
|messageName|
O FMN também usa um token de expoente que representa a instrução de retorno. O controlador mostrado acima será implementado da seguinte maneira:
class MyController {
Aqui, vemos com que clareza a notação FMN corresponde a um padrão de implementação fácil de entender e codificar.
Depois de definir esses aspectos básicos de eventos, controladores, máquinas, estados e manipuladores de eventos, podemos resolver problemas reais com a ajuda deles.
Máquinas de foco único
Acima, vimos um controlador sem estado que era bastante inútil.
#MyController
Um passo mais alto na cadeia de utilidade alimentar é uma classe com um único estado que, embora não seja inútil, é simplesmente chato. Mas pelo menos ele está fazendo
alguma coisa .
Primeiro, vamos ver como uma classe com apenas um estado (implícito) será implementada:
class Mono { String status() { return "OFF" } }
Nenhum estado é declarado ou mesmo implícito aqui, mas vamos assumir que, se o código fizer alguma coisa, o sistema estará no estado "Trabalhando".
Também apresentaremos uma idéia importante: as chamadas de interface serão consideradas semelhantes ao envio de um evento para um objeto. Portanto, o código acima pode ser considerado como um método de transmissão do | status | a classe Mono, sempre no estado $ Working.
Essa situação pode ser visualizada usando a tabela de ligação de eventos:
Agora vamos dar uma olhada no FMN, que demonstra a mesma funcionalidade e corresponde à mesma tabela de ligação:
#Mono -machine- $Working |status| ^("OFF")
Aqui está a aparência da implementação:
class Mono {
Você pode perceber que também introduzimos uma nova notação para a
declaração de retorno , o que significa avaliar a expressão e retornar o resultado à interface:
^(return_expr)
Este operador é equivalente
@^ = return_expr
ou apenas
^ = return_expr
Todos esses operadores são funcionalmente equivalentes e você pode usá-los, mas
^(return_expr)
parece o mais expressivo.
Ligue o fogão
Até agora, vimos um controlador com 0 estados e um controlador com 1 estado. Eles ainda não são muito úteis, mas já estamos à beira de algo interessante.
Para cozinhar nossas massas, primeiro você precisa ligar o fogão. A seguir, é apresentada uma classe Switch simples com uma única variável booleana:
class Switch { boolean _isOn; func status() { if (_isOn) { return "ON"; } else { return "OFF"; } } }
Embora, à primeira vista, isso não seja óbvio, o código mostrado acima implementa a seguinte tabela de ligações de eventos:
Para comparação, aqui está um FMN para o mesmo comportamento:
#Switch1 -machine- $Off |status| ^("OFF") $On |status| ^("ON")
Agora vemos como exatamente a notação Frame corresponde ao objetivo do nosso código - anexar um evento (chamada de método) ao comportamento com base no estado em que o controlador está localizado. Além disso, a estrutura de implementação também corresponde à tabela de ligação:
class Switch1 {
A tabela permite que você entenda rapidamente a finalidade do controlador em seus vários estados. A estrutura de notação de quadro e o padrão de implementação têm vantagens semelhantes.
No entanto, nosso switch tem um problema funcional perceptível. É inicializado no estado $ Off, mas não pode alternar para o estado $ On! Para fazer isso, precisamos inserir um operador de
mudança de estado .
Alterar estado
A declaração de mudança de estado é a seguinte:
->> $NewState
Agora podemos usar esse operador para alternar entre $ Off e $ On:
#Switch2 -machine- $Off |toggle| ->> $On ^ |status| ^("OFF") $On |toggle| ->> $Off ^ |status| ^("ON")
E aqui está a tabela de ligação de evento correspondente:
Novo evento | alternar | agora desencadeia uma mudança que simplesmente percorre os dois estados. Como uma operação de mudança de estado pode ser implementada?
Nenhum lugar é mais fácil. Aqui está a implementação do Switch2:
class Switch2 {
Você também pode fazer o último aprimoramento no Switch2, para que ele não apenas permita alternar entre estados, mas também defina explicitamente o estado:
#Switch3 -machine- $Off |turnOn| ->> $On ^ |toggle| ->> $On ^ |status| ^("OFF") $On |turnOff| ->> $Off ^ |toggle| ->> $Off ^ |status| ^("ON")
Diferente do evento | toggle |, se | turnOn | transmitida quando o Switch3 já está ligado ou | desligar | quando já está desligado, a mensagem é ignorada e nada acontece.
Essa pequena melhoria oferece ao cliente a capacidade de indicar explicitamente o estado em que o comutador deve estar: class Switch3 {
O passo final na evolução do nosso switch mostra como é fácil entender o objetivo do controlador FMN. O código relevante demonstra como é fácil implementar usando os mecanismos Frame.Depois de criar a máquina Switch, podemos acender o fogo e começar a cozinhar!Estado do som
Um aspecto chave, embora sutil, dos autômatos é que o estado atual da máquina é o resultado de uma situação (por exemplo, ligar) ou de algum tipo de análise de dados ou do ambiente. Quando a máquina mudou para o estado desejado, ela está implícita. que a situação não vai mudar sem o conhecimento do carro.No entanto, essa suposição nem sempre é verdadeira. Em algumas situações, a verificação (ou "detecção") dos dados é necessária para determinar o estado lógico atual:- estado restaurado inicial - quando a máquina é restaurada de um estado constante
- estado externo - define a “situação real” existente no ambiente no momento da criação, restauração ou operação da máquina
- estado interno volátil - quando parte dos dados internos gerenciados por uma máquina em execução pode mudar fora do controle da máquina
Em todos esses casos, dados, ambiente ou ambos devem ser "analisados" para determinar a situação e definir o estado da máquina de acordo. Idealmente, essa lógica booleana pode ser implementada em uma única função que define o estado lógico correto. Para suportar esse padrão, a notação de quadro tem um tipo especial de função que investiga o universo e determina a situação no momento atual. Tais funções são indicadas pelo prefixo $ antes do nome do método que retorna um link para o estado : $probeForState()
Em nossa situação, esse método pode ser implementado da seguinte maneira: func probeForState():FrameState { if (temp < 32) return Solid if (temp < 212) return Liquid return Gas }
Como podemos ver, o método simplesmente retorna uma referência à função state correspondente ao estado lógico correto. Essa função de detecção pode ser usada para entrar no estado correto: ->> $probeForState()
O mecanismo de implementação fica assim: _state = probeForState()
O método de detecção de estado é um exemplo de notação de quadro para gerenciar estado de uma determinada maneira. Em seguida, também aprenderemos a notação importante para gerenciar os FrameEvents.Herança comportamental e despachante
A herança comportamental e o dispatcher são um poderoso paradigma de programação e o último tópico sobre a notação de quadros neste artigo.O uso de quadros usa herança de comportamento , não herança de dados ou outros atributos. Para esse estado, os FrameEvents são enviados para outros estados se o estado inicial não manipular o evento (ou, como veremos nos próximos artigos, apenas desejar transmiti-lo). Essa cadeia de eventos pode atingir qualquer profundidade desejada.Para isso, as máquinas podem ser implementadas usando uma técnica chamada encadeamento de métodos . A notação FMN para enviar eventos de um estado para outro é o expedidor => : $S1 => $S2
Esta declaração FMN pode ser implementada da seguinte maneira: func S1(e:FrameEvent) { S2(e)
Agora vemos como é fácil encadear métodos de estado. Vamos aplicar esta técnica a uma situação bastante difícil: #Movement -machine- $Walking => $Moving |getSpeed| ^(3) |isStanding| ^(true) $Running => $Moving |getSpeed| ^(6) |isStanding| ^(true) $Crawling => $Moving |getSpeed| ^(.5) |isStanding| ^(false) $AtAttention => $Motionless |isStanding| ^(true) $LyingDown => $Motionless |isStanding| ^(false) $Moving |isMoving| ^(true) $Motionless |getSpeed| ^(0) |isMoving| ^(false)
No código acima, vemos que existem dois estados básicos - $ Moving e $ Motionless - e os outros cinco estados herdam uma funcionalidade importante deles. A ligação de evento mostra claramente como serão as ligações em geral:Graças às técnicas que aprendemos, a implementação será muito simples: class Movement {
Máquina de água
Agora, temos os conhecimentos básicos sobre FMN, permitindo entender como reimplementar a classe WaterSample com estados e de uma maneira muito mais inteligente. Também o tornaremos útil para o físico de nossos alunos de pós-graduação e adicionaremos um novo estado $ Plasma:Veja como é a implementação completa da FMN: #WaterSample -machine- $Begin |create|
Como você pode ver, temos o estado inicial de $ Begin, que responde à mensagem | create | e retém valor temp
. A função de detecção verifica primeiro o valor inicial temp
para determinar o estado lógico e, em seguida, executa a transição da máquina para esse estado.Todos os estados físicos ($ Sólido, $ Líquido, $ Gás, $ Plasma) herdam o comportamento de proteção do estado $ Padrão. Todos os eventos que não são válidos para o estado atual são passados para o estado $ Default, que gera um erro InvalidStateError. Isso mostra como a simples programação defensiva pode ser implementada usando a herança de comportamento.E agora a implementação: class WaterSample {
Conclusão
Autômatos é um conceito básico de ciência da computação que tem sido usado por muito tempo apenas em áreas especializadas de desenvolvimento de software e hardware. A principal tarefa do Frame é criar uma notação para descrever autômatos e definir padrões simples para escrever código ou "mecanismos" para sua implementação. Espero que a notação de quadro mude a maneira como os programadores veem as máquinas, fornecendo uma maneira fácil de colocá-las em prática nas tarefas diárias de programação e, é claro, salvá-las de espaguete no código.O Terminator come macarrão (foto de Suzuki san)Em artigos futuros, com base nos conceitos que aprendemos, criaremos ainda mais poder e expressividade da notação FMN. Com o tempo, expandirei a discussão para um estudo de modelagem visual, que inclui FMN e resolve os problemas de comportamento incerto nas abordagens modernas da modelagem de software.