¿Te ha pasado alguna vez que estas utilizando una librería, o plugin, y en algún momento se ha decidido cambiarlo? Si has tenido que cambiar mucho código es porque no tenías un adapter.
La principal función de este patrón estructural es «adaptarse» a una interfaz, o contrato, de forma que los consumidores no se enteren de que hay detrás.
También nos puede ayudar para convertir clases estáticas, como la clase Input o PlayerPrefs de Unity, en clases que tengamos que instanciar, y por lo tanto testeables e inyectables.
Para hacer un análogo con la vida real, un adapter tiene el mismo funcionamiento que un adaptador de corriente para cargar el móvil. Por un lado tiene la interfaz que utilizan todos los enchufes (el consumidor) y por otro lado tiene la implementación que nuestro móvil necesita, un cable USB a USB-C, esto es la parte adaptada.
Este adaptador de corriente también suele permitir cambiar la parte adaptada, y conectar cualquier dispositivo que utilice un cable USB.
Ahora que ya sabemos qué es un adapter en la vida real, vamos a ver un ejemplo de programación con un parser de JSON, algo que es muy utilizado en juegos cliente-servidor.
Un ejemplo de porqué utilizar un Adapter
Como veníamos diciendo, solemos utilizar parsers en los servicios que hacen peticiones al servidor. Le pasamos una clase tipada como argumento y esta la envía en forma de string, o cómo lo necesite, al servidor. O recibe los datos en string del servidor y los devuelve en una clase tipada.
Si estas en Unity lo más común es que utilices la clase estática JsonUtility, que no está mal pero tiene el inconveniente de ser estática y dificultarnos el cambio y testing. Y peor es aún cuando utilizamos la estática Input para leer el input del usuario, esto nos impide completamente crear tests automatizados (sin tener que hacer un montón de ninjadas).
La alternativa a esto es utilizar un adapter y convertir estas clases estáticas en clases qué debamos instanciar. Además de poder instanciar estas clases, esto nos permitirá cambiar el comportamiento de la clase consumidora sin tener que hacer nada, solo tendremos que inyectar otra case.
Imaginaros que mañana decidimos que los datos de server no van a venir en un string y lo que vamos a recibir es son los datos en binario y con encriptación porque queremos mejorar la seguridad de nuestro sistema.
O en lugar de venir en JSON, van a llegarnos en XML, otro estándar bastante utilizado. O qué vamos a añadir otro servicio de red que en lugar de JSON utiliza XML y queremos que convivan los dos servicios. Podríamos duplicar la clase de red o simplemente cambiar el parser que utilizamos.
Yo prefiero hacer lo segundo, así que vamos a ver un ejemplo de cómo aplicar un Adapter para tener la habilidad de cambiar el parseador cuando lo necesitemos. En este ejemplo utilizaremos dos parsers de JSON, el de Unity y el nativo de .NET, per podemos aplicarlo sobre cualquier parser.
Implementación del patrón de diseño Adapter
Parseador
Lo primero será definir nuestra interfaz para parsear datos, necesitaremos un método que vaya de string a objeto y otro de objeto a string.
public interface IParser
{
string Serialize<T>(T data);
T Deserialize<T>(string data);
}
Con la interfaz lista solo tenemos que crear los adapters de nuestros parsers, por un lado crearemos el adapter de JSONUtility de Unity
public class JsonUtilityAdapter : IParser
{
public string Serialize<T>(T data)
{
return JsonUtility.ToJson(data);
}
public T Deserialize<T>(string data)
{
return JsonUtility.FromJson<T>(data);
}
}
Y por otra parte tendremos el adapter del parser de .Net para Unity.
public class JsonNetAdapter : IParser
{
public string Serialize<T>(T data)
{
return JsonConvert.SerializeObject(data);
}
public T Deserialize<T>(string data)
{
return JsonConvert.DeserializeObject<T>(data);
}
}
Aunque creamos que la librería de Unity es suficiente para esto, es mucho mejor crearnos un adapter que nos facilite el cambio el día de mañana. Esto es así porque el día de mañana podríamos descubrir qué esta librería es incompatible con algunas plataformas, o no tuviese el rendimiento esperado.
De esta forma solo tendremos que crear un nuevo adapter para la nueva librería y cambiar donde hacíamos la construcción. Si la construcción la tenemos abstraída solo nos tendría que suponer un par de lineas más.
Sin pensarlo también estamos cumpliendo con el principio de Inversión de Dependencias de SOLID, que nos dice que es mejor depender de abstracciones que de concreciones, esto nos facilita el mantenimiento y el testing.
La forma de consumir este parser es muy sencilla, solo tenemos que pasarselo al consumidor por constructor y este recibirá la interfaz, muy importante que sea la interfaz para poder cambiar el parser de forma fácil sin modificar código y respetando el principio SOLID Open-Close.
public class NetworkService
{
private readonly IParser parser;
public NetworkService(IParser parser)
{
this.parser = parser;
}
}
public class Installer
{
public void Install()
{
var networkServiceWithJsonUtility = new NetworkService(new JsonUtilityAdapter());
var networkServiceWithJsonNet = new NetworkService(new JsonNetAdapter());
}
}
Conclusión
Utilizar Adapters hace que nuestro código sea más flexible y tolerante al cambio, por no mencionar el testing (eso es una batalla que lucharemos en otro post 😛).
Por supuesto, no siempre es necesario utilizar adapters, pero teniendo en cuenta que el coste de utilizarlo es una interfaz y una clase, yo prefiero protegerme contra el cambio. Seguro que a más de uno os ha pasado que por el coste de no poder cambiar algo, o bien habéis tenido que invertir horas de más, o bien no lo habéis podido hacer.
A mi esto me ha pasado varias veces, una cuando necesitamos cambiar el sistema de guardado y en lugar de escribir en PlayerPrefs teníamos que escribir en un archivo binario porque en PlayStation no nos iba bien esta solución.
La otra fue cuando nos planteamos ejecutar el servidor en local para poder agilizar el proceso de desarrollo y queríamos utilizar un sistema de base de datos local distinto al que utilizamos en remoto, al no tener este adapter y estar acoplado a la implementación nos quedamos sin poder hacer esto 😢.
Espero haberte convencido para que utilices adapters en tus proyectos y te protejas contra el cambio, pero si no es así me gustaría leer tu opinión al respecto en los comentarios de esta entrada, y si te he convencido me alegro de que sea así 😄.
Puedes descargar el ejemplo del Adapter en Unity desde aquí.
Fuentes
- El libro de GANG OF FOUR, patrones de diseño (Erich Gamma)
- Game Programming Patterns (Robert Nystrom – Electronic Arts)
- Agile Principles, Patterns, and Practices in C# (Robert C. Martin)