Object Calisthenics en PHP – “Reglas de oro” para escribir mejor código orientado a objetos

Object Calisthenics – Reglas de oro para escribir mejor código orientado a objetos

Object Calisthenics es un artículo de Jeff Bay escribió hace bastante en el que nos cuenta una serie de reglas para escribir mejor código orientado a objetos.

El planteamiento de Jeff es sencillo: es fácil conocer todos los conceptos básicos que rodean a un buen diseño: cohesión, poco acoplamiento, legibilidad, tests,… sin embargo es tremendamente dificil poner estos conceptos en práctica, ya que una cosa es entender la encapsulación y otra muy distinta es implementarla.

La idea es tener una serie de reglas que nos ayuden a saber si nuestro código va por buen camino.

Este es el artículo original http://www.cs.helsinki.fi/u/luontola/tdd-2009/ext/ObjectCalisthenics.pdf intentaré hacer una pequeña traducción/adaptación utilizando PHP.

Las reglas

  1. One level of indentation per method
  2. Don’t use the ELSE keyword
  3. Wrap all primitives and Strings
  4. First class collections
  5. One dot per line
  6. Don’t abbreviate
  7. Keep all entities small
  8. No classes with more than two instance variables
  9. No getters/setters/properties

Como podéis ver estas reglas están pensadas para Java pero puede intentar hacer un “simil” a PHP

One level of indentation per method

Más de una vez te habrás encontrado delante de un método y habrá dicho ¿por donde empiezo? Un método muy grande sin cohesión. Una directriz es tener métodos pequeños pero si tenemos un método de 500 lineas es un poco difícil refactorizar.

A medida que empezamos a trabajar en métodos que solo hacen una cosa dentro de clases que hacen solo una cosa nuestro código empieza a mejorar debido sobre todo a la reutilización

function validate(array $products)
{
    $requiredFields = ['price', 'name', 'description'];
    $valid = true;
    foreach ($products as $rawProduct) {
        $fields = array_keys($rawProduct);
        foreach ($requiredFields as $requiredField) {
            if (!in_array($requiredField, $fields)) {
                $valid = false;
            }
        }
    }

    return $valid;
}

Es difícil reutilizar un método de 100 lineas que hace 5 cosas, es más fácil reutilizar un método de 10 lineas que solo hace 1

Lo primero que tenemos que hacer es utilizar en nuestro IDE favorito(PhpStorm) la función extraer método para llegar a 1 solo nivel de sangría.

function validate(array $products)
{
    $requiredFields = ['price', 'name', 'description'];
    $valid = true;
    foreach ($products as $rawProduct) {
        $valid = $valid && $this->validateSingle($rawProduct, $requiredFields);
    }

    return $valid;
}

function validateSingle(array $product, array $requiredFields)
{
    $fields = array_keys($product);

    return count(array_diff($requiredFields, $fields)) == 0;
}

Don’t use the ELSE keyword

Quizás es con la regla que más en desacuerdo estoy, pero en algunos casos puede ayudar mucho. Esta regla dice que todos los programadores entiende la estructura if/else. Tambiés es cierto que todos los programadores hemos visto if/else anidados que son imposibles de entender y lo peor es que es mucho más fácil añadir otra condición más al if que refactorizar por lo que los if se convierten en una fuente de código duplicado.

Partamos de este ejemplo:

function do_stuff() {

// ...

    if (is_writable($folder)) {

        if ($fp = fopen($file_path,'w')) {

            if ($stuff = get_some_stuff()) {

                if (fwrite($fp,$stuff)) {

                    // ...

                } else {
                    return false;
                }
            } else {
                return false;
            }
        } else {
            return false;
        }
    } else {
        return false;
    }
}

¿que tal si lo hacemos así?

function do_stuff() {

// ...

    if (!is_writable($folder)) {
        return false;
    }

    if (!$fp = fopen($file_path,'w')) {
        return false;
    }

    if (!$stuff = get_some_stuff()) {
        return false;
    }

    if (fwrite($fp,$stuff)) {
        // ...
    } else {
        return false;
    }
}

Queda más claro y es más fácil de seguir.

Quizás este sea un ejemplo un poco extremo, pero seguro que si miras tu código encontraras unas cuantas maneras hacer más sencillos los caminos de código eliminando else.

Wrap all primitives and Strings

Esta regla quizás no sea muy Phpera pero la idea detrás de ella puede servirnos. Se trata de encapsular validaciones en objetos para prevenir el código duplicado. Con esto conseguimos “type hinting”.

Vamos a explicarlo con un ejemplo, imaginemos que tenemos una clase Item que tiene un método find al que se le pasa un id y un método create al que también se le pasa el id, seguramente la validación de parámetros esté duplicada:

class Item
{
    final public static function find($id)
    {
        if (!is_string($id) || trim($id) == '') {
            throw new InvalidArgumentException('$id must be a non-empty string');
        } // do find ...
    }

    final public static function create($id, array $data)
    {
        if (!is_string($id) || trim($id) == '') {
            throw new InvalidArgumentException('$id must be a non-empty string');
        } // do create ...
    }
}

¿Como solucionarlo? Creando una clase Id que sea la que realice las validaciones y usar type hiting en los parámetros de las funciones:

class Id
{
    private $value;

    final public function __construct($value)
    {
        if (!is_string($value) || trim($value) == '') {
            throw new InvalidArgumentException('$value must be a non-empty string');
        }
        $this->value = $value;
    }

    final public function getValue()
    {
        return $this->value;
    }
}

Así quedaría nuestra nueva clase Item

class Item
{
    final public static function find(Id $id)
    { // do find ...
    }

    final public static function create(Id $id, array $data)
    { // do create ...

    }
}

Con esto mejoramos en reutilización pero debemos tener cuidados si utilizamos demasiados objetos (objetos para todo) el uso de la memoria PHP se disparará.

First class collections

Es una regla simple: Cualquier clase que contenga una “Collection”(array en PHP) no puede contener otras propiedades. Puede parecer que esta regla está en confrontación con otras del post, no te preocupes, con un ejemplo veremos en acción a esta regla:

class User
{
    private $name;
    // ...
    private $albumList;

    public function getPublicAlbumList()
    {
        $filteredAlbumList = array();
        foreach ($this->albumList as $album) {
            if ($album->getPrivacy() === AlbumPrivacy::PUBLIC) {
                $filteredAlbumList[] = $album;
            }
        }

        return $filteredAlbumList;
    }  
    // ...

}  
// Ejemplo:
$publicAlbumList = $user->getPublicAlbumList();

Podemos dejarlo como esto:

class AlbumList extends Collection
{
    public function getPublic()
    {
        return new ArrayCollection(
            array_filter(
                $this->value,
                function ($album) {
                    return $album->isPublic();
                }
            )
        );
    }
}

class User
{
    private $name;
    private $albumList = new AlbumList();
    // ... 
}

// Ejemplo:
$publicAlbumList = $user->getAlbumList()->getPublic();

La idea es utilizar Colletcion, Iterator,… es decir utilizar interfaces SPL para apoyarnos en sus métodos.

One dot per line

Esta regla es de Java, pero puede aplicarse a PHP cambiando “.” por “->”
A veces es complicado saber que objeto está asumiendo una responsabilidad, cuantos más puntos flechas más repartida está esa responsabilidad. Esos puntos demás nos indican que la encapsulación no es la adecuada.

Aplicar La Ley de Demeter (solo habla con tus amigos) puede ser un buen comienzo.

$user->getLocationPoint()->getCountry()->getName();

¿Por qué es bueno todo esto?, Utilizando pocos puntosflechas nos será más fácil mockear y por tanto más sencillo de testear y si surge algún inconveniente no tendremos problemas al debugear

$filterChain
    ->addFilter(new Zend_Filter_Alpha())
    ->addFilter(new Zend_Filter_StringToLower());

Don’t abbreviate

A menudo es tentador abreviar los nombres de las clases, los métodos o las variables. Tenemos que resistir la tentación, a la larga las abreviaturas son confusas.

¿Por qué abreviamos? Acaso no tenemos autocompletado en nuestro IDE, o es que no tenemos limitado el número de caracteres por linea. Puede que sea porque utilizamos demasiado una palabra, esto puede ser un signo de duplicación.

if ($sx >= $sy) {
    if ($sx > $strSysMatImgW) {
        $ny = $strSysMatImgW * $sy / $sx;
        $nx = $strSysMatImgW;
    }
    if ($ny > $strSysMatImgH) {
        $nx = $strSysMatImgH * $sx / $sy;
        $ny = $strSysMatImgH;
    }
} else {
    if ($sy > $strSysMatImgH) {
        $nx = $strSysMatImgH * $sx / $sy;
        $ny = $strSysMatImgH;
    }
    if ($nx > $strSysMatImgW) {
        $ny = $strSysMatImgW * $sy / $sx;
        $nx = $strSysMatImgW;
    }
}

¿Entiendes algo?
De la misma manera tener métodos demasiado largos puede ser contraproducente tener un nombre de método con la palabra and es contraproducente denota la no separación de responsabilidad

function proccessResponseHeaderAndDefineOutput

Ganamos mucho manteniendo buenos nombres, ganamos en legibilidad, en comprensión.

Keep all entities small

Las clases demasiado grandes a menudo suelen hacer más de una cosa, lo cual las hace más difíciles de mantener y reutilizar

  • 15 – 20 lineas por método
  • 200 lineas por clase incluyendo docblock
  • 10 métodos por clase
  • 10 clases por namespace

Son unos números orientativos, pero con ello conseguimos acercarnos a los principios SOLID, tener una única responsabilidad por clase/método y claridad en los métodos.

No classes with more than two instance variables

La mayoría de nuestras clases solo deberían ser responsables de manejar de 2 a 5 variables de instancia en PHP. Cuanta más variables de instancia haya en una clase más disminuiremos su cohesión.

class Client
{
    protected $adapter;
    protected $cache;
    protected $logger;

    //...
}

Con esto nuestras clases serán más fáciles de mockear y nuestros tests unitarios más sencillos.

No getters/setters/properties

Esto NO es aplicable a PHP. Hay que utilizar siempre getter y setter

class BankAccount
{
    public $balance = 0;

    public function deposit($amount)
    {
        $this->balance += $amount;
    }

    public function withdraw($amount)
    {
        $this->balance -= $amount;
    }
}

// Ejemplo:
$account = new BankAccount();
$account->deposit(100.00);  

// ...

$account->balance = 0;  // ¡NO!

// ...

$account->withdraw(10.00);

echo $account->balance . PHP_EOL;

En este ejemplo vemos como el balance puede ser alterado causando errores.

Utilizando getter y setter mejoramos nuestro código haciéndolo más “abierto” (O de SOLID).

Conclusiones

Creo que intentar aplicar estas reglas nos ayudarán tener un mejor código, más mantenible y testeable.

Para ayudarnos con estas reglas y no tener que memorizarlas podemos utilizar estas reglas https://github.com/object-calisthenics/phpcs-calisthenics-rules para CodeSniffer

Referencias

Anuncios

4 comentarios en “Object Calisthenics en PHP – “Reglas de oro” para escribir mejor código orientado a objetos

  1. Muy bueno Jesus.
    Yo lo descubri hace “poco” limpiando el historial.
    Dos puntos.
    1. Lo de if/else a mi si me gusta. El ejemplo que pones es perfecto pero yo lo estoy aplicando a una aplicacion java y al debugar vas descartando opciones en cuanto se presentan. Es como decir “a la minima te vas a la calle”
    2. Lo de los get/set en PHP ayuda. Creo que lo dice por Java porque un JavaBean es una clase con set/get para todos y es contraproducente tener tanto metodo que luego ni usas y no pasa por ninguna logica.
    Me apunto el comprobador phpcs. Saludos

    Me gusta

Comenta la entrada

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s