Cada entrada en los cuatro volúmenes — técnicas de refactorización, patrones de diseño, smells de código y patrones arquitectónicos — agrupados por familia.
Restructuración controlada del código existente: preserva el comportamiento, mejora el diseño.
Problema: Un método no tiene suficientes datos para realizar ciertas acciones. Solución: Crea un nuevo parámetro para pasar los datos necesarios.
Problema: Un método no es utilizado por otras clases o se utiliza únicamente dentro de su propia jerarquía de clases. Solución: Haz el método privado o protegido.
Problema: Tus métodos contienen un grupo de parámetros que se repite. Solución: Sustituye estos parámetros por un objeto.
Problema: Varios métodos realizan acciones similares que solo se diferencian en sus valores internos, números u operaciones. Solución: Combina estos métodos utilizando un parámetro que pase el valor especial necesario.
Problema: Obtienes varios valores de un objeto y después los pasas como parámetros a un método. Solución: En lugar de eso, intenta pasar el objeto completo.
Problema: Un parámetro no se utiliza en el cuerpo de un método. Solución: Elimina el parámetro no utilizado.
Problema: El valor de un campo debería establecerse solo cuando se crea, y no cambiar en ningún momento posterior. Solución: Por lo tanto, elimina los métodos que establecen el valor del campo.
Problema: El nombre de un método no explica lo que hace. Solución: Renombra el método.
Problema: tienes un constructor complejo que hace algo más que limitarse a asignar los valores de los parámetros a los campos del objeto. Solución: crea un método de fábrica y úsalo para reemplazar las llamadas al constructor.
Problema: ¿Un método devuelve un valor especial que indica un error? Solución: Lanza una excepción en su lugar.
Problema: ¿Lanzas una excepción en un lugar donde una simple comprobación haría el trabajo? Solución: Reemplaza la excepción con una comprobación de condición.
Problema: Un método está dividido en partes, cada una de las cuales se ejecuta dependiendo del valor de un parámetro. Solución: Extrae las partes individuales del método en sus propios métodos y llámalos en lugar del método original.
Problema: Llamas a un método de consulta y pasas sus resultados como parámetros de otro método, cuando ese método podría llamar a la consulta directamente. Solución: En lugar de pasar el valor a través de un parámetro, intenta colocar una llamada a la consulta dentro del cuerpo del método.
Problema: ¿Tienes un método que devuelve un valor pero también cambia algo dentro de un objeto? Solución: Divide el método en dos métodos independientes. Como es de esperar, uno de ellos debe devolver el valor y el otro modifica el objeto.
Problema: Tienes una asociación bidireccional entre clases, pero una de las clases no usa las características de la otra. Solución: Elimina la asociación no utilizada.
Problema: Tienes un objeto de referencia demasiado pequeño y que cambia con poca frecuencia como para justificar la gestión de su ciclo de vida. Solución: Conviértelo en un objeto de valor.
Problema: Tienes dos clases que necesitan usar cada una las características de la otra, pero la asociación entre ellas es únicamente unidireccional. Solución: Añade la asociación que falta a la clase que la necesita.
Problema: Tienes muchas instancias idénticas de una misma clase que necesitas reemplazar con un único objeto. Solución: Convierte los objetos idénticos en un único objeto de referencia.
Problema: ¿Se almacenan datos de dominio en clases responsables de la GUI? Solución: En ese caso, conviene separar los datos en clases independientes, asegurando la conexión y sincronización entre la clase de dominio y la GUI.
Problema: Una clase contiene un campo de colección y un getter y un setter simples para trabajar con la colección. Solución: Haz que el valor devuelto por el getter sea de solo lectura y crea métodos para añadir o eliminar elementos de la colección.
Problema: Tienes un campo público. Solución: Haz que el campo sea privado y crea métodos de acceso para él.
Problema: Tienes un array que contiene varios tipos de datos. Solución: Reemplaza el array con un objeto que tendrá campos separados para cada elemento.
Problema: Una clase (o grupo de clases) contiene un campo de datos. El campo tiene su propio comportamiento y datos asociados. Solución: Crea una nueva clase, coloca en ella el antiguo campo y su comportamiento, y almacena el objeto de la clase dentro de la clase original.
Problema: Tu código usa un número que tiene cierto significado. Solución: Reemplaza ese número por una constante con un nombre legible que explique el significado del número.
Problema: Tienes subclases que se diferencian únicamente en sus métodos (que devuelven constantes). Solución: Reemplaza los métodos con campos en la clase padre y elimina las subclases.
Problema: Una clase tiene un campo que contiene un código de tipo. Los valores de este tipo no se usan en condiciones de operadores y no afectan al comportamiento del programa. Solución: Crea una nueva clase y usa sus objetos en lugar de los valores del código de tipo.
Problema: tienes un tipo codificado que afecta al comportamiento pero no puedes usar subclases para deshacerte de él. Solución: reemplaza el código de tipo por un objeto de estado. Si es necesario reemplazar el valor de un campo con código de tipo, se «conecta» otro objeto de estado.
Problema: tienes un tipo codificado que afecta directamente al comportamiento del programa (los valores de este campo disparan distinto código en condicionales). Solución: crea subclases para cada valor del tipo codificado. Luego extrae los comportamientos relevantes de la clase original a estas subclases. Reemplaza el código de control de flujo con polimorfismo.
Problema: Usas acceso directo a campos privados dentro de una clase. Solución: Crea un getter y un setter para el campo, y úsalos únicamente a ellos para acceder al campo.
Problema: Tienes una jerarquía de clases en la que una subclase es prácticamente igual que su superclase. Solución: Fusiona la subclase y la superclase.
Problema: Varios clientes utilizan la misma parte de la interfaz de una clase. Otro caso: parte de la interfaz de dos clases es idéntica. Solución: Mueve esa porción idéntica a su propia interfaz.
Problema: Una clase tiene características que solo se usan en determinados casos. Solución: Crea una subclase y úsala en esos casos.
Problema: Tienes dos clases con campos y métodos comunes. Solución: Crea una superclase compartida para ellas y mueve a ella todos los campos y métodos idénticos.
Problema: Tus subclases implementan algoritmos que contienen pasos similares en el mismo orden. Solución: Mueve la estructura del algoritmo y los pasos idénticos a una superclase, y deja la implementación de los pasos diferentes en las subclases.
Problema: tus subclases tienen constructores con código que es casi idéntico. Solución: crea un constructor en la superclase y traslada a él el código que es igual en las subclases. Llama al constructor de la superclase desde los constructores de las subclases.
Problema: Dos clases tienen el mismo campo. Solución: Elimina el campo de las subclases y muévelo a la superclase.
Problema: Tus subclases tienen métodos que realizan un trabajo similar. Solución: Haz que los métodos sean idénticos y luego muévelos a la superclase correspondiente.
Problema: ¿Se usa un campo solo en unas pocas subclases? Solución: Mueve el campo a esas subclases.
Problema: ¿El comportamiento implementado en una superclase es utilizado por una sola subclase (o por unas pocas)? Solución: Mueve este comportamiento a las subclases.
Problema: Una clase contiene muchos métodos sencillos que delegan en todos los métodos de otra clase. Solución: Haz que la clase sea heredera del delegado, lo que vuelve innecesarios los métodos delegantes.
Problema: Tienes una subclase que usa solo una parte de los métodos de su superclase (o no es posible heredar los datos de la superclase). Solución: Crea un campo y coloca en él un objeto de la superclase, delega los métodos en el objeto de la superclase y elimina la herencia.
Problema: Tienes varios condicionales que conducen al mismo resultado o acción. Solución: Consolida todos esos condicionales en una sola expresión.
Problema: Se encuentra código idéntico en todas las ramas de un condicional. Solución: Mueve el código fuera del condicional.
Problema: Tienes un condicional complejo (if-then/else o switch). Solución: Descompón las partes complicadas del condicional en métodos separados: la condición, el then y el else.
Problema: para que una porción de código funcione correctamente, ciertas condiciones o valores deben ser verdaderos. Solución: sustituye estas suposiciones por comprobaciones de aserción concretas.
Problema: Como algunos métodos devuelven null en lugar de objetos reales, tienes muchas comprobaciones de null en tu código. Solución: En lugar de null, devuelve un objeto nulo que muestre el comportamiento por defecto.
Problema: Tienes una variable booleana que actúa como bandera de control para múltiples expresiones booleanas. Solución: En lugar de la variable, utiliza break, continue y return.
Problema: Tienes un condicional que ejecuta diversas acciones en función del tipo o las propiedades de un objeto. Solución: Crea subclases que se correspondan con las ramas del condicional. En ellas, crea un método compartido y traslada a él el código de la rama correspondiente del condicional. Después, reemplaza el condicional por la llamada al método pertinente. El resultado es que se alcanzará la implementación adecuada mediante polimorfismo en función de la clase del objeto.
Problema: Tienes un grupo de condicionales anidados y es difícil determinar el flujo normal de ejecución del código. Solución: Aísla todas las comprobaciones especiales y los casos límite en cláusulas separadas y colócalas antes de las comprobaciones principales. Lo ideal es tener una lista «plana» de condicionales, uno tras otro.
Problema: Cuando una clase hace el trabajo de dos, surge la incomodidad. Solución: En su lugar, crea una clase nueva y coloca en ella los campos y métodos responsables de la funcionalidad correspondiente.
Problema: El cliente obtiene el objeto B a partir de un campo o método del objeto A. Después, el cliente llama a un método del objeto B. Solución: Crea un nuevo método en la clase A que delegue la llamada al objeto B. Ahora el cliente no conoce la clase B ni depende de ella.
Problema: Una clase casi no hace nada y no es responsable de nada, y no se planean responsabilidades adicionales para ella. Solución: Mueve todas las características de la clase a otra.
Problema: una clase de utilidad no contiene el método que necesitas y no puedes añadirlo a la clase. Solución: añade el método a una clase cliente y pásale como argumento un objeto de la clase de utilidad.
Problema: una clase de utilidad no contiene algunos métodos que necesitas, pero no puedes añadir esos métodos a la clase. Solución: crea una clase nueva que contenga los métodos y conviértela en hija o en envoltorio (wrapper) de la clase de utilidad.
Problema: Un campo se utiliza más en otra clase que en la suya propia. Solución: Crea un campo en una nueva clase y redirige hacia él a todos los usuarios del campo antiguo.
Problema: un método se usa más en otra clase que en la suya propia. Solución: crea un método nuevo en la clase que más lo usa y traslada allí el código del método antiguo. Convierte el código del método original en una referencia al nuevo método de la otra clase, o bien elimínalo por completo.
Problema: Una clase tiene demasiados métodos que simplemente delegan en otros objetos. Solución: Elimina estos métodos y obliga al cliente a llamar directamente a los métodos finales.
Problema: tienes un fragmento de código que se puede agrupar. Solución: traslada este código a un nuevo método (o función) independiente y sustituye el código antiguo por una llamada al método.
Problema: Tienes una expresión difícil de entender. Solución: Coloca el resultado de la expresión o de sus partes en variables separadas que se expliquen por sí mismas.
Problema: Cuando el cuerpo de un método es más evidente que el propio método, usa esta técnica. Solución: Sustituye las llamadas al método por el contenido del método y elimina el propio método.
Problema: Tienes una variable temporal a la que se asigna el resultado de una expresión simple y nada más. Solución: Sustituye las referencias a la variable por la propia expresión.
Problema: Se asigna algún valor a un parámetro dentro del cuerpo del método. Solución: Usa una variable local en lugar de un parámetro.
Problema: Tienes un método largo en el que las variables locales están tan entrelazadas que no puedes aplicar Extraer método. Solución: Transforma el método en una clase separada de modo que las variables locales se conviertan en campos de la clase. Después podrás dividir el método en varios métodos dentro de la misma clase.
Problema: Colocas el resultado de una expresión en una variable local para su uso posterior en el código. Solución: Mueve toda la expresión a un método independiente y devuelve el resultado desde él. Consulta el método en lugar de usar una variable. Incorpora el nuevo método en otros métodos, si es necesario.
Problema: Tienes una variable local que se utiliza para almacenar varios valores intermedios dentro de un método (salvo las variables de los bucles). Solución: Usa variables distintas para valores distintos. Cada variable debe ser responsable de una sola cosa en concreto.
Problema: ¿Quieres reemplazar un algoritmo existente por uno nuevo? Solución: Reemplaza el cuerpo del método que implementa el algoritmo por un algoritmo nuevo.
Soluciones típicas a problemas comunes en el diseño de software, organizadas por intención.
Abstract Factory es un patrón de diseño creacional que nos permite producir familias de objetos relacionados sin especificar sus clases concretas.
Builder es un patrón de diseño creacional que nos permite construir objetos complejos paso a paso. El patrón nos permite producir distintos tipos y representaciones de un objeto empleando el mismo código de construcción.
Factory Method es un patrón de diseño creacional que proporciona una interfaz para crear objetos en una superclase, mientras permite a las subclases alterar el tipo de objetos que se crearán.
Prototype es un patrón de diseño creacional que nos permite copiar objetos existentes sin que el código dependa de sus clases.
Singleton es un patrón de diseño creacional que nos permite asegurarnos de que una clase tenga una única instancia, a la vez que proporciona un punto de acceso global a dicha instancia.
Adapter es un patrón de diseño estructural que permite la colaboración entre objetos con interfaces incompatibles.
Bridge es un patrón de diseño estructural que te permite dividir una clase grande, o un grupo de clases estrechamente relacionadas, en dos jerarquías separadas (abstracción e implementación) que pueden desarrollarse independientemente la una de la otra.
Composite es un patrón de diseño estructural que te permite componer objetos en estructuras de árbol y trabajar con esas estructuras como si fueran objetos individuales.
Decorator es un patrón de diseño estructural que te permite añadir funcionalidades a objetos colocando estos objetos dentro de objetos encapsuladores especiales que contienen estas funcionalidades.
Facade es un patrón de diseño estructural que proporciona una interfaz simplificada a una biblioteca, un framework o cualquier otro grupo complejo de clases.
Flyweight es un patrón de diseño estructural que te permite mantener más objetos dentro de la cantidad disponible de RAM compartiendo las partes comunes del estado entre varios objetos en lugar de mantener toda la información en cada objeto.
Proxy es un patrón de diseño estructural que te permite proporcionar un sustituto o marcador de posición para otro objeto. Un proxy controla el acceso al objeto original, permitiéndote hacer algo antes o después de que la solicitud llegue al objeto original.
Chain of Responsibility es un patrón de diseño de comportamiento que te permite pasar solicitudes a lo largo de una cadena de manejadores. Al recibir una solicitud, cada manejador decide si la procesa o si la pasa al siguiente manejador de la cadena.
Command es un patrón de diseño de comportamiento que convierte una solicitud en un objeto independiente que contiene toda la información sobre la solicitud. Esta transformación te permite parametrizar los métodos con diferentes solicitudes, retrasar o poner en cola la ejecución de una solicitud y soportar operaciones que no se pueden realizar.
Iterator es un patrón de diseño de comportamiento que te permite recorrer elementos de una colección sin exponer su representación subyacente (lista, pila, árbol, etc.).
Mediator es un patrón de diseño de comportamiento que te permite reducir las dependencias caóticas entre objetos. El patrón restringe las comunicaciones directas entre los objetos, forzándolos a colaborar únicamente a través de un objeto mediador.
Memento es un patrón de diseño de comportamiento que te permite guardar y restaurar el estado previo de un objeto sin revelar los detalles de su implementación.
Observer es un patrón de diseño de comportamiento que te permite definir un mecanismo de suscripción para notificar a varios objetos sobre cualquier evento que le suceda al objeto que están observando.
State es un patrón de diseño de comportamiento que permite a un objeto alterar su comportamiento cuando su estado interno cambia. Parece como si el objeto cambiara su clase.
Strategy es un patrón de diseño de comportamiento que te permite definir una familia de algoritmos, colocar cada uno de ellos en una clase separada y hacer sus objetos intercambiables.
Template Method es un patrón de diseño de comportamiento que define el esqueleto de un algoritmo en la superclase pero permite que las subclases sobrescriban pasos del algoritmo sin cambiar su estructura.
Visitor es un patrón de diseño de comportamiento que te permite separar algoritmos de los objetos sobre los que operan.
Indicadores de problemas más profundos, y las refactorizaciones que los tratan.
Dos clases realizan funciones idénticas, pero tienen nombres de método diferentes.
Si una subclase usa solo algunos de los métodos y propiedades heredados de sus padres, la jerarquía está desequilibrada. Los métodos innecesarios pueden simplemente quedar sin usar o redefinirse y lanzar excepciones.
Tienes un operador switch complejo o una secuencia de sentencias if.
Los campos temporales reciben sus valores (y, por tanto, los objetos los necesitan) solo en determinadas circunstancias. Fuera de esas circunstancias, están vacíos.
Un método está lleno de comentarios explicativos.
Una clase de datos es una clase que contiene únicamente campos y métodos rudimentarios para acceder a ellos (getters y setters). No son más que contenedores de datos que utilizan otras clases. Estas clases no contienen ninguna funcionalidad adicional y no pueden operar de forma independiente sobre los datos que poseen.
Una variable, parámetro, campo, método o clase ya no se utiliza (normalmente porque ha quedado obsoleto).
Dos fragmentos de código tienen un aspecto casi idéntico.
Comprender y mantener clases siempre cuesta tiempo y dinero. Por eso, si una clase no hace lo suficiente para merecer tu atención, debería eliminarse.
Hay una clase, un método, un campo o un parámetro sin usar.
A veces, diferentes partes del código contienen grupos idénticos de variables (como los parámetros para conectarse a una base de datos). Estos grupos deberían convertirse en sus propias clases.
Una clase contiene muchos campos/métodos/líneas de código.
Un método contiene demasiadas líneas de código. Por lo general, cualquier método con más de diez líneas debería hacerte empezar a hacer preguntas.
Más de tres o cuatro parámetros para un método.
Uso de primitivos en lugar de pequeños objetos para tareas sencillas (como monedas, rangos, cadenas especiales para números de teléfono, etc.)Uso de constantes para codificar información (como una constante USER_ADMIN_ROLE = 1 para referirse a usuarios con derechos de administrador.)Uso de constantes de cadena como nombres de campo para su uso en arrays de datos.
Te ves obligado a modificar muchos métodos no relacionados cuando haces cambios en una clase. Por ejemplo, al añadir un nuevo tipo de producto tienes que cambiar los métodos para encontrar, mostrar y pedir productos.
Cada vez que creas una subclase para una clase, te ves obligado a crear una subclase para otra clase.
Realizar cualquier modificación requiere que hagas muchos cambios pequeños en muchas clases diferentes.
Un método accede a los datos de otro objeto más que a sus propios datos.
Una clase usa los campos y métodos internos de otra clase.
En el código ves una serie de llamadas que se asemejan a $a->b()->c()->d()
Si una clase realiza una única acción, delegando el trabajo en otra clase, ¿por qué existe siquiera?
Patrones y metodologías a nivel de sistema: DDD, CQRS, TDD y Spec-Driven Development.
Un enfoque de desarrollo de software que centra el diseño en un modelo rico del dominio de negocio, expresado en un lenguaje compartido por ingenieros y expertos del dominio.
Separa el modelo que cambia el estado (comandos) del modelo que lee el estado (consultas), de modo que cada lado pueda modelarse, escalarse y optimizarse de forma independiente.
Una disciplina de desarrollo en la que escribes una prueba que falla antes que el código que la hace pasar y, a continuación, refactorizas, dejando que las pruebas guíen el diseño en ciclos cortos y ajustados.
Escribe primero una especificación explícita y autoritativa y, a partir de ella, dirige la implementación, las pruebas y el código generado, manteniendo la especificación como única fuente de verdad.