Qual estilo é melhor (variável de instância versus valor de retorno) em Java

32

Muitas vezes me encontro lutando para decidir qual dessas duas maneiras usar quando preciso usar dados comuns em alguns métodos de minhas classes. Qual seria uma escolha melhor?

Nesta opção, eu posso criar uma variável de instância para evitar a necessidade de declarar variáveis ​​adicionais e também para definir parâmetros de método, mas pode não estar tão claro onde essas variáveis ​​estão sendo instanciadas / modificadas:

public class MyClass {
    private int var1;

    MyClass(){
        doSomething();
        doSomethingElse();
        doMoreStuff();
    }

    private void doSomething(){
        var1 = 2;
    }

    private void doSomethingElse(){
        int var2 = var1 + 1;
    }

    private void doMoreStuff(){
        int var3 = var1 - 1;
    }
}

Ou apenas instanciar variáveis ​​locais e passá-las como argumentos?

public class MyClass {  
    MyClass(){
        int var1 = doSomething();
        doSomethingElse(var1);
        doMoreStuff(var1);
    }

    private int doSomething(){
        int var = 2;
        return var;
    }

    private void doSomethingElse(int var){
        int var2 = var + 1;
    }

    private void doMoreStuff(int var){
        int var3 = var - 1;
    }
}

Se a resposta é que ambos estão corretos, qual deles é visto / usado com mais frequência? Além disso, se você puder fornecer prós / contras adicionais para cada opção, seria muito valioso.

Carlossierra
fonte
1
Reletad (duplicado?): Programmers.stackexchange.com/questions/164347/…
Martin Ba
1
Acho que ninguém apontou ainda que colocar resultados intermediários em variáveis ​​de instância pode dificultar a concorrência, devido à possibilidade de contenção entre threads para essas variáveis.
sdenham

Respostas:

107

Estou surpreso que isso ainda não tenha mencionado ...

Depende se var1é realmente parte do estado do seu objeto .

Você assume que essas duas abordagens estão corretas e que é apenas uma questão de estilo. Você está errado.

Isso é inteiramente sobre como modelar corretamente.

Da mesma forma, privateexistem métodos de instância para alterar o estado do seu objeto . Se não é isso que seu método está fazendo, deve ser private static.

MetaFight
fonte
7
@mucaho: transientnão tem nada a ver com isso, porque transientse trata de estado persistente , como em partes do objeto que são salvas quando você faz algo como serializar o objeto. Por exemplo, o repositório de backup de um ArrayList é transienttotalmente crucial para o estado do ArrayList, porque quando você serializa um ArrayList, deseja salvar apenas a parte do repositório que contém elementos reais do ArrayList, não o espaço livre no final, reservado para mais adições de elementos.
User2357112 suporta Monica
4
Acrescentando à resposta - se var1for necessário para alguns métodos, mas não parte do estado de MyClass, talvez seja hora de colocar var1esses métodos em outra classe a ser usada MyClass.
Mike Partridge
5
@CandiedOrange Estamos falando sobre modelagem correta aqui. Quando dizemos "parte do estado do objeto", não estamos falando sobre as partes literais no código. Estamos discutindo uma noção conceitual de estado, qual deve ser o estado do objeto para modelar os conceitos corretamente. A "vida útil" é provavelmente o maior fator decisivo nisso.
jpmc26
4
@CandiedOrange Next e Previous são obviamente métodos públicos. Se o valor de var1 é relevante apenas em uma sequência de métodos privados, claramente não faz parte do estado persistente do objeto e, portanto, deve ser passado como argumento.
Taemyr
2
@CandiedOrange Você esclareceu o que queria dizer após dois comentários. Estou dizendo que você poderia facilmente ter na primeira resposta. Além disso, sim, esqueci que há mais do que apenas o objeto; Concordo que, às vezes, os métodos de instância privada têm um propósito. Eu simplesmente não conseguia lembrar ninguém. Edição: Acabei de perceber que você não era, de fato, a primeira pessoa a me responder. Minha culpa. Eu estava confuso sobre o porquê de uma resposta ser curta, com uma frase e sem resposta, enquanto a próxima realmente explicava as coisas.
Fund Monica's Lawsuit
17

Não sei qual é mais prevalente, mas sempre faria o último. Ele comunica mais claramente o fluxo de dados e a vida útil e não incha todas as instâncias da sua classe com um campo cuja única vida útil relevante é durante a inicialização. Eu diria que o primeiro é apenas confuso e torna a revisão de código significativamente mais difícil, porque tenho que considerar a possibilidade de qualquer método ser modificado var1.

Derek Elkins
fonte
7

Você deve reduzir o escopo de suas variáveis ​​o máximo possível (e razoável). Não apenas em métodos, mas em geral.

Para sua pergunta, isso significa que depende se a variável faz parte do estado do objeto. Se sim, não há problema em usá-lo nesse escopo, ou seja, todo o objeto. Neste caso, vá com a primeira opção. Se não, escolha a segunda opção, pois ela reduz a visibilidade das variáveis ​​e, portanto, a complexidade geral.

Andy
fonte
5

Qual estilo é melhor (variável de instância versus valor de retorno) em Java

Há outro estilo - use um contexto / estado.

public static class MyClass {
    // Hold my state from one call to the next.
    public static final class State {
        int var1;
    }

    MyClass() {
        State state = new State();
        doSomething(state);
        doSomethingElse(state);
        doMoreStuff(state);
    }

    private void doSomething(State state) {
        state.var1 = 2;
    }

    private void doSomethingElse(State state) {
        int var2 = state.var1 + 1;
    }

    private void doMoreStuff(State state) {
        int var3 = state.var1 - 1;
    }
}

Há vários benefícios nessa abordagem. Os objetos de estado podem mudar independentemente do objeto, por exemplo, dando muito espaço de manobra para o futuro.

Esse é um padrão que também funciona bem em um sistema distribuído / servidor em que alguns detalhes devem ser preservados nas chamadas. Você pode armazenar detalhes do usuário, conexões com o banco de dados, etc. no stateobjeto.

OldCurmudgeon
fonte
3

É sobre efeitos colaterais.

Perguntar se var1é parte do Estado erra o objetivo desta questão. Claro que se var1deve persistir, tem que ser uma instância. Qualquer uma das abordagens pode ser feita para trabalhar se a persistência é necessária ou não.

A abordagem do efeito colateral

Algumas variáveis ​​de instância são usadas apenas para se comunicar entre métodos privados de chamada em chamada. Esse tipo de variável de instância pode ser refatorado fora da existência, mas não precisa ser. Às vezes as coisas são mais claras com eles. Mas isso não é isento de riscos.

Você está deixando uma variável fora de seu escopo porque é usada em dois escopos particulares diferentes. Não porque é necessário no escopo que você está colocando. Isso pode ser confuso. Os "globais são maus!" nível de confusão. Isso pode funcionar, mas não será bem dimensionado. Só funciona no pequeno. Não há grandes objetos. Não há longas cadeias de herança. Não cause um efeito ioiô .

A abordagem funcional

Agora, mesmo que var1deva persistir, nada indica que você deve usá-lo para cada valor transitório que possa assumir antes de atingir o estado que você deseja preservar entre chamadas públicas. Isso significa que você ainda pode definir uma var1instância usando nada além de métodos mais funcionais.

Portanto, parte do estado ou não, você ainda pode usar qualquer uma das abordagens.

Nestes exemplos, 'var1' é tão encapsulado que nada além do seu depurador sabe que ele existe. Suponho que você fez isso deliberadamente porque não quer nos influenciar. Felizmente eu não ligo para qual.

O risco de efeitos colaterais

Dito isto, eu sei de onde vem sua pergunta. Eu trabalhei sob a miserável herança de yo yo 'que modifica uma variável de instância em vários níveis em vários métodos e me esqueci tentando segui-la. Esse é o risco.

Essa é a dor que me leva a uma abordagem mais funcional. Um método pode documentar suas dependências e saída em sua assinatura. Essa é uma abordagem clara e poderosa. Também permite alterar o que você passa no método privado, tornando-o mais reutilizável dentro da classe.

A vantagem dos efeitos colaterais

Também é limitador. Funções puras não têm efeitos colaterais. Isso pode ser uma coisa boa, mas não é orientada a objetos. Uma grande parte da orientação a objetos é a capacidade de se referir a um contexto fora do método. Fazer isso sem vazar globais por aqui e por aí vai é a força da OOP. Eu recebo a flexibilidade de um global, mas ele está bem contido na classe. Eu posso chamar um método e alterar todas as variáveis ​​de instância de uma só vez, se eu quiser. Se fizer isso, sou obrigado a pelo menos dar um nome ao método que deixe claro o que está acontecendo, para que as pessoas não se surpreendam quando isso acontecer. Comentários também podem ajudar. Às vezes, esses comentários são formalizados como "pós-condições".

A desvantagem dos métodos privados funcionais

A abordagem funcional esclarece algumas dependências. A menos que você esteja em uma linguagem funcional pura, ele não pode excluir dependências ocultas. Você não sabe, olhando apenas para uma assinatura de métodos, que não está escondendo um efeito colateral de você no restante do código. Você simplesmente não.

Postar condicionais

Se você e todos os outros membros da equipe documentam com segurança os efeitos colaterais (condições pré / pós) nos comentários, o ganho da abordagem funcional é muito menor. Sim, eu sei, sonhe.

Conclusão

Pessoalmente, eu tendem a usar métodos privados funcionais em ambos os casos, se puder, mas honestamente é principalmente porque esses comentários de efeitos colaterais condicionais pré / pós não causam erros de compilador quando estão desatualizados ou quando os métodos são chamados fora de ordem. A menos que eu realmente precise da flexibilidade dos efeitos colaterais, prefiro apenas saber que as coisas funcionam.

MagicWindow
fonte
1
"Algumas variáveis ​​de instância são usadas apenas para se comunicar entre métodos privados de chamada em chamada." Isso é um cheiro de código. Quando variáveis ​​de instância são usadas dessa maneira, é um sinal de que a classe é muito grande. Extraia essas variáveis ​​e métodos para uma nova classe.
Kevin Cline
2
Isso realmente não faz sentido. Embora o OP possa escrever o código no paradigma funcional (e isso geralmente seria uma coisa boa), esse obviamente não é o contexto da questão. Tentando dizer ao OP que eles podem evitar o armazenamento de estado do objeto mudando paradigmas não é realmente relevante ...
jpmc26
@kevincline o exemplo do OP tem apenas uma variável de instância e 3 métodos. Essencialmente, ele já foi extraído para uma nova classe. Não que o exemplo faça alguma coisa útil. O tamanho da classe não tem nada a ver com a maneira como seus métodos auxiliares privados se comunicam.
MagicWindow
@ jpmc26 O OP já demonstrou que eles podem evitar o armazenamento var1como uma variável de estado alterando paradigmas. O escopo da classe NÃO é exatamente onde o estado está armazenado. É também um escopo anexo. Isso significa que existem duas motivações possíveis para colocar uma variável no nível da classe. Você pode reivindicar fazê-lo apenas para o escopo anexo e não para o estado ser mau, mas eu digo que é uma troca com a aridade que também é má. Eu sei disso porque tive que manter o código que faz isso. Alguns foram bem feitos. Alguns foi um pesadelo. A linha entre os dois não é estado. É legibilidade.
MagicWindow
A regra de estado aprimora a legibilidade: 1) evite que os resultados intermediários das operações sejam parecidos com o estado 2) evite ocultar as dependências dos métodos. Se a aridade de um método é alta, representa irredutivelmente e genuinamente a complexidade desse método ou o design atual possui complexidade desnecessária.
precisa saber é o seguinte
1

A primeira variante parece não intuitiva e potencialmente perigosa para mim (imagine por qualquer motivo que alguém torne seu método privado público).

Eu preferiria instanciar suas variáveis ​​na construção da classe ou passá-las como argumentos. O último oferece a opção de usar idiomas funcionais e não depender do estado do objeto que o contém.

Brian Agnew
fonte
0

Já existem respostas falando sobre o estado do objeto e quando o segundo método é preferido. Eu só quero adicionar um caso de uso comum para o primeiro padrão.

O primeiro padrão é perfeitamente válido quando tudo o que sua classe faz é encapsular um algoritmo . Um caso de uso para isso é que, se você escrevesse o algoritmo em um único método, ele seria muito grande. Então, você a divide em métodos menores, torna uma classe e torna os sub métodos privados.

Agora, passar todo o estado do algoritmo por meio de parâmetros pode ser entediante, então você usa os campos privados. Também está de acordo com a regra do primeiro parágrafo, porque é basicamente um estado da instância. Você só deve ter em mente e documentar adequadamente que o algoritmo não é reentrante se você usar campos particulares para isso . Isso não deve ser um problema na maioria das vezes, mas pode morder você.

Honza Brabec
fonte
0

Vamos tentar um exemplo que faça alguma coisa. Perdoe-me, pois este é javascript, não java. O ponto deve ser o mesmo.

Visite https://blockly-games.appspot.com/pond-duck?lang=en , clique na guia javascript e cole:

hunt = lock(-90,1), 
heading = -135;

while(true) {
  hunt()  
  heading += 2
  swim(heading,30)
}

function lock(direction, width) {
  var dir = direction
  var wid = width
  var dis = 10000

  //hunt
  return function() {
    //randomize() //Calling this here makes the state of dir meaningless
    scanLock()
    adjustWid()
    if (isSpotted()) {
      if (inRange()) {
        if (wid <= 4) {
          cannon(dir, dis)
        }
      }
    } else {
      if (!left()) {
        right()
      }
    }
  }

  function scanLock() {
    dis = scan(dir, wid);
  }

  function adjustWid() {
    if (inRange()) {
      if (wid > 1)
        wid /= 2;
    } else {
      if (wid < 16) {
        wid *= 2; 
      }
    }
  }

  function isSpotted() {
    return dis < 1000;
  }

  function left() {
    dir += wid;
    scanLock();
    return isSpotted();
  }

  function right() {
    dir -= wid*2;
    scanLock();
    return isSpotted()
  }

  function inRange() {
    return dis < 70;
  }

  function randomize() {
    dir = Math.random() * 360
  }
}

Você deve perceber que dir, wide disnão estão sendo passados ao redor muito. Você também deve observar que o código na função que lock()retorna se parece muito com pseudo-código. Bem, é um código real. No entanto, é muito fácil de ler. Poderíamos adicionar aprovação e atribuição, mas isso acrescentaria uma confusão que você nunca veria em pseudo-código.

Se você deseja argumentar que não executar a atribuição e aprovação é bom porque esses três vars são de estado persistente, considere uma reformulação que atribua dirum valor aleatório a cada loop. Não é persistente agora, é?

Claro, agora podemos diminuir diro escopo agora, mas não sem sermos forçados a desordenar nosso código pseudo-semelhante com a passagem e a configuração.

Portanto, não, o estado não é o motivo pelo qual você decide usar ou não os efeitos colaterais, em vez de passar e retornar. Os efeitos colaterais também não significam que seu código é ilegível. Você não obtém os benefícios do código funcional puro . Mas bem feitos, com bons nomes, eles podem realmente ser legais de ler.

Isso não quer dizer que eles não possam se transformar em um espaguete como um pesadelo. Mas então, o que não pode?

Feliz caça ao pato.

candied_orange
fonte
1
Funções internas / externas são um paradigma diferente do que o OP descreve. - Concordo que o trade-off entre variáveis ​​locais em uma função externa contra argumentos passados ​​para as funções internas é semelhante a variáveis ​​privadas em uma classe contra argumentos passados ​​para funções privadas. No entanto, se você fizer essa comparação, a vida útil do objeto corresponderia a uma única execução da função, portanto, as variáveis ​​na função externa fazem parte do estado persistente.
Taemyr 22/04
@ Taemyr, o OP descreve métodos privados chamados dentro de um construtor público que me parecem muito interiores. O estado não é realmente 'persistente' se você pisar nele toda vez que inserir uma função. Às vezes, o estado é usado como um local compartilhado, os métodos podem escrever e ler. Não significa que precisa ser persistido. O fato de persistir de qualquer maneira não é algo em que DEVE depender.
candied_orange
1
É persistente, mesmo se você pisar nele toda vez que descartar o objeto.
Taemyr
1
Bons pontos, mas expressá-los em JavaScript realmente ofusca a discussão. Além disso, se tirar fora a sintaxe JS, acaba com o objecto de contexto que lhe é transmitido em torno (do fecho da função exterior)
fdreger
1
@sdenham Estou argumentando que há uma diferença entre os atributos de uma classe e os resultados intermediários transitórios de uma operação. Eles não são equivalentes. Mas um pode ser armazenado no outro. Certamente não precisa ser assim. A questão é se deveria ser. Sim, existem problemas semânticos e vitalícios. Há também problemas de aridade. Você não acha que eles são importantes o suficiente. Isso é bom. Mas dizer que usar o nível de classe para algo que não seja o estado do objeto é modelagem incorreta, é insistir em uma maneira de modelar. Eu mantive o código profissional que fez o contrário.
candied_orange