Como você refatora com segurança em um idioma com escopo dinâmico?

13

Para aqueles que têm a sorte de não trabalhar em uma linguagem com escopo dinâmico, deixe-me fazer uma pequena revisão sobre como isso funciona. Imagine uma pseudo-linguagem, chamada "RUBELLA", que se comporta assim:

function foo() {
    print(x); // not defined locally => uses whatever value `x` has in the calling context
    y = "tetanus";
}
function bar() {
    x = "measles";
    foo();
    print(y); // not defined locally, but set by the call to `foo()`
}
bar(); // prints "measles" followed by "tetanus"

Ou seja, as variáveis ​​se propagam para cima e para baixo na pilha de chamadas livremente - todas as variáveis ​​definidas em foosão visíveis (e mutáveis ​​por) ao seu chamador bar, e o inverso também é verdadeiro. Isso tem sérias implicações para a refatorabilidade do código. Imagine que você tenha o seguinte código:

function a() { // defined in file A
    x = "qux";
    b();
}
function b() { // defined in file B
    c();
}
function c() { // defined in file C
    print(x);
}

Agora, as chamadas para a()serão impressas qux. Mas então, um dia, você decide que precisa mudar bum pouco. Você não conhece todos os contextos de chamada (alguns dos quais podem, de fato, estar fora da sua base de código), mas isso deve ser bom - suas alterações serão completamente internas b, certo? Então você reescreve assim:

function b() {
    x = "oops";
    c();
}

E você pode pensar que não mudou nada, pois acabou de definir uma variável local. Mas, de fato, você quebrou a! Agora, aimprime em oopsvez de qux.


Trazendo isso de volta para o mundo das pseudo-línguas, é exatamente assim que o MUMPS se comporta, embora com sintaxe diferente.

As versões modernas ("modernas") do MUMPS incluem a chamada NEWinstrução, que permite impedir que variáveis ​​vazem de um chamado para um chamador. Assim, no primeiro exemplo acima, se tivéssemos feito NEW y = "tetanus"em foo(), em seguida, print(y)em bar()imprimiria nada (em MUMPS, todos os nomes apontam para a cadeia vazia a menos que explicitamente definido para algo mais). Mas não há nada que possa impedir que variáveis ​​vazem de um chamador para um chamado: se tivermos function p() { NEW x = 3; q(); print(x); }, pelo que sabemos, q()poderia sofrer mutação x, apesar de não recebermos explicitamente xcomo parâmetro. Essa ainda é uma situação ruim, mas não tão ruim quanto costumava ser.

Com esses perigos em mente, como podemos refatorar com segurança o código no MUMPS ou em qualquer outro idioma com escopo dinâmico?

Existem algumas boas práticas óbvias para facilitar a refatoração, como nunca usar variáveis ​​em uma função diferente daquelas que você inicializa ( NEW) ou são passadas como um parâmetro explícito e documentar explicitamente quaisquer parâmetros que são implicitamente passados ​​pelos chamadores de uma função. Mas em uma base de código de ~ 10 8- LOC de décadas, esses são luxos que geralmente não se tem.

E, é claro, essencialmente todas as boas práticas para refatoração em idiomas com escopo lexical também são aplicáveis ​​em idiomas com escopo dinâmico - testes de gravação e assim por diante. A questão, então, é a seguinte: como mitigamos os riscos especificamente associados ao aumento da fragilidade do código de escopo dinâmico ao refatorar?

(Observe que, embora Como você navegue e refatore o código escrito em um idioma dinâmico? Tenha um título semelhante a esta pergunta, ele não tem nenhuma relação.)

senshin
fonte
relacionado (possivelmente uma duplicata): Existe uma correlação entre a escala do projeto e o rigor do idioma?
gnat
@gnat Não estou vendo como essa pergunta / suas respostas são relevantes para essa pergunta.
senshin 12/09/2015
1
@gnat Você está dizendo que a resposta é "use processos diferentes e outras coisas pesadas"? Quero dizer, isso provavelmente não está errado, mas também é geral demais a ponto de não ser particularmente útil.
senshin 12/09/2015
2
Honestamente, acho que não há uma resposta para isso além de "mudar para um idioma em que as variáveis ​​realmente têm regras de escopo" ou "usar o enteado bastardo da notação húngara, em que todas as variáveis ​​são prefixadas pelo nome do arquivo e / ou método do que tipo ou tipo ". O problema que você descreve é tão terrível que não consigo imaginar uma boa solução.
Ixrec 12/09/2015
4
Pelo menos você não pode acusar MUMPS de publicidade falsa por ter o nome de uma doença desagradável.
precisa saber é o seguinte

Respostas:

4

Uau.

Como não conheço o MUMPS como idioma, não sei se meu comentário se aplica aqui. De um modo geral - você deve refatorar de dentro para fora. Os consumidores (leitores) do estado global (variáveis ​​globais) devem ser refatorados em métodos / funções / procedimentos usando parâmetros. O método c deve ficar assim após a refatoração:

function c(c_scope_x) {
   print c(c_scope_x);
}

todos os usos de c devem ser reescritos (o que é uma tarefa mecânica)

c(x)

isso é para isolar o código "interno" do estado global usando o estado local. Quando terminar, você terá que reescrever b em:

function b() {
   x="oops"
   print c(x);
}

a tarefa x = "oops" existe para manter os efeitos colaterais. Agora devemos considerar b como poluidor do estado global. Se você tiver apenas um elemento poluído, considere esta refatoração:

function b() {
   x="oops"
   print c(x);
   return x;
}

final reescreva cada uso de b com x = b (). A função b deve usar apenas métodos já limpos (você pode querer renomear co-esclarecer isso) ao fazer essa refatoração. Depois disso, você deve refatorar b para não poluir o ambiente global.

function b() {
   newvardefinition b_scoped_x="oops"
   print c_cleaned(b_scoped_x);
   return b_scoped_x;
}

renomeie b para b_cleaned. Eu acho que você terá que brincar um pouco com isso para se acostumar com essa refatoração. Claro que nem todo método pode ser refatorado por isso, mas você terá que começar pelas partes internas. Tente isso com Eclipse e java (métodos de extração) e "estado global", também conhecido como membros da classe, para ter uma idéia.

function x() {
  fifth_to_refactor();
  {
    forth_to_refactor()
    ....
    {
      second_to_refactor();
    }
    ...
    third_to_refactor();
  }
  first_to_refactor()
}

hth.

Pergunta: Com esses perigos em mente, como podemos refatorar com segurança o código no MUMPS ou em qualquer outro idioma com escopo dinâmico?

  • Talvez alguém possa dar uma dica.

Pergunta: Como mitigamos os riscos especificamente associados ao aumento da fragilidade do código de escopo dinâmico ao refatorar?

  • Escreva um programa que faça as refatorações seguras para você.
  • Escreva um programa que identifique candidatos / primeiros candidatos seguros.
thepacker
fonte
Ah, existe um obstáculo específico ao MUMPS para tentar automatizar o processo de refatoração: o MUMPS não possui funções de primeira classe, nem ponteiros de função ou noção semelhante. O que significa que qualquer grande base de código MUMPS inevitavelmente terá muitos usos de eval (no MUMPS, chamado EXECUTE), às vezes até na entrada higienizada do usuário - o que significa que pode ser impossível encontrar e reescrever estaticamente todos os usos de uma função.
senshin 12/09/2015
Ok, considere minha resposta como inadequada. Um vídeo do youtube, acho que refatorar @ google scale, fez uma abordagem muito única. Eles usaram o clang para analisar um AST e, em seguida, usaram seu próprio mecanismo de pesquisa para encontrar qualquer (mesmo uso oculto) para refatorar seu código. Essa pode ser uma maneira de encontrar todos os usos. Quero dizer uma abordagem de análise e pesquisa no código da caxumba.
thepacker
2

Eu acho que sua melhor chance é colocar toda a base de código sob seu controle e ter uma visão geral sobre os módulos e suas dependências.

Portanto, pelo menos você tem a chance de fazer pesquisas globais e a possibilidade de adicionar testes de regressão para as partes do sistema em que espera um impacto causado por uma alteração no código.

Se você não tiver a chance de realizar o primeiro, meu melhor conselho é: não refatorar nenhum módulo reutilizado por outros módulos ou para o qual você não sabe que outros dependem deles . Em qualquer base de código de tamanho razoável, as chances são altas de encontrar módulos dos quais nenhum outro módulo depende. Portanto, se você possui um mod A dependendo de B, mas não vice-versa, e nenhum outro módulo depende de A, mesmo em uma linguagem com escopo dinâmico, você pode fazer alterações em A sem quebrar B ou quaisquer outros módulos.

Isso permite que você substitua a dependência de A a B por uma de A a B2, onde B2 é uma versão reescrita e higienizada de B. B2 deve ser uma nova escrita com as regras em mente mencionadas acima para criar o código mais evolutível e mais fácil de refatorar.

Doc Brown
fonte
Esse é um bom conselho, apesar de acrescentar que isso é inerentemente difícil no MUMPS, já que não há noção de especificadores de acesso nem qualquer outro mecanismo de encapsulamento, o que significa que as APIs que especificamos em nossa base de código são efetivamente apenas sugestões para os consumidores do código sobre quais funções eles devem chamar. (Claro, esta dificuldade particular não está relacionado com escopo dinâmico;. Estou apenas fazendo uma nota de isso como um ponto de interesse)
Senshin
Depois de ler este artigo , tenho certeza de que não o invejo por sua tarefa.
Doc Brown
0

Para afirmar o óbvio: como fazer a refatoração aqui? Prossiga com muito cuidado.

(Como você descreveu, o desenvolvimento e a manutenção da base de código existente já deve ser bastante difícil, e muito menos tentar refatorá-la.)

Acredito que aplicaria retroativamente uma abordagem orientada a testes aqui. Isso envolveria a criação de um conjunto de testes para garantir que a funcionalidade atual permaneça funcionando quando você inicia a refatoração, primeiro apenas para facilitar o teste. (Sim, espero um problema de galinha e ovo aqui, a menos que seu código já seja modular o suficiente para ser testado sem alterá-lo.)

Em seguida, você pode prosseguir com outra refatoração, verificando se não quebrou nenhum teste à medida que avança.

Finalmente, você pode começar a escrever testes que esperam novas funcionalidades e, em seguida, escrever o código para fazer esses testes funcionarem.

Mark Hurd
fonte