Primero, quizas algunos se pregunten por qué esta mal usar IFs ?
Existen varias razones para no usarlos, pero tratare de ser preciso y consistente para luego pasar a lo que nos aplica que es como reemplezar, en la medida que se pueda esta forma de decisión:
1.- Mas allá que el "if" es la forma inicial de plantear estrategias de decisión dentro del código, estamos haciendolo de una forma es muy posible que, a menos que sean necesarios, posiblemente encapsule comportamiento que no corresponda a la clase que lo aloja y que quede en un lugar donde luego tengamos que modificar relativamente seguido si los cambios o escenarios cambian.
2.- Hablando puramente de objetos, y que el paradigma es “intereactuan los objetos solo a a travez de métodos”, el if es una forma en la cual estamos saliendo de este paradigma , para meternos en una idea mas procedural. Si quisieramos ser mas "objetosos” deberiamos salir de este tipo de implementación.
3.- Subjetivamente, El código queda feo; Saca legibilidad y aumenta las chances de errores.
Dado que java dentro de los puristas de objetos es bastante vapuleado y con razón, tratare en este articulo mostrar 3 técnicas que nos permitirán evitar los "if" en nuestro código para este lenguaje, logrando además hacerlo más mantenible.
Si bien son approachs muy especificos los que planteo, ya que decidí volcar el articulo a un hecho meramente implementativo, lo que muestro esta basado en los patrones conocidos ( Strategy, State, etc). El objetivo es que mas alla de los nombres puedan ver algunas simples ideas que mejoran la implementación.
Primero la base de Todo: Polimorfismo.
Dado que lo que queremos es tener elección de comportamiento en función de las condiciones que se den , lo que estamos queriendo hacer es hablar de unificar tipos de comportamientos bajo una jerarquia, en la cual bajo una interfaz/clase abstracta decidamos en la clase concreta implementar el comportamiento deseado.
Para mejorar esta idea nadie mejor que leer sobre el principio de Liskov, y mejor aun leer sobre SOLID.
Como ejemplo usaré este esquema, que lo iremos usando en el resto de los ejemplos. El concepto de negocio sobre el que trabajaré es:
Dado distintos tipos de clientes, queremos realizar la cobranza de los mismos.
Dado distintos tipos de clientes, queremos realizar la cobranza de los mismos.
Podemos tener Un Cliente , con los siguientes Tipos:
- ClienteSimple
- ClientePyme
- ClienteGrande
Tendríamos que tener una interfaz:
public interface CobranzaClientes{
void realizarCobranza();
}
Tendriamos las siguientes implementaciones:
public class CobrarClienteSimple implements CobranzaClientes{
public void realizarCobranza(){
// Forma de Cobrar Para Clientes Simples
// Forma de Cobrar Para Clientes Simples
}
}
public class CobrarClientePyme implements CobranzaClientes{
public void realizarCobranza(){
// Forma de Cobrar Para Clientes Pyme
// Forma de Cobrar Para Clientes Pyme
}
}
public class CobrarClienteGrande implements CobranzaClientes{
public void realizarCobranza(){
// Forma de Cobrar Para Clientes Grande
// Forma de Cobrar Para Clientes Grande
}
}
Ahora pasemos a las técnicas:
1.A-Estrategia por Mapas (Opción Sin Java 8)
Asumamos que en función del tipo de Cliente, por ahora un String , realizamos la tarea de cobranza según el tipo de cliente.
Es entonces la solución posible a partir de una Factory (o alguna herramienta de IOC, que nos cargue un mapa) nos proveera conceptualmente funcionalidad similar a lo siguiente:
public class FactoryCobranzas{
public static Map getMapaCobranza(){
Map cobranza = new HashMap<String,CobranzaClientes>();
cobranza.put(“ClienteSimple”, new CobranzaClienteSimple());
cobranza.put(“ClientePyme””, new CobranzaClientePyme());
cobranza.put(“ClienteGrande””, new CobranzaClienteGrande());
return cobranza;
}
}
Entonces en tu clase que recibe cada cliente para efectuar la cobranza sería, evitando los ifs:
private Map cobranzaStrategy = FactoryCobranzas.getMapaCobranza();
public void efectuarCobranzaPorCliente(Cliente cliente){
cobranzaStrategy.get(cliente.getTipo()).realizarCobranza();
}
Como vemos evitamos de hacer "if" validando el tipo , y ademas si surgiese un nuevo tipo de ciente, por el momento es solo agregar un campo mas al mapa, y no cambiamos nuestro fuente.
Notar que la Key en este caso puede ser un String, pero podría ser un Enum u otro identificador que nos permita seleccionar la ram de funcionalidad que pretendamos. Ademas notar lo útil del concepto de programar orientado a Interfaces, y no a implementaciones.
1.B-Estrategia por Mapas (Opción Con Java 8)
La clase Factory nos quedaria , mucho mas simple y sin agregar quizas el esquema de Jerarquia clases, lo cual es aplicable si a lo que se quiere aplicar es mas simple en logica :
public class FactoryCobranzas{
Function cobranzaClienteSimple = () -> ;// Hacer cosas de Cobranza Simple
Function cobranzaClientePyme = () -> ;// Hacer cosas de Cobranza Pyme
Function cobranzaClienteGrande = () -> ;// Hacer cosas de Cobranza Grande
public static Map getMapaCobranza(){
Map cobranza = new HashMap<String, Function>();
cobranza.put(“ClienteSimple”, cobranzaClienteSimple);
cobranza.put(“ClientePyme””, cobranzaClientePyme);
cobranza.put(“ClienteGrande””, cobranzaClienteSimple);
return cobranza;
}
}
Esta solucion es similar en un punto en aplicacion , aunque para mi pierde un poco en legibilidad:
cobranzaStrategy.get(cliente.getTipo()).apply();
2.-Estrategia por Reflection
Esta estrategia, ademas de un fuerte elemento de “Convention over Configuration” , basado en el mismo criterio que en el anterior, hacemos algo a mi gusto mas rustico, usando reflection, pero no por ello deja que evitemos usar if:
private String packageWhereStrategyExists = “com.ar.paquete.”;
public void efectuarCobranzaPorCliente(Cliente cliente){
CobranzaClientes cobranzaCliente = (CobranzaClientes) Class.forName(packageWhereStrategyExists+ cliente.getTipoCliente()).newInstance();
cobranzaCliente.realizarCobranza();
}
Esto podria usarse tambien con Enums en el tipo:
String className = packageWhereStrategyExists+ cliente.getTipoCliente().name();
Class.forName(className).newInstance();
cobranzaCliente.realizarCobranza();
Un problema que podemos observar en este approach , es que si la clase que instanciamos posee colaboradores, los mismos no se crean ni vendran por este medio, por lo que se podria deducir que este acercamiento es para modelar logicas no muy complejas .
Un detalle que acerco es no tener miedo a la meta-programacion que se vislumbra en este ejemplo. Es una herramienta que hay que conocer y no abusar, con implementaciones mas simples en otros lenguajes ( como ruby) pero no por eso temerle por su complejidad, como ocurre en java.
3.-Estrategia Por Enums:
Si definimos un Enum de Cobranza:
public enum TipoCliente{
ClienteSimple("ClienteSimple") {
@Override
public void realizarCobranza() {
new CobranzaClienteSimple().realizarCobranza();
}
},
ClientePyme("ClientePyme") {
@Override
public void realizarCobranza() {
new CobranzaClientePyme().realizarCobranza();
}
},
ClienteGrande("ClienteGrande") {
@Override
public void realizarCobranza() {
new CobranzaClienteGrande().realizarCobranza();
}
};
//….. Otras implementaciones que necesitemos
// Este es el secreto para que los campos del enum implementen la funcionalidad!
public abstract void realizarCobranza();
}
ClienteSimple("ClienteSimple") {
@Override
public void realizarCobranza() {
new CobranzaClienteSimple().realizarCobranza();
}
},
ClientePyme("ClientePyme") {
@Override
public void realizarCobranza() {
new CobranzaClientePyme().realizarCobranza();
}
},
ClienteGrande("ClienteGrande") {
@Override
public void realizarCobranza() {
new CobranzaClienteGrande().realizarCobranza();
}
};
//….. Otras implementaciones que necesitemos
// Este es el secreto para que los campos del enum implementen la funcionalidad!
public abstract void realizarCobranza();
}
En este caso no necesitamos tener la interfaz ( aunque es recomendable) y en la clase Cliente, el tipo corresponde a este Enum , entonces:
public void efectuarCobranzaPorCliente(Cliente cliente){
cliente.getTipoCliente().realizarCobranza();
}
Para este tipo de estrategia son validos hibridos como con el mapa adentro de la clase del Enum.
Conclusión
En cualquiera de los 3 escenarios planteados, nos enfocamos que la necesidad de enfrentar el uso de distintos tipos de estrategia de realizar tareas, no nos lleve a romper el paradigma de objetos, usando abusivamente la herramienta “IF”. Vemos tambien es que si nos habituamos cuando es valido a evitar este uso, usualemente lograremos un codigo mas escalable, mas limpio y focalizado en la implementación y no en cómo llamar a la implementación.
Como corolario, es bueno tambien pensar llegar a este tipo de soluciones son las herramientas finales que usamos cuando vemos que nuestros test llegaron al “verde” y nos encontramos en la etapa de mejorar implementaciones; Hacerlo antes es riesgoso, no invalido, pero si factible de sobre-implementacion.
Gracias!