Variables temporales vs requisitos de longitud de línea

10

He estado leyendo Refactorización de Martin Fowler . En general, es excelente, pero una de las recomendaciones de Fowler parece estar causando algunos problemas.

Fowler recomienda que reemplace las variables temporales con una consulta, así que en lugar de esto:

double getPrice() {
    final int basePrice = _quantity * _itemPrice;
    final double discountFactor;
    if (basePrice > 1000) discountFactor = 0.95;
    else discountFactor = 0.98;
    return basePrice * discountFactor;
}

sacas un método auxiliar:

double basePrice() {
    return _quantity * _itemPrice;
}

double getPrice() {
    final double discountFactor;
    if (basePrice() > 1000) discountFactor = 0.95;
    else discountFactor = 0.98;
    return basePrice() * discountFactor;
}

En general, estoy de acuerdo, excepto que una razón por la que uso variables temporales es cuando una línea es demasiado larga. Por ejemplo:

$host = 'https://api.twilio.com';
$uri = "$host/2010-04-01/Accounts/$accountSid/Usage/Records/AllTime";
$response = Api::makeRequest($uri);

Si intentara incluir eso, la línea tendría más de 80 caracteres.

Alternativamente, termino con cadenas de código, que en sí mismas no son mucho más fáciles de leer:

$params = MustacheOptions::build(self::flattenParams($bagcheck->getParams()));

¿Cuáles son algunas estrategias para conciliar los dos?

Kevin Burke
fuente
10
80 caracteres es aproximadamente 1/3 de 1 de mis monitores. ¿estás seguro de que vale la pena seguir 80 líneas de carbón?
jk.
10
sí, ver por ejemplo programmers.stackexchange.com/questions/604/…
Kevin Burke
Tu $hosty el $uriejemplo es una especie de urdido, sin embargo - a menos que el anfitrión se están leyendo desde un entorno u otra entrada, preferiría que estar en la misma línea, incluso si lo hace envoltura o ir fuera del borde.
Izkata
55
No es necesario ser tan dogmático. El libro es una lista de técnicas que se pueden usar cuando ayudan, no un conjunto de reglas que debe aplicar en todas partes, siempre. El punto es hacer que su código sea más fácil de mantener y más fácil de leer. Si un refactor no hace esto, no lo usa.
Sean McSomething
Si bien creo que un límite de 80 caracteres es un poco excesivo, un límite similar (¿100?) Es razonable. Por ejemplo, a menudo me gusta programar en monitores orientados a retratos, por lo que las líneas extra largas pueden ser molestas (al menos si son comunes).
Thomas Eding

Respuestas:

16

Cómo
1. Existen restricciones de longitud de línea para que pueda ver + comprender más código. Aún son válidos.
2. Enfatice el juicio sobre la convención ciega .
3. Evite las variables temporales a menos que optimice el rendimiento .
4. Evite el uso de sangría profunda para la alineación en declaraciones de varias líneas.
5. Divida las declaraciones largas en varias líneas a lo largo de los límites de la idea :

// prefer this
var distance = Math.Sqrt(
    Math.Pow(point2.GetX() - point1.GetX(), 2) + // x's
    Math.Pow(point2.GetY() - point1.GetY(), 2)   // y's
);

// over this
var distance = Math.Sqrt(Math.Pow(point2.GetX() -
    point1.GetX(), 2) + Math.Pow(point2.GetY() -
    point1.GetY(), 2)); // not even sure if I typed that correctly.

Razonamiento
La fuente principal de mis problemas (depuración) con variables temporales es que tienden a ser mutables. Es decir, supondré que son un valor cuando escribo el código, pero si la función es compleja, algún otro fragmento de código cambia su estado a la mitad. (O al contrario, donde el estado de la variable permanece igual pero el resultado de la consulta ha cambiado).

Considere atenerse a las consultas a menos que esté optimizando el rendimiento . Esto mantiene cualquier lógica que haya utilizado para calcular ese valor en un solo lugar.

Los ejemplos que ha dado (¿Java y ... PHP?) Permiten enunciados de varias líneas. Si las líneas se alargan, sepárelas. La fuente jquery lleva esto a los extremos. (¡La primera declaración corre a la línea 69!) No es que esté necesariamente de acuerdo, pero hay otras maneras de hacer que su código sea legible que usar temp vars.

Algunos ejemplos
1. Guía de estilo PEP 8 para python (no es el ejemplo más bonito)
2. Paul M Jones en la Guía de estilo Pear (argumento del medio del camino)
3. Longitud de línea de Oracle + convenciones de envoltura (estratagemas útiles para mantener 80 caracteres)
4. MDN Java Practices (enfatiza el juicio del programador sobre la convención)

Zachary Yates
fuente
1
La otra parte del problema es que una variable temporal a menudo supera su valor. No es un problema en los bloques de alcance pequeño, pero en los más grandes, sí, es un gran problema.
Ross Patterson
8
Si le preocupa la modificación temporal, coloque una constante sobre ella.
Thomas Eding
3

Creo que el mejor argumento para usar métodos auxiliares en lugar de variables temporales es la legibilidad humana. Si usted, como humano, tiene más problemas para leer la cadena de métodos auxiliares que el varialbe temporal, no veo ninguna razón por la que deba extraerlos.

(Por favor corrígeme si estoy equivocado)

mhr
fuente
3

No creo que deba seguir estrictamente las pautas de 80 caracteres o que se deba extraer alguna variable temporal local. Pero las largas colas y las temperaturas locales deben investigarse para encontrar mejores formas de expresar la misma idea. Básicamente, indican que una función o línea dada es demasiado complicada y necesitamos desglosarla. Pero debemos ser cuidadosos, porque romper una tarea en partes malas solo complica la situación. Así que debo dividir las cosas en componentes valiosos y simples.

Déjame ver los ejemplos que publicaste.

$host = 'https://api.twilio.com';
$uri = "$host/2010-04-01/Accounts/$accountSid/Usage/Records/AllTime";
$response = Api::makeRequest($uri);

Mi observación es que todas las llamadas twilio api comenzarán con "https://api.twilio.com/2010-04-1/", y por lo tanto hay una función reutilizable muy obvia:

$uri = twilioURL("Accounts/$accountSid/Usage/Records/AllTime")

De hecho, creo que la única razón para generar una URL es hacer la solicitud, por lo que haría:

$response = TwilioApi::makeRequest("Accounts/$accountSid/Usage/Records/AllTime")

De hecho, muchas de las URL realmente comienzan con "Cuentas / $ accountSid", por lo que probablemente también extraería eso:

$response = TwilioApi::makeAccountRequest($accountSid, "Usage/Records/AllTime")

Y si hacemos de la twilio api un objeto que contenga el número de cuenta, podríamos hacer algo como:

$response = $twilio->makeAccountRequest("Usage/Records/AllTime")

El uso de un objeto $ twilio tiene la ventaja de facilitar las pruebas unitarias. Puedo darle al objeto un objeto $ twilio diferente que en realidad no llama a twilio, que será más rápido y no hará cosas extrañas a twilio.

Miremos al otro

$params = MustacheOptions::build(self::flattenParams($bagcheck->getParams()));

Aquí pensaría en cualquiera de los dos:

$params = MustacheOptions::buildFromParams($bagcheck->getParams());

o

$params = MustacheOptions::build($bagcheck->getFlatParams());

o

$params = MustacheOptions::build(flatParams($backCheck));

Dependiendo de cuál es el idioma más reutilizable.

Winston Ewert
fuente
1

En realidad, no estoy de acuerdo con el eminente Sr. Fowler sobre esto en el caso general.

La ventaja de extraer un método del código anteriormente incorporado es la reutilización del código; el código en el método ahora está separado de su uso inicial, y ahora puede usarse en otros lugares del código sin ser copiado y pegado (lo que requeriría hacer cambios en varios lugares si la lógica general del código copiado alguna vez tuviera que cambiar) .

Sin embargo, de igual valor conceptual, a menudo mayor es la "reutilización del valor". El Sr. Fowler llama a estos métodos extraídos para reemplazar las variables temporales "consultas". Bueno, lo que es más eficiente; consultar una base de datos cada una de las múltiples veces que necesita un valor particular, o consultar una vez y almacenar el resultado (suponiendo que el valor sea lo suficientemente estático como para que no espere que cambie).

Para casi cualquier cálculo más allá del relativamente trivial en su ejemplo, en la mayoría de los idiomas es más barato almacenar el resultado de un cálculo que seguir calculándolo. Por lo tanto, la recomendación general de recalcular a pedido es falsa; Cuesta más tiempo de desarrollador y más tiempo de CPU, y ahorra una cantidad trivial de memoria, que en la mayoría de los sistemas modernos es el recurso más barato de esos tres.

Ahora, el método auxiliar, junto con otro código, podría hacerse "vago". Sería, cuando se ejecuta por primera vez, inicializar una variable. Todas las llamadas posteriores devolverán esa variable hasta que se indique explícitamente al método que vuelva a calcular. Esto podría ser un parámetro para el método, o una marca establecida por otro código que cambia cualquier valor que el cálculo de este método depende de:

double? _basePrice; //not sure if Java has C#'s "nullable" concept
double basePrice(bool forceCalc)
{
   if(forceCalc || !_basePrice.HasValue)
      return _basePrice = _quantity * _itemPrice;
   return _basePrice.Value;
}

Ahora, para este cálculo trivial, es incluso más trabajo realizado que guardado, por lo que generalmente recomiendo seguir con la variable temporal; sin embargo, para cálculos más complejos que generalmente desearía evitar ejecutar varias veces, y que necesita en varios lugares del código, esta es la forma en que lo haría.

KeithS
fuente
1

Los métodos auxiliares tienen un lugar, pero debe tener cuidado al garantizar la coherencia de los datos y el aumento innecesario del alcance de las variables.

Por ejemplo, su propio ejemplo cita:

double getPrice() {
    final double discountFactor;
    if (basePrice() > 1000) discountFactor = 0.95;      <--- first call
    else discountFactor = 0.98;
    return basePrice() * discountFactor;                <--- second call
}

Es evidente que tanto _quantityy _itemPriceson variables globales (o por lo menos a nivel de clase) y por lo tanto existe un potencial para que sean fuera modificada degetPrice()

¡Por lo tanto, existe la posibilidad de que la primera llamada basePrice()devuelva un valor diferente de la segunda llamada!

Por lo tanto, sugeriría que las funciones auxiliares pueden ser útiles para aislar las matemáticas complejas, pero como reemplazo de las variables locales, debe tener cuidado.


También debe evitar la reducción ad absurdum : ¿debería el cálculo de discountFactorderivarse a un método? Entonces su ejemplo se convierte en:

double getPrice()
{
    final double basePrice      = calculateBasePrice();
    final double discountFactor = calculateDiscount( basePrice );

    return basePrice * discountFactor;
}

Particionar más allá de cierto nivel en realidad hace que el código sea menos legible.

Andrés
fuente
+1 para hacer que el código sea menos legible. El particionamiento excesivo puede ocultar el problema comercial que el código fuente está tratando de resolver. Podría haber casos especiales en los que se aplica un cupón en getPrice (), pero si eso está oculto en una cadena de llamadas de función, la regla de negocio también está oculta.
Reactgular
0

Si trabaja en un lenguaje con parámetros con nombre (ObjectiveC, Python, Ruby, etc.), las variables temporales son menos útiles.

Sin embargo, en su ejemplo de basePrice, la consulta puede tardar un tiempo en ejecutarse y es posible que desee almacenar el resultado en una variable temporal para su uso futuro.

Sin embargo, al igual que usted, uso variables temporales para consideraciones de claridad y longitud de línea.

También he visto a programadores hacer lo siguiente en PHP. Es interesante y genial para la depuración, pero es un poco extraño.

$rs = DB::query( $query = "SELECT * FROM table" );
if (DEBUG) echo $query;
// do something with $rs
Dimitry
fuente
0

La razón detrás de esta recomendación es que desea poder utilizar la misma precomputación en otra parte de su aplicación. Consulte Reemplazar temp con consulta en el catálogo de patrones de refactorización:

El nuevo método se puede utilizar en otros métodos.

    double basePrice = _quantity * _itemPrice;
    if (basePrice > 1000)
        return basePrice * 0.95;
    else
        return basePrice * 0.98;

           http://i.stack.imgur.com/mKbQM.gif

    if (basePrice() > 1000)
        return basePrice() * 0.95;
    else
        return basePrice() * 0.98;
...
double basePrice() {
    return _quantity * _itemPrice;
}

Por lo tanto, en su ejemplo de host y URI, solo aplicaría esta recomendación si planeo reutilizar la misma definición de URI o host.

Si este es el caso, debido al espacio de nombres, no definiré un método global uri () o host (), sino un nombre con más información, como twilio_host () o archive_request_uri ().

Luego, para el problema de longitud de línea, veo varias opciones:

  • Crea una variable local, como uri = archive_request_uri().

Justificación: en el método actual, desea que el URI sea el mencionado. La definición de URI todavía está factorizada.

  • Definir un método local, como uri() { return archive_request_uri() }

Si usa a menudo la recomendación de Fowler, sabría que el método uri () es el mismo patrón.

Si debido a la elección del idioma necesita acceder al método local con un 'self', recomendaría la primera solución, para una mayor expresividad (en Python, definiría la función uri dentro del método actual).

Marc-Emmanuel Coupvent des Gra
fuente