En esta entrada vamos a ver la utilidad del patrón Service Locator para construir aplicaciones distribuidas. Los componentes de estas aplicaciones deben estar totalmente desacoplados y por lo tanto la mejor manera de hacerlo es mediante el consumo de servicios que obviamente respeten ciertos contratos. Ok, largamos con el patrón FactoryMethod como patrón inicial para ver como llegamos al ServiceLocator. Entonces, basicamente, todo método que tiene por objetivo crear diferentes tipos de objetos implementa implementa el patrón FactoryMethod. Veamos un ejemplo:
1 using System.Windows.Forms;
2
3 namespace MyDocumentManager
4 {
5 enum DocumentExtensionEnum {DOC, PDF, XML};
6
7 /* La clase Abstracta */
8 public abstract class Document
9 {
10 public abstract void Show(Form container);
11 }
12
13 /* Las clases concretas */
14 public class WordDocument : Document
15 {
16 public override void Show(Form container){/*...*/}
17 }
18
19 public class PDFDocument : Document
20 {
21 public override void Show(Form container){/*...*/}
22 }
23
24 public class XMLDocument : Document
25 {
26 public override void Show(Form container){/*...*/}
27 }
28
29 /* La clase creadora o consumidora de documentos */
30 public class DocumentManager
31 {
32 DocumentManager(string filename)
33 {
34 Document doc = FactoryMethod(filename);
35 doc.Show(new DocumentViewerForm());
36 }
37
38 /* Este es el metodo */
39 private Document FactoryMethod(string filename)
40 {
41 Document doc;
42 DocumentExtensionEnum extension = GetExtension(filename);
43
44 /* crea el tipo de documento adecuado segun la extension del archivo */
45 switch(extension){
46 case DocumentExtensionEnum.DOC:
47 doc = new WordDocument();
48 break;
49 case DocumentExtensionEnum.PDF:
50 doc = new PDFDocument();
51 break;
52 case DocumentExtensionEnum.XML:
53 doc = new XMLDocument();
54 break;
55 }
56 retrun doc;
57 }
58 }
59 }
60
Este es un ejemplo claro, según la extensión del archivo, nos devuelve el objeto adecuado. El tema es ahora que en este método tenemos un switch hardcodeado! Es decir, no podemos agregarle otros tipos de objetos a crear sin modificar el código :(
Y aquí entra el patrón que nos convoca, el Service Locator. Se trata de un patrón creational el cual permite registrar servicios o componentes y luego solicitar una instacia de alguno de ellos. Veamos un poco de código:
public class ServiceLocator
{
private Dictionary<string, object> services = new Dictionary<string, object>();
public void AddService(string serviceName, object service)
{
if (string.IsEmptyOrNull(serviceName))
throw new ArgumentNullException("serviceName");
if (service == null)
throw new ArgumentNullException("service");
services.Add(serviceName, service);
}
public object GetService(string serviceName)
{
if (string.IsEmptyOrNull(serviceName))
throw new ArgumentNullException("serviceName");
if (services.ContainsKey(serviceName))
return services[serviceName];
return null;
}
}
Aquí lo he implementado con un diccionario string-object para hacerlo mas facil pero despues voy a mostrar una alternativa mejor. Otra cosa, por lo general, solo existe una instancia de esta clase as'i que se implementa mediante un Singleton.
La ventaja que tenemos ahora es que podemos hacer una clase "Services Loader" la cual lea un archivo de configuración (seguramente deserealizando un xml) y que de acuerdo a eso cargue los servicios que hagan falta dentro del Service Locator. Esto es especialmente útil cuando queremos hacer un sistema plug-able porque podemos poner un assembly en un directorio y modificar nuestro xml de configuración de modo que esta clase Service Loader cargue ahora los nuevos servicios de nuestro assembly haciendo que estos esten disponibles para la aplicación.
A ver si se entiende bien! puedo por ejemplo:
IStorageManager sm = (IStorageManager)serviceLocator.GetService("Storage Service");
if(sm != null)
{
sm.Save(myPersistentObject);
}
else
{
IEventLogger ev = (IEventLogger)serviceLocator.GetService("Event Logger Service");
if(ev != null)
ev.WriteWarning(
String.Format("{0} wasn't persisted", myPersistentObject.ToString()
);
}Bien, alguno debe estar pensando "y como hace un cast así! habria que validar que respete esa interface antes de castearla tan burramente!!!". Y sí, quien piesa así tiene razón, el tema es que lo hago así por sencilles y porque lo que quiero mostrar es que de esta manera lo que hacemos es: primero preguntamos si tenemos tal o cual servicio y de ser así lo usamos. Con este patrón (que todavia no tiene buena forma) podemos agregarle, sin tocar el código, un servicio de logueo cualquiera (que implemente la IEventLogger por supuesto). También podemos ponerle, sacarle o cambiarle el servicio de Storage registrando por ejemplo bajo el nombre de "Storage Service" a cualquier servicio que implemente la interface IStorageManage como por ejemplo, "SQL Storage Service", "Amazon Storage Services", "Web Blog Storage Service", etc.
Tanto que hablamos del "Service Loader" veamos como puede ser:
(Antes que nada, si ven algo que no anda es porque en la máquina en que estoy escribiendo esta entrada no tengo el .Net Framework 2.0)
[XmlRootAttribute("ServiceCatalog", Namespace="http://www.temosoft.com.ar/ServiceLoader", IsNullable = false)]
public class ServiceCatalog
{
private Service[] services;
public Service[] Services
{
get{ return services; }
set{ services=value; }
}
}
[Serializable]
public class Service
{
private string serviceName;
private string assemblyName;
private string typeName;
private Boolean isAvailable;
[XmlAttribute]
public string ServiceName
{
get { return serviceName; }
set { serviceName= value; }
}
[XmlAttribute]
public string AssemblyName
{
get { return AssemblyName; }
set { AssemblyName= value; }
}
[XmlAttribute]
public string TypeName
{
get { return typeName; }
set { typeName= value; }
}
[XmlAttribute]
public Boolean IsAvailable
{
get { return isAvailable; }
set { isAvailable= value; }
}
}
public class ServiceLoader
{
public void Load()
{
XmlSerializer serializer = new XmlSerializer(typeof(ServiceCatalog));
FileStream fs = new FileStream("ServiceCatalog.xml", FileMode.Open);
ServiceCatalog servicesCatalog = (ServiceCatalog)serializer.Deserialize(fs);
foreach(Service s in servicesCatalog.Services)
{
try{
Assembly asm = Assembly.Load(s.AssemblyName);
Type serviceType asm.GetType(s.TypeName);
ServiceLocator.Instance.AddService( s.ServiceName, Activator.CreateInstance(serviceType));
}catch(Exception e){
}
}
}
}
En este caso asuminos que el ServiceLocator es un Singleton. Es posible también hacer que cada servicio pueda recibir parámetros en su constructor. Ahora, como hacemos para mejorar esto?
public static class ServiceLocator
{
private static ServiceLocator instance;
private static object instanceLock = new object();
private Dictionary<object, object> services = new Dictionary<object, object>();
public ServiceLocator Instance
{
get
{
if (instance == null)
{
lock (instanceLock)
{
if (instance == null)
{
instance = new ServiceLocator();
}
}
}
return instance;
}
}
public void Add(object serviceKeyObject, object service)
{
if (serviceKeyObject==null)
throw new ArgumentNullException("serviceKeyObject");
if (service == null)
throw new ArgumentNullException("service");
if (!services.ContainKey(serviceKeyObject))
services.Add(serviceKeyObject, service);
}
public object Get(object serviceKeyObject)
{
if (serviceName==null)
throw new ArgumentNullException("serviceKeyObject");
if (services.ContainsKey(serviceName))
return services[serviceName];
return null;
}
public T Get<T> (object serviceKeyObject)
{
object obj = Get(serviceKeyObject);
if (obj != null && (obj is T))
return (T)obj;
return null;
}
}
Si no te sirvió usa el ObjectBuilder :) ,que además de implementar este y otros muchos patrones muy grosos, implementa Inyección de Dependencias para un desacople muy bien pensado.
Lucas Ontivero.
No hay comentarios:
Publicar un comentario