Qué estilo es mejor (variable de instancia versus valor de retorno) en Java

32

A menudo me encuentro luchando por decidir cuál de estas dos formas usar cuando necesito usar datos comunes a través de algunos métodos en mis clases. ¿Cuál sería una mejor opción?

En esta opción, puedo crear una variable de instancia para evitar la necesidad de tener que declarar variables adicionales y también para evitar definir los parámetros del método, pero puede que no esté tan claro dónde se instancian / modifican esas variables:

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;
    }
}

¿O simplemente instanciar variables locales y pasarlas 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;
    }
}

Si la respuesta es que ambos son correctos, ¿cuál se ve / usa con más frecuencia? Además, si puede proporcionar ventajas / desventajas adicionales para cada opción, sería muy valioso.

carlossierra
fuente
1
Reletad (¿duplicado?): Programmers.stackexchange.com/questions/164347/…
Martin Ba
1
No creo que nadie haya señalado aún que poner resultados intermedios en variables de instancia puede hacer que la concurrencia sea más difícil, debido a la posibilidad de contención entre hilos para estas variables.
sdenham

Respuestas:

107

Me sorprende que esto no haya mencionado aún ...

Depende de si var1es realmente parte del estado de su objeto .

Asume que ambos enfoques son correctos y que es solo una cuestión de estilo. Está usted equivocado.

Esto es completamente acerca de cómo modelar adecuadamente.

Del mismo modo, privateexisten métodos de instancia para mutar el estado de su objeto . Si eso no es lo que está haciendo su método, entonces debería serlo private static.

MetaFight
fuente
77
@mucaho: transientno tiene nada que ver con esto, porque transientse trata de un estado persistente , como en partes del objeto que se guardan cuando haces algo como serializar el objeto. Por ejemplo, una tienda de respaldo de ArrayList es transienta pesar de que es completamente crucial para el estado de ArrayList, porque cuando se serializa una ArrayList, solo desea guardar la parte de la tienda de respaldo que contiene elementos de ArrayList reales, no el espacio libre al final reservado para más adiciones de elementos.
user2357112 es compatible con Monica el
44
Agregando a la respuesta: si var1es necesario para un par de métodos, pero no es parte del estado MyClass, podría ser el momento de poner var1esos métodos en otra clase para ser utilizados MyClass.
Mike Partridge
55
@CandiedOrange Estamos hablando del modelado correcto aquí. Cuando decimos "parte del estado del objeto", no estamos hablando de las piezas literales en el código. Estamos discutiendo una noción conceptual de estado, cuál debería ser el estado del objeto para modelar los conceptos correctamente. La "vida útil" es probablemente el mayor factor decisivo en eso.
jpmc26
44
@CandiedOrange Next y Previous son obviamente métodos públicos. Si el valor de var1 solo es relevante en una secuencia de métodos privados, claramente no es parte del estado persistente del objeto y, por lo tanto, debe pasarse como argumentos.
Taemyr
2
@CandiedOrange Aclaraste lo que querías decir, después de dos comentarios. Estoy diciendo que fácilmente podrías tenerlo en la primera respuesta. Además, sí, olvidé que hay más que solo el objeto; Estoy de acuerdo en que a veces los métodos de instancias privadas tienen un propósito. Simplemente no podía recordar nada. EDITAR: Acabo de darme cuenta de que no eras, de hecho, la primera persona en responderme. Mi error. Estaba confundido sobre por qué una respuesta era cortante, de una oración, sin respuesta, mientras que la siguiente realmente explicaba las cosas.
Financia la demanda de Mónica el
17

No sé cuál es más frecuente, pero siempre haría lo último. Comunica más claramente el flujo de datos y la vida útil, y no llena cada instancia de su clase con un campo cuya única vida útil relevante es durante la inicialización. Diría que lo primero es confuso y hace que la revisión del código sea mucho más difícil, porque tengo que considerar la posibilidad de que cualquier método pueda modificarse var1.

Derek Elkins
fuente
7

Debe reducir el alcance de sus variables tanto como sea posible (y razonable). No solo en métodos, sino en general.

Para su pregunta, eso significa que depende de si la variable es parte del estado del objeto. En caso afirmativo, está bien usarlo en ese ámbito, es decir, todo el objeto. En este caso ve con la primera opción. Si no, elija la segunda opción, ya que reduce la visibilidad de las variables y, por lo tanto, la complejidad general.

Andy
fuente
5

Qué estilo es mejor (variable de instancia versus valor de retorno) en Java

Hay otro estilo: use un 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;
    }
}

Hay varios beneficios para este enfoque. Los objetos de estado pueden cambiar independientemente del objeto, por ejemplo, dando mucho margen de maniobra para el futuro.

Este es un patrón que también funciona bien en un sistema distribuido / servidor donde se deben preservar algunos detalles en las llamadas. Puede almacenar detalles de usuario, conexiones de base de datos, etc. en el stateobjeto.

OldCurmudgeon
fuente
3

Se trata de efectos secundarios.

Al preguntar si var1es parte del estado se pierde el punto de esta pregunta. Claro que si var1debe persistir, tiene que ser una instancia. Cualquiera de los dos enfoques puede funcionar si se necesita persistencia o no.

El enfoque de efectos secundarios

Algunas variables de instancia solo se utilizan para comunicarse entre métodos privados de llamada a llamada. Este tipo de variable de instancia se puede refactorizar fuera de existencia, pero no tiene que ser así. A veces las cosas son más claras con ellos. Pero esto no está exento de riesgos.

Está dejando una variable fuera de su alcance porque se usa en dos ámbitos privados diferentes. No porque sea necesario en el ámbito en el que lo está colocando. Esto puede ser confuso. ¡Los "globales son malos!" nivel de confusión Esto puede funcionar pero simplemente no escalará bien. Solo funciona en los pequeños. No hay grandes objetos. No largas cadenas de herencia. No provoques un efecto yo yo .

El enfoque funcional

Ahora, incluso si var1debe persistir, nada dice que debe usar if para cada valor transitorio que pueda tomar antes de que alcance el estado que desea conservar entre las llamadas públicas. Eso significa que aún puede establecer una var1instancia utilizando nada más que métodos más funcionales.

Entonces, como parte del estado o no, aún puede usar cualquiera de los enfoques.

En estos ejemplos, 'var1' está tan encapsulado que nada más que su depurador sabe que existe. Supongo que lo hiciste deliberadamente porque no quieres sesgarnos. Afortunadamente no me importa cuál.

El riesgo de efectos secundarios.

Dicho esto, sé de dónde viene tu pregunta. He trabajado bajo desgraciada yo yo 'ing herencia que muta una variable de instancia en múltiples niveles en varios métodos y se han ido ardilla tratando de seguirlo. Este es el riesgo.

Este es el dolor que me lleva a un enfoque más funcional. Un método puede documentar sus dependencias y resultados en su firma. Este es un enfoque poderoso y claro. También le permite cambiar lo que pasa el método privado haciéndolo más reutilizable dentro de la clase.

La ventaja de los efectos secundarios

También es limitante. Las funciones puras no tienen efectos secundarios. Eso puede ser algo bueno, pero no está orientado a objetos. Una gran parte de la orientación a objetos es la capacidad de referirse a un contexto fuera del método. Hacer eso sin fugas de glóbulos por todas partes aquí y allá es la fuerza de OOP. Tengo la flexibilidad de un global pero está muy bien contenido en la clase. Puedo llamar a un método y mutar cada variable de instancia a la vez si lo deseo. Si hago eso, estoy obligado a dar al menos al método un nombre que aclare lo que está haciendo para que la gente no se sorprenda cuando eso suceda. Los comentarios también pueden ayudar. Algunas veces estos comentarios se formalizan como "condiciones de publicación".

La desventaja de los métodos privados funcionales

El enfoque funcional aclara algunas dependencias. A menos que esté en un lenguaje funcional puro, no puede descartar dependencias ocultas. No sabe, mirando solo una firma de métodos, que no le está ocultando un efecto secundario en el resto de su código. Simplemente no lo haces.

Publicaciones condicionales

Si usted y todos los demás miembros del equipo documentan de manera confiable los efectos secundarios (condiciones previas / posteriores) en los comentarios, la ganancia del enfoque funcional es mucho menor. Sí, lo sé, sigue soñando.

Conclusión

Personalmente, tiendo a métodos privados funcionales en cualquier caso si puedo, pero honestamente es principalmente porque esos comentarios de efectos secundarios condicionales previos / posteriores no causan errores de compilación cuando están desactualizados o cuando los métodos se llaman fuera de servicio. A menos que realmente necesite la flexibilidad de los efectos secundarios, preferiría saber que las cosas funcionan.

MagicWindow
fuente
1
"Algunas variables de instancia solo se utilizan para comunicarse entre métodos privados de llamada a llamada". Eso es un olor a código. Cuando las variables de instancia se usan de esta manera, es una señal de que la clase es demasiado grande. Extrae esas variables y métodos en una nueva clase.
Kevin Cline
2
Esto realmente no tiene ningún sentido. Si bien el OP podría escribir el código en el paradigma funcional (y esto generalmente sería algo bueno), ese obviamente no es el contexto de la pregunta. Intentar decirle al OP que pueden evitar almacenar el estado del objeto cambiando los paradigmas no es realmente relevante ...
jpmc26
@kevincline el ejemplo del OP tiene solo una variable de instancia y 3 métodos. Esencialmente, ya se ha extraído en una nueva clase. No es que el ejemplo haga algo útil. El tamaño de la clase no tiene nada que ver con cómo sus métodos de ayuda privada se comunican entre sí.
MagicWindow
@ jpmc26 OP ya ha demostrado que pueden evitar el almacenamiento var1como una variable de estado cambiando los paradigmas. El alcance de la clase NO es solo donde se almacena el estado. También es un alcance cerrado. Eso significa que hay dos posibles motivaciones para poner una variable a nivel de clase. Puede afirmar que hacerlo solo por el alcance que lo abarca y no por el estado es malo, pero yo digo que es una compensación con el arity, que también es malo. Sé esto porque he tenido que mantener el código que hace esto. Algunos se hicieron bien. Algo fue una pesadilla. La línea entre los dos no es estado. Es legibilidad.
MagicWindow
La regla de estado mejora la legibilidad: 1) evite que los resultados provisionales de las operaciones se vean como el estado 2) evite ocultar las dependencias de los métodos. Si la aridad de un método es alta, entonces representa irreductible y genuinamente la complejidad de ese método o el diseño actual tiene una complejidad innecesaria.
sdenham
1

La primera variante parece no intuitiva y potencialmente peligrosa para mí (imagine por cualquier razón que alguien hace público su método privado).

Prefiero crear instancias de sus variables en la construcción de la clase, o pasarlas como argumentos. Este último le ofrece la opción de utilizar expresiones idiomáticas funcionales y no depender del estado del objeto que lo contiene.

Brian Agnew
fuente
0

Ya hay respuestas que hablan sobre el estado del objeto y cuándo se prefiere el segundo método. Solo quiero agregar un caso de uso común para el primer patrón.

El primer patrón es perfectamente válido cuando todo lo que hace su clase es que encapsula un algoritmo . Un caso de uso para esto es que si escribiera el algoritmo en un solo método, sería demasiado grande. Entonces lo divide en métodos más pequeños, lo convierte en una clase y hace que los submétodos sean privados.

Ahora, pasar todo el estado del algoritmo a través de parámetros puede ser tedioso, por lo que debe usar los campos privados. También está de acuerdo con la regla del primer párrafo porque es básicamente un estado de la instancia. Solo debe tener en cuenta y documentar adecuadamente que el algoritmo no es reentrante si utiliza campos privados para esto . Esto no debería ser un problema la mayor parte del tiempo, pero posiblemente puede morderte.

Honza Brabec
fuente
0

Probemos un ejemplo que hace algo. Perdóname ya que esto es JavaScript, no Java. El punto debería ser el mismo.

Visite https://blockly-games.appspot.com/pond-duck?lang=en , haga clic en la pestaña javascript y pegue esto:

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
  }
}

Debes notar eso dir, widy disno se pasan mucho. También debe notar que el código en la función que lock()devuelve se parece mucho al pseudocódigo. Bueno, es el código real. Sin embargo, es muy fácil de leer. Podríamos agregar pases y asignaciones, pero eso agregaría un desorden que nunca verías en un pseudocódigo.

Si desea argumentar que no hacer la asignación y pasar está bien porque esos tres vars son de estado persistente, considere un rediseño que asigne dirun valor aleatorio a cada ciclo. No es persistente ahora, ¿verdad?

Claro, ahora podríamos dirreducir el alcance ahora, pero no sin estar obligados a abarrotar nuestro pseudo código similar con pasar y configurar.

Entonces, no, el estado no es la razón por la que decide usar o no usar los efectos secundarios en lugar de pasar y regresar. Tampoco los efectos secundarios solo significan que su código es ilegible. No obtienes los beneficios del código funcional puro . Pero bien hecho, con buenos nombres, en realidad pueden ser agradables de leer.

Eso no quiere decir que no puedan convertirse en un espagueti como una pesadilla. Pero entonces, ¿qué no puede?

Feliz caza de patos.

naranja confitada
fuente
1
Las funciones internas / externas son un paradigma diferente al que describe OP. - Estoy de acuerdo en que la compensación entre variables locales en una función externa contra argumentos pasados ​​a las funciones internas es similar a las variables privadas en una clase contra argumentos pasados ​​a funciones privadas. Sin embargo, si realiza esta comparación, la vida útil del objeto correspondería a una sola ejecución de la función, por lo que las variables en la función externa forman parte del estado persistente.
Taemyr
@Taemyr, el OP, describe un método privado llamado dentro de un constructor público que me parece muy interno. El estado no es realmente 'persistente' si lo pisa cada vez que ingresa a una función. A veces, el estado se usa como un lugar compartido donde los métodos pueden escribir y leer. No significa que deba persistir. El hecho de que sea persistente de todos modos no es algo de lo que tenga que depender.
candied_orange
1
Es persistente incluso si lo pisa cada vez que desecha el objeto.
Taemyr
1
Buenos puntos, pero expresarlos en JavaScript realmente ofusca la discusión. Además, si elimina la sintaxis JS, termina con el objeto de contexto que se pasa (el cierre de la función externa)
fdreger
1
@ Sdenham Estoy argumentando que hay una diferencia entre los atributos de una clase y los resultados intermedios transitorios de una operación. No son equivalentes. Pero uno puede ser almacenado en el otro. Ciertamente no tiene que ser así. La pregunta es si alguna vez debería ser. Sí, hay problemas semánticos y de por vida. También hay problemas de arity. No crees que sean lo suficientemente importantes. Esta bien. Pero decir que usar el nivel de clase para algo que no sea el estado del objeto es un modelado incorrecto, insiste en una forma de modelar. He mantenido un código profesional que lo hizo al revés.
candied_orange