Sinalizadores de método como argumentos ou como variáveis ​​de membro?

8

Eu acho que o título "Método sinalizadores como argumentos ou como variáveis ​​de membro?" pode ser subótimo, mas como estou perdendo uma terminologia melhor, aqui vai:

Atualmente, estou tentando entender o problema de saber se os sinalizadores para um determinado método de classe ( privado ) devem ser passados ​​como argumentos de função ou via variável de membro e / ou se existe algum padrão ou nome que cubra esse aspecto e / ou se isso sugere alguns outros problemas de design.

Por exemplo (a linguagem pode ser C ++, Java, C #, realmente não importa IMHO):

class Thingamajig {
  private ResultType DoInternalStuff(FlagType calcSelect) {
    ResultType res;
    for (... some loop condition ...) {
      ...
      if (calcSelect == typeA) {
        ...
      } else if (calcSelect == typeX) {
        ...
      } else if ...
    }
    ...
    return res;
  }

  private void InteralStuffInvoker(FlagType calcSelect) {
    ...
    DoInternalStuff(calcSelect);
    ...
  }

  public void DoThisStuff() {
    ... some code ...
    InternalStuffInvoker(typeA);
    ... some more code ...
  }

  public ResultType DoThatStuff() {
    ... some code ...
    ResultType x = DoInternalStuff(typeX);
    ... some more code ... further process x ...
    return x;
  }
}

O que vemos acima é que o método InternalStuffInvokerusa um argumento que não é usado dentro dessa função, mas é encaminhado apenas para o outro método privado DoInternalStuff. (Onde DoInternalStuffserá usado em particular em outros lugares desta classe, por exemplo, no DoThatStuffmétodo (público).)

Uma solução alternativa seria adicionar uma variável de membro que carregasse essas informações:

class Thingamajig {
  private ResultType DoInternalStuff() {
    ResultType res;
    for (... some loop condition ...) {
      ...
      if (m_calcSelect == typeA) {
        ...
      } ...
    }
    ...
    return res;
  }

  private void InteralStuffInvoker() {
    ...
    DoInternalStuff();
    ...
  }

  public void DoThisStuff() {
    ... some code ...
    m_calcSelect = typeA;
    InternalStuffInvoker();
    ... some more code ...
  }

  public ResultType DoThatStuff() {
    ... some code ...
    m_calcSelect = typeX;
    ResultType x = DoInternalStuff();
    ... some more code ... further process x ...
    return x;
  }
}

Especialmente para cadeias de chamadas profundas em que o sinalizador seletor para o método interno é selecionado fora, o uso de uma variável de membro pode tornar as funções intermediárias mais limpas, pois elas não precisam carregar um parâmetro de passagem.

Por outro lado, essa variável de membro não está realmente representando nenhum estado do objeto (como não está definida nem disponível fora), mas é realmente um argumento adicional oculto para o método privado "interno".

Quais são os prós e os contras de cada abordagem?

Martin Ba
fonte
1
Considere que, se você precisar passar sinalizadores, provavelmente terá várias funções executando consideravelmente mais de uma coisa. Você pode considerar como o controle flui para não precisar ofuscá-lo com sinalizadores.
cHao 10/09/12
Também quero mencionar que "DoInternalStuff" e "InternalStuff invoker" são nomes realmente ruins. :) Sim, eu sei que é apenas um exemplo, mas é meio ameaçador. Se o seu código real faz algo tão vago quanto "coisas internas", isso é um problema maior. Resolva isso e é mais provável que alguma maneira de simplificar o fluxo de controle se revele.
cHao 10/09/12
1
Concordo com o @cHao - onde já tive situações como essa no passado, o sinalizador de opções transmitido com frequência era um cheiro de código que o princípio de responsabilidade única estava sendo violado.
Bevan

Respostas:

15

Por outro lado, essa variável de membro não está realmente representando nenhum estado do objeto (como não está definida nem disponível fora), mas é realmente um argumento adicional oculto para o método privado "interno".

Isso reforça minha intuição ao ler sua pergunta: tente manter esses dados o mais local possível, ou seja, não os adicione ao estado do objeto . Isso reduz o espaço de estado da sua classe, tornando seu código mais simples de entender, testar e manter. Sem mencionar que evitar o estado compartilhado também torna sua classe mais segura - isso pode ou não ser uma preocupação para você agora, mas pode fazer uma enorme diferença no futuro.

Obviamente, se você precisar passar excessivamente por essas bandeiras, isso pode se tornar um incômodo. Nesse caso, você pode considerar

  • criar objetos de parâmetro para agrupar parâmetros relacionados em um único objeto e / ou
  • encapsular esse estado e a lógica associada em uma família separada de estratégias . A viabilidade disso depende da linguagem: em Java, é necessário definir uma interface distinta e uma classe de implementação (possivelmente anônima) para cada estratégia, enquanto em C ++ ou C # você pode usar ponteiros de função, delegados ou lambdas, o que simplifica o código consideravelmente.
Péter Török
fonte
Obrigado por mencionar a estratégia / lambda. Eu vou ter que manter essa solução em torno de quando as coisas precisam ser virado do avesso :-)
Martin Ba
6

Eu definitivamente votaria na opção "argumento" - minhas razões:

  1. Sincronização - e se o método for chamado de vários threads ao mesmo tempo? Ao passar os sinalizadores como argumento, você não precisará sincronizar o acesso a esses sinalizadores.

  2. Testabilidade - Vamos imaginar que você está escrevendo um teste de unidade para sua classe. Como você testará o estado interno do membro? E se o seu código lançar uma exceção e o membro terminar em um estado inesperado?

  3. Separação de preocupações - Ao adicionar as sinalizações aos argumentos do método, fica claro que nenhum outro método deve usá-lo. Você não o utilizará por engano em outro método.

Sulthan
fonte
3

A primeira alternativa parece boa para mim. Uma função é chamada com um parâmetro e faz algo que depende desse parâmetro . Mesmo se você apenas encaminhar o parâmetro para outra função, espera que o resultado dependa de alguma forma do parâmetro.

A segunda alternativa parece usar uma variável global, apenas dentro do escopo da classe, em vez do escopo do aplicativo. Mas é o mesmo problema ... você deve ler atentamente todo o código da classe para determinar quem e quando está lendo e gravando esses valores.

Portanto, a primeira alternativa vence.

Se você usar muitos parâmetros (leia-se: dois ou mais) na primeira alternativa, todos passados ​​juntos para a próxima função, considere colocá-los em um único objeto (torne-o uma classe interna, se não for relevante para o restante do aplicativo) e usando esse objeto como parâmetro.

Viliam Búr
fonte
1

Eu quero dizer "nenhum".

Primeiro, considere que você já sabe o que fará quando ligar DoInternalStuff. A bandeira diz o mesmo. Então agora, em vez de seguir em frente e fazê- lo, você está peidando ao chamar uma função que agora precisa decidir o que fazer.

Se você quiser dizer à função o que fazer, diga-lhe o que fazer . Você pode passar uma função real que deseja executar para cada iteração como um representante de C # (ou um objeto de função C ++, ou um executável em Java ou semelhante).

Em c #:

class Thingamajig {
  private delegate WhateverType Behavior(ArgsType args);

  private ResultType DoInternalStuff(Behavior behavior) {
    ResultType res;
    for (... some loop condition ...) {
      ...
      var subResult = behavior(someArgs);
      ...
    }
    ...
    return res;
  }

  private void InteralStuffInvoker(Behavior behavior) {
    ...
    DoInternalStuff(behavior);
    ...
  }

  public void DoThisStuff() {
    ... some code ...
    InternalStuffInvoker(doThisStuffPerIteration);
    ... some more code ...
  }

  public ResultType DoThatStuff() {
    ... some code ...
    ResultType x = DoInternalStuff(doThatStuffPerIteration);
    ... some more code ... further process x ...
    return x;
  }
}

Você ainda acaba com o argumento passando por todo o lado, mas se livra da maior parte da porcaria de if / else DoInternalStuff. Você também pode armazenar o delegado como um campo, se desejar.

Quanto a distribuir ou armazenar a coisa toda ... se você tiver que escolher uma, passe-a por aí. Armazená-lo provavelmente irá morder sua bunda mais tarde, pois você praticamente negligenciou a segurança do thread. Considere que, se você pretende fazer algo com threads, agora precisa bloquear por toda a duração do loop, ou alguém pode alternar as tarefas de baixo de você. Considerando que, se você passar, é menos provável que seja destruído ... e se você ainda precisar bloquear ou algo assim, poderá fazê-lo por parte de uma iteração em vez de todo o loop.

Francamente, é bom que seja irritante. Provavelmente existe uma maneira muito mais simples de fazer o que você quer, o que eu realmente não poderia aconselhá-lo, já que o código obviamente não é a coisa real. A melhor resposta varia de acordo com o caso.

cHao
fonte