martes, 22 de mayo de 2012

Setters y getters, friend or foe?

Esto me lo he preguntado alguna vez y Kpacha al final me convencía, aunque ahora no recuerdo sus argumentos. ¿Por qué quiero programar así?:

private String nombre;
public void setNombre(String nombre) { this.nombre = nombre; }
public String getNombre() { return nombre; }


En lugar de hacer simplemente esto:

private String nombre;

y acceder directamente a las propiedades cuando haga falta...¿?
Hay buenos motivos para usar esas funciones get, set, como por ejemplo:

  • Encapsulación: Así nos protegemos frente a cambios de implementación. Por ejemplo si decidimos que hay que verificar algo antes de hacer el set solo habrá que hacerlo en ese método.
  • Es necesario para que sean compatibles con clases como serialize.
  • Al heredar puedes modificar el funcionamiento.

Peeeeeroooo...

Realmente el uso de getters y setters sigue sin ser una buena práctica, no sigue la filosofía de la programación con objetos. ¿Por qué?
Uno de los principios que engrandecen la OOP es la abstracción de los datos. Un objeto no debe mostrar cómo funciona internamente, de ese modo siempre se puede modificar ese comportamiento sin afectar a los que hacen uso de él. Por eso las propiedades han de ser privadas como norma general.
Usar getters y setters son un rodeo para, en realidad, hacer pública una propiedad. Si por ejemplo tenemos que cambiar el tipo que devuelve el getCantidad de int a long tendremos que recorrer todo el código donde se use ese método para arreglar la recepción del dato. Esos datos no tendrían que ir pululando por ahí de clase en clase, en vez de pedir el dato para hacer algo, habría que decirle a la clase que tiene la información que lo haga.

En resumen, hay que tener todos los getters y setters si tienes pensado usar serialize o similares. En caso contrario hay que reducir al mínimo posible este tipo de accesos porque exponen el funcionamiento interno de la clase y por tanto hacen el código más difícil de mantener. Para conseguirlo hay que planificar previamente la visión global del funcionamiento de las clases y su interacción, es decir hacer bien las cosas desde el principio. ¡Qué fácil es decirlo!

Voy a avisar a Kpacha para que se pase por aquí a poner los puntos sobres las íes.

5 comentarios:

PPi dijo...

Yo hasta que el Uri no dé su visión sobre la programación orientada a ojetes, no pienso decir nada.

kpacha dijo...

sólo son necesarios en clases que requieran ofrecer una interfaz pública a ciertas propiedades (por ejemplo, una entidad que deba sincronizarse con la bbdd necesitaría los (g/s)etter).

Para casi todo el resto de casos no se necesitan pq uno de los objetivos de la POO es que las clases encapsulen lógica (aquí el link), ocultándola.

Eso si, los setter tb se utilizan para otras cosas como la inyección de dependencias, por ejemplo: para desacoplar clases haciendo que ninguna se instancie dentro de la otra. (ejemplo)

Y lo mejor de todo es que como se pueden extender y sobrecargar, puedes ir adaptando la funcionalidad que encapsula el (g/s)etter.

PD: si programas en java y con un ide como eclipse/netbeans, puedes hacer un refactor y que se propaguen los cambios por todas las referencias de forma automática

PD2: en php se puede hacer más sencillo: usando los magic methods. Eso quiere decir que, a no ser que vayas a hacer algo realmente interesante en el (g/s)etter, la clase tira del magic method que corresponda donde ya he programado el comportamiento por defecto (si alguien está interesado, cuelgo un ejemplo muy simple)

kpacha dijo...

En php, si haces que tus entidades (clases del modelo) extiendan de esta clase básica, sólo deben implementarse los (s/g)etter que hagan algo especial. En el ejemplo, sólo implemento el setter que formatea la fecha.

/**
* Base model class.
*
* The classes extended from this could expose their properties without explicit setter and getters declarations
*
* @author kpacha
*/
class BaseModel
{
/**
* Default constructor
*
* If an array is passed as argument, it tries to set every value as a property
*
* @param $data array
*/
function __construct ($data = null)
{
if ($data !== null && is_array($data)) {
$reflect = new ReflectionClass($this);
$props = $reflect->getProperties();
foreach ($props as $property) {
$propertyName = $property->getName();
$setter = 'set' . ucfirst($propertyName);
$this->{$setter}($data[$propertyName]);
}
}
}

/**
* Magic method to manage the calls to unexistent methods (like the basic setters and getters)
*
* @author kpacha
* @param string $methodName
* @param array $args
* @throws Exception
* @return BaseModel
*/
public function __call ($methodName, $args)
{
if (preg_match('~^(set|get)([A-Z])(.*)$~', $methodName, $matches)) {
$property = strtolower($matches[2]) . $matches[3];
if (! property_exists($this, $property)) {
throw new Exception("Property $property does not exist");
}
switch ($matches[1]) {
case 'set':
return $this->set($property, $args[0]);
case 'get':
return $this->get($property);
case 'default':
throw new Exception("Method $methodName does not exist");
}
}
}

/**
* Default global getter
*
* @author kpacha
* @param string $property
* @return mixed the value stored at the property
*/
protected function get ($property)
{
return $this->$property;
}

/**
* Default global setter
*
* @author kpacha
* @param string $property
* @param mixed $value
* @return BaseModel
*/
protected function set ($property, $value)
{
$this->$property = $value;
return $this;
}
}


/**
* @author kpacha
*/
class Activity extends BaseModel
{
protected $id;
protected $srcImg;
// ... the properties list
// protected as usual
protected $executionDate;
protected $conceptDate;

/**
* @param string $executionDate;
*/
public function setExecutionDate($executionDate)
{
$this->executionDate= $this->_formatClientTime($executionDate);
}

/**
* @param string $conceptDate;
*/
public function setConceptDate($conceptDate)
{
$this->conceptDate= $this->_formatClientTime($conceptDate);
}

protected function _formatClientTime($time)
{
$date = new DateTime($time);
return $date->format('d-m-Y');
}
}

Albert dijo...

Kpacha, te has dejado algo, porqué he hecho un ctrl+F y por "Hello World" no me viene nada.

kpacha dijo...

juas!

lo que me había dejado por decir es que, como todo en esta vida, el simplificar las cosas conlleva un coste: cada (g/s)etter no definido y redirigido al método genérico sobrecarga la ejecución...

como siempre, nadie da duros a cuatro pesetas!