Aparentemente, programar usando un diseño orientado a objetos significa hacer un dibujito UML, decirle a nuestro IDE que cree un montón de atributos (obviamente todos privados; no querríamos romper el encapsulamiento), y luego generar todos los accesores y mutadores, no vaya a ser que después no podamos cambiar algo.
Al final, terminamos con una clase Persona, a la que cualquiera le puede decir que se cambie el nombre, el DNI, y cualquier cosa que le corresponda.
Eh, no.
Preguntémosle a Alan Kay, a Barbara Liskov, a Bjarne Stroustrup… y tantos otros, y creo que lo primero que nos van a decir es “Eso no es una clase; es una estructura”, y “¿No se suponía que ibas a encapsular los atributos?”…
A una interfaz siempre se puede agregar más métodos, sin romper nada, pero si borramos el más inútil de nuestros métodos, siempre va a haber alguien que estaba usándolo. Por eso el primer paso a un buen diseño es «No agregar accesores hasta que se demuestre útil. No agregar mutadores hasta que sea totalmente necesario.».
Ah, y, de paso: Las Properties1 no son más que accesores y mutadores disfrazados.
Tell, Don’t Ask
Traduciendo libremente de The Art of Enbugging, “Supongamos que el diariero viene a la puerta a cobrar lo que le debés de la semana. Te das vuelta, y él saca tu billetera de tu bolsillo, toma dos dólares, y te la devuelve. En vez de eso, el diariero debería pedirle al cliente que le pague los $2,00.”.
Para ponerlo en código (porque sí):
Diariero::Cobrar(Cliente* cliente, int cantidad) throw (EsUnRataException) { Billetera* billetera = Cliente->getBilletera(); if(billetera->contenido() < cantidad) { cliente->setBilletera(billetera); throw EsUnRataException; } billetera.remove(cantidad); Cliente.setBilletera(billetera); }
vs.
Diariero::Cobrar(Cliente& cliente, int cantidad) throw (DineroInsuficienteException) { Cliente.cobrar(cantidad); }
Y el primero probablemente necesite revisar si el cliente tiene una billetera, ver si usa monedero o nada, cosa que nos obliga a agregar varios if donde podríamos haber usado polimorfismo.
¿Y mis porotos?
Los Beans (Objetos con accesores y mutadores para cada atributo, más un constructor sin parámetros) parecen ser la necesidad básica del programador de Java moderno (?). Todo tiene que ser un bean, si vamos a usar Hibernate, JPA o <inserte sigla de moda>.
Para muchos, eso es todo lo que se necesita para decidir que todos los mutadores deben ser públicos: ¡Si no no puedo usar Hibernate!… pero no. Hibernate pide que existan, pero no necesariamente que sean públicos: si son protegidos, es más que suficiente.
Of course…
Por supuesto, hay lugares donde los accesores son útiles, y mucho mejores que un constructor repleto de parámetros. Para los que no me crean, revisen la API de Windows:
m_hWindow = ::CreateWindow("AppWindow", /* class name */ m_pszTitle, /* title to window */ WS_OVERLAPPEDWINDOW, /* style */ CW_USEDEFAULT, /* start pos x */ CW_USEDEFAULT, /* start pos y */ m_nWidth, /* width */ m_nHeight, /* height */ NULL, /* parent HWND */ NULL, /* menu HANDLE */ hInstance, /* */ NULL); /* creatstruct param */
Todos los NULL y CW_USEDEFAULT podrían haber sido valores por defecto. The Little Manual of API Design, de los creadores de QT, ofrece una alternativa, más similar a la que se usa en QT4:
window = new Window; window->setClassName("AppWindow"); window->setWindowTitle(winTitle); window->setStyle(Window::Overlapped); window->setSize(width, height); window->setModuleHandle(moduleHandle);
Una opción indiscutiblemente mejor: Ocupa menos líneas, y no involucra memorizar el orden arbitrario de los 11 parámetros del constructor anterior. Podría parecer más larga de escribir, por todos los “window” que se repiten, pero podría reducirse aún más si la API2 soporta un estilo más fluido (cada método retorna this), permitiendo algo como:
window = new Window; window->setClassName("AppWindow") ->setWindowTitle(winTitle) ->setStyle(Window::Overlapped) ->setSize(width, height) ->setModuleHandle(moduleHandle);
…de lo cual más de un smalltalker se reirá.
Colecciones
Un lugar donde se nota obviamente la bestialidad de los accesores y mutadores es cuando hay colecciones de por medio. Primero, porque exponer una colección elimina cualquier posibilidad de concurrencia: Si dos métodos quieren modificarla, el único resultado va a ser un bug.
Además, se expone el tipo interno de la colección, removiendo otra de las características de la orientación a objetos: la implementación tiene libertad sobre la interfaz.
Acá hay que aplicar uno de los refactoreos de Fowler (y esto ya debería llegar a volverse regla natural): Encapsulate collection. Es simple: Primero, matamos el setter. No cumple ninguna función útil. Nos aseguramos, además, de que en el constructor se cree la colección. Después procedemos a liberar la responsabilidad de la colección: El getter retorna una copia inmutable. Y, finalmente, agregamos métodos que se encarguen de agregar o remover objetos de la colección.
Voilá: Nuestro objeto está un paso más cerca de ser, bueno, orientado a objetos.
Ley Anti-mutantes
Le debo una a William Stryker.
Una vez, discutiendo sobre OO con un amigo, le dije que todo debería ser inmutable, excepto en raros casos. Me dijo «¡Pero el chiste de la Orientación a Objetos es la mutabilidad del estado!».
Bueno, sí y no.
Más vale no.
Siguiendo con las colecciones, algunas son más críticas que otras. Si sólo tenemos un array de objetos, no importa que cualquiera de sus valores cambie. Ahora, si tenemos una hashtable, más vale que el hash nunca cambie, o van a pasar cosas muy malas.
Cualquier valor del que dependa hashCode (en Java) tiene que ser inmutable. Si no, estaríamos violando el contrato, y sufriremos mil y un bugs sutiles.
¡Bueno, basta, prometo no hacerlo más, pero dejá de escribir!
No todos los atributos de un objeto merecen un getter: Por cada getter exponemos implementación.
Muy pocos atributos merecen un setter: Si tiene que cambiar, que lo haga el objeto sólo.
Las interfaces deben revelar lo menos posible: No digas HashSet donde podés decir Collection.
Encapsulá tus colecciones.
El primer paso a una buena orientación a objetos es la paranoia.