Hace algunas semanas recibimos un correo solicitando confirmación de nuestra asistencia a un evento de la empresa. Incluido venia un pequeño código:
if (employee.WantsToAttend()) { if (employee.IsWorkingFromOffice1()) { employee.reply(manager1, "I wanna be there"); } else if (employee.IsWorkingFromOffice2()) { employee.reply(manager2, "Dude! I wanna be there!"); } }
Entiendo que el propósito del código era mas de marketing, pero he visto código como este en varias ocasiones. Vamos a revisarlo.
Pilares de la programación orientada a objetos
Todos hemos escuchado de los principios de la programación orientada a objetos: encapsulamiento, polimorfismo, herencia y abstracción. Vamos a aplicarlos al código que acabamos de ver.
Encapsulamiento
Encapsulamiento es un principio que dicta que debemos esconder los mecanismos internos de un objeto, de manera que si los modificamos esto no afecte a los demás objetos relacionados.
Con eso en mente, revisemos los siguientes métodos:
employee.WantsToAttend() employee.IsWorkingFromOffice1() employee.reply(manager1, "I wanna be there");
Estos métodos son detalles de implementacion del escenario «confirmar asistencia». La verdad no nos importa como el empleado confirma la asistencia, solo que lo haga. Podemos mover la primera evaluación:
class Employee { ... public void ConfirmAssistance(string manager, string msg) { if(wantsToAttend()) reply(manager,msg); } }
Ahora el código cliente quedaría un poco mas limpio:
if (employee.IsWorkingFromOffice1()) employee.ConfirmAssistance(manager1, "I wanna be there"); else if (employee.IsWorkingFromOffice2()) employee.ConfirmAssistance(manager2, "Dude! I wanna be there!");
Al hacer la evaluación dentro del objeto evitamos la necesidad de exponer datos internos. Esto implica un cambio en las responsabilidades del objeto. De ahora en adelante no tenemos que preocuparnos por averiguar si el employee quiere asistir o no. Es automático.
Allen Holub se refiere a esto como pedir ayuda no datos. Esta es una consecuencia directa del encapsulamiento y probablemente el consejo que mas me ha ayudado en el cambio del pensamiento relacional al orientado a objetos.
Vamos a encapsular la evaluación de las oficinas:
class Employee { ... public void ConfirmAssistance(Func<string,string> msgFactory) { if(wantsToAttend()) reply(Office.Manager,msgFactory(Office.Id)); } }
Y así nos quedamos con una linea en el cliente:
employee.ConfirmAssistance(officeId => officeId == 1?"I wanna be there": "Dude! I wanna be there!");
En este caso el texto del mensaje se decide fuera del objeto, así que pasamos la función encargada de eso al objeto. De esta forma evitamos exponer el funcionamiento y al mismo tiempo inyectamos el comportamiento deseado.
Si el código te resulta extraño, solo estamos declarando una función anónima usando una sintaxis llamada lambda expressions (los ejemplos están en C#).
Comparada con la versión previa, cual código te parece mas reutilizable?
Algunas observaciones:
1) ahora tenemos un mensaje para la oficina 1 y para el resto de las oficinas (no solo la oficina 2)
2) realmente no nos importa como se almacenan los datos de la oficina. Podríamos cambiarlos y no afectaríamos nada.
Separando responsabilidades
El método reply implica la llamada a un tercero. Almacenar una referencia a un tercero en este caso esta de mas. Vamos a separar esto en 2 partes: la creación y el envió del mensaje.
class MessageGateway { Send(Message msg){...} } class Message { public Message(string recipient, string body) { Recipient = recipient; Body = body; } public string Recipient {get;set;} public string Body {get;set;} } class Employee { ... public Message ConfirmAssistance(Func<string,string> msgFactory) { if(wantsToAttend()) return new Message(Office.Manager,msgFactory(Office.Id)); else return null; } }
El código cliente quedaría:
Message reply = employee.ConfirmAssistance(officeId => officeId == 1?"I wanna be there": "Dude! I wanna be there!"); if(reply != null) new MessageGateway().Send(reply);
Ahora el objeto employee es el encargado de crear el mensaje y el objeto MessageGateway el encargado de enviarlo. Separar las responsabilidades de esta manera cumple con el Single Responsibility Principle.
Pero ahora estamos rompiendo el encapsulamiento en el objeto Message.
Vamos a arreglarlo.
class MessageGateway { Send(string recipient, string body){...} } class Message { public Message(string recipient, string body) { Recipient = recipient; Body = body; } string Recipient; string Body; public SendThrough(MessageGateway gateway) { gateway.Send(Recipient,Body); } } class Employee { ... public Message ConfirmAssistance(Func<string,string> msgFactory) { if(wantsToAttend()) return new Message(Office.Manager,msgFactory(Office.Id)); else return null; } }
En el código cliente:
Message reply = employee.ConfirmAssistance(officeId => officeId == 1?"I wanna be there": "Dude! I wanna be there!"); if(reply != null) reply.SendThrough(new MessageGateway());
Quizá esto no tenga mucho sentido ahora. Todo lo que hicimos fue invertir la dirección en que se pasan los datos. Pero ahora podemos usar polimorfismo para remover esa validación de null.
Polimorfismo
Hay un concepto que indica que entre mas bifurcaciones tiene un programa, mas difícil es su mantenimiento. Se llama complejidad ciclomatica y es un indicador de la calidad del código. En resumen entre menos ‘if’ y ‘switch’ mejor.
Con los cambios iniciales habíamos removimos todos los ‘if’. Pero luego introdujimos uno nuevo al validar si el mensaje venia null. Vamos a quitarlo. Una técnica común en la programación orientada a objetos es el null object pattern. Utiliza polimorfismo para eliminar bifurcaciones en el código cliente. Vamos a verlo.
1) extraemos una interface común
interface IMessage { SendThrough(MessageGateway gateway); }
2) creamos un objeto que no haga nada, como lo haríamos si recibiéramos un null.
class Message: IMessage { public Message(string recipient, string body) { Recipient = recipient; Body = body; } string Recipient; string Body; public SendThrough(MessageGateway gateway) { gateway.Send(Recipient,Body); } //usually the null object it's used in a singleton fashion class NullMessage: IMessage { public SendThrough(MessageGateway gateway) { //Do nothing :) } } public static IMessage Null{get;private set;} public static Message() { Null = new NullMessage(); } }
3) regresa ese objeto en lugar de null
class Employee { ... public Message ConfirmAssistance(Func<string,string> msgFactory) { if(wantsToAttend()) return new Message(Office.Manager,msgFactory(Office.Id)); else return Message.Null; } }
Presto! actualizamos el código cliente:
Message reply = employee.ConfirmAssistance(officeId => officeId == 1?"I wanna be there": "Dude! I wanna be there!"); reply.SendThrough(new MessageGateway());
y de vuelta a una linea
employee .ConfirmAssistance(officeId => officeId == 1? "I wanna be there": "Dude! I wanna be there!") .SendThrough(new MessageGateway());
Polimorfismo nos permite cambiar el comportamiento de un sistema sin cambiar el código. Esto se logra creando variaciones de un método e intercambiándolos según se necesite.
Si no es programación orientada a objetos, entonces que es?
Repasemos:
objeto – datos (estado) = modulo (recuerdan vb6?)
objeto – métodos (comportamiento) = struct (disponible desde C)
Es facil escribir un programa que usa módulos y structs. Y funciona bien en muchos casos (forms over data ;))
En conclusión
1) Encapsulamiento habilita el Polimorfismo
2) Polimorfismo habilita el uso de patrones de diseño y otras bondades de la programación orientada a objetos
La programación orientada a objetos permite la creación de aplicaciones sumamente flexible pero tiene un costo: indireccion. Si tu proyecto es relativamente simple (como este ejemplo) quizá te convenga usar otro paradigma como programación estructurada (módulos + structs). Pero si decides usar la programación orientada a objetos, ten en mente que los objetos hacen cosas. Pide ayuda, no datos!
Extra: un toque funcional
Closures pueden simplificar mucho este código. Ya que están presentes desde smalltalk, los considero parte de la programación orientada a objetos. Así es como quedaria:
class MessageGateway { Send(string recipient, string body){...} } class Employee { ... public Message ConfirmAssistance(Action<string,string> confirm) { if(wantsToAttend()) confirm(Office.Manager,Office.Id); } } //client code employee.ConfirmAssistance((manager,officeId)=> { var response = officeId == 1? "I wanna be there": "Dude! I wanna be there!"; new MessageGateway().Send(manager, response); });