Lunes,
22 de Febrero 2010
Artículo en inglés

Reformulando codigo php con demasiados niveles anidados


Demasiada Indentación Al programar escribir líneas cortas y comprensibles tiene muchas ventajas:

  • Podemos ver toda la línea sin hacer scroll.
  • Nos fuerza a simplificar el enfoque de algún modo.
  • Para otras utilidades como diff nos permite ver varios documentos completos a la vez.

Ejemplo

Esta función de ejemplo acepta un elemento de tipo array o var y un parámetro opcional de tipo boolean. La función devuelve el valor multiplicado por 2. En caso de ser un array duplica el valor para cada uno de los elementos. Si el parámetro opcional es verdadero (true) devuelve el valor original y el duplicado en un array.

function duplicate($elem, $return_both= false) {
    if(is_array($elem)) {
        $ret= array();
        foreach($elem as $val) {
            if($return_both) {
                $ret[]= array($val*2,$val);
            } else {
                $ret[]= $val*2;
            }
        }
        return $ret;
    } else {
        if($return_both) {
            return array($elem*2,$elem);
        } else {
            return $elem*2;
        }
    }
}

Podemos escribir este código de varias maneras diferentes para simplificarlo y que sea más fácil de entender.

Tipos

Podemos observar que parte de la complejidad del código proviene del hecho de que $elem puede ser un array u otro tipo. Vamos a hacer que sea siempre un array.

function duplicate($elem, $return_both= false) {
    if(!is_array($elem)) {
        $elem= array($elem);
    }

    $ret= array();
    foreach($elem as $val) {
        if($return_both) {
            $ret[]= array($val*2,$val);
        } else {
            $ret[]= $val*2;
        }
    }
    return $ret;
}

En este caso habríamos cambiado la función, ya que si pasamos un elemento nos devuelve un array. Podríamos cambiarlo así para que devuelva el elemento solamente:

function duplicate($elem, $return_both= false) {
    if(!is_array($elem)) {
        $elem= array($elem);
    }

    $ret= array();
    foreach($elem as $val) {
        if($return_both) {
            $ret[]= array($val*2,$val);
        } else {
            $ret[]= $val*2;
        }
    }

    if(count($ret) > 1) {
        return $ret;
    } else {
        return $ret[0];
    }
}

De este modo sólo retornamos el primer elemento si tiene un elemento solamente.

Múltiples retornos

Una manera de evitar mucha indentación es usar varios returns. Aunque usar un sólo return tiene sus ventajas, en algunas ocasiones es más conveniente.

function duplicate($elem, $return_both= false) {
    if(!is_array($elem)) {
        $elem= array($elem);
    }

    $ret= array();
    foreach($elem as $val) {
        if($return_both) {
            $ret[]= array($val*2,$val);
        } else {
            $ret[]= $val*2;
        }
    }

    if(count($ret) == 1) {
        return $ret[0];
    }

    return $ret;
}

Lógica de condicionales

Reorganzizar las condiciones es una buena manera de cambiar el enfoque de un problema. También podemos abreviar algunas condiciones de forma muy clara con el operador ternario

function duplicate($elem, $return_both= false) {
    $elem= is_array($elem)? $elem : array($elem);

    $ret= array();
    foreach($elem as $val) {
        if($return_both) {
            $ret[]= array($val*2,$val);
        } else {
            $ret[]= $val*2;
        }
    }

    $ret= count($ret) == 1? $ret[0]: $ret;
    return $ret;
}

Funciones que trabajan sobre arrays

PHP dispone de muchas funciones que utilizan arrays y facilitan mucho el trabajo. Podemos utilizarlas para sustituir algunos bucles for y similares. En este caso utilizamos array_map para procesar los elementos del array.

function dup($v) {
    return array($v*2, $v);
}
function get_first($v) {
    return $v[0];
}
function duplicate($elem, $return_both= false) {
    $elem= is_array($elem)? $elem : array($elem);

    $ret= array();
    $b = array_map('dup', $elem);
    //Ahora $b tiene un array de arrays

    if($return_both) {
        $ret= $b;
    } else {
        $ret= array_map('get_first',$b);
    }

    $ret= count($ret) == 1? $ret[0]: $ret;
    return $ret;
}

Para no recorrer el array varias veces procesamos todo de una pasada y luego filtramos.

funciones anónimas (o lambda)

Hemos creado una función dup para utilizar en la función duplicate. En realidad lo que queríamos era una función de usar y tirar a la que no haya acceso desde otro código. A partir de la versión 5.3 de PHP podemos usar funciones lambda. De este modo no "contaminamos" el espacio de nombres.

function duplicate($elem, $return_both= false) {
    $elem= is_array($elem)? $elem : array($elem);
    $ret= array();

    $dup= function($v) { return array($v*2, $v); };
    $get_first= function($v) { return $v[0]; };

    $b= array_map($dup, $elem);

    if($return_both) {
        $ret= $b;
    } else {
        $ret= array_map($get_first,$b);
    }

    $ret= count($ret) == 1? $ret[0]: $ret;
    return $ret;
}

Volviendo a revisar los condicionales podemos mejorarlo con:

function duplicate($elem, $return_both= false) {
    $elem= is_array($elem)? $elem : array($elem);
    $ret= array();

    $dup= function($v) { return array($v*2, $v); };
    $get_first= function($v) { return $v[0]; };

    $ret= array_map($dup, $elem);
    if(!$return_both) {
        $ret= array_map($get_first,$b);
    }

    $ret= count($ret) == 1? $ret[0]: $ret;
    return $ret;
}

Otros enfoques

Estas opciones son sólo sugerencias que dependerán del estilo y del tipo de problema a tratar. En la variedad está el gusto. Otra opción, p.ej. sería este pequeño hack:

function duplicate($elem, $return_both= false) {
    //así pasamos varios parámetros
    $c= array($elem,$return_both);

    switch($c) {
        case (is_array($c[0])):
        case ($c[1]):
            foreach($elem as $val) {
                $ret[]= array($val*2,$val);
            }
            return $ret;
        case (!$c[1]):
            foreach($elem as $val) {
                $ret[]= $val*2;
            }
            return $ret;
        break;
        case (!is_array($c[0])):
        case ($c[1]):
            return array($elem*2,$elem);
        case (!$c[1]):
            return $elem*2;
        break;
    }
}

En este caso nos aprovechamos del hecho de que sin un break después de case se siguen ejecutando instrucciones en cascada y del paso de varios parámetros a un switch. En este último caso tenemos que pensar que dentro de unos meses nosotros u otra persona tiene que ser capaz de entender claramente el código.

blog comments powered by Disqus
Bookmark and Share