Retrospectiva sobre MEF: usos en el mundo real

Traducción aproximada del artículo MEF in the Wild: A Retrospective publicado en inglés por Jeremy Likness el 27 de setiembre del 2010 en su blog C#er:Image.

 

Luego de haber trabajado en varios proyectos grandes usando la Infraestructura de extensibilidad administrada, me parece buena idea compartir mis experiencias en cómo lo usamos y las ventajas que nos dio. Las siguientes son diferentes formas en las que MEF nos ayudó a crear aplicaciones modulares en Silverlight .

A diferencia de mi artículo anterior titulado Diez razones para usar MEF, este es más un catálogo de casos específicos usando MEF en proyectos reales.

 

Inversión de control e inyección de dependencias

Esta fue probablemente la decisión más fácil. Ninguno de los proyectos necesitaba un mecanismo de inversión de control (IoC) o inyección de dependencias más sofisticado que el ofrecido por MEF. Ya que de todos modos íbamos a usarlo por otras razones, y además viene incluido en la plataforma .NET, nos pareció lógico usarlo para resolver este problema. En la mayoría de los casos bastó con simplemente declarar una interfase, exportarla donde es implementada e importarla donde fuese necesario. Describo a continuación otros casos más específicos.

 

Configuración

Para nosotros fue muy importante el manejo de los datos de configuración y MEF nos hizo la tarea fácil. La razón por la que lo menciono en esta categoría es por ser, en realidad, un efecto secundario de la inversión de control. Mediante definir una interfase es posible que sea usada por el resto de código; además, en la etapa de prueba los valores de configuración pueden ser fácilmente simulados. El servicio principal de la aplicación implementa la configuración y usa una combinación de constantes definidas durante la compilación y otros parámetros para cargar los valores. Los consumidores en cualquier parte de la aplicación, incluso si son módulos cargados de forma dinámica y sin tener referencias concretas a al ensamblaje donde reside el mecanismo de configuración, tan sólo necesitan importar la interfase para tener acceso a los valores.

 

Registros de historial

El patrón IoC implementado en MEF también nos ayudó de forma indirecta a implementar un registro de historial. Lo que hicimos fue crear una interfase implementada por un registro de historial y acostumbrarnos a usarlo en vez de Debug.WriteLine, con la ventaja de poder indicar el grado de severidad, la fuente del evento y otros datos auxiliares. Al separar las funciones de este modo pudimos usar al principio un mecanismo simple de registro (por ejemplo, escribir en la ventana de depuración) con la flexibilidad de poder añadir más adelante otros mecanismos como almacenaje aislado, llamadas a otros servicios, y así por el estilo. Así simplificamos el manejo de transacciones desde un punto central sin tener que alterar el resto de la aplicación.

 

Administración de regiones

Antes que nada quiero aclarar que Prism 4.0 posee una excelente infraestructura para el manejo de regiones y no tengo nada en su contra ni es mi intención reinventar algo ya existente. Lo que pasó es que nuestro proyecto tenía sólo unas pocas regiones especificadas por lo que era mejor usar algo más liviano.

El manejo de regiones se compone esencialmente de unos cuantos componentes. Tenemos una interfase que sirve como adaptador para las regiones y que admite clases de tipo contenedor (como Grid o ContentControl y demás). Para crear nuevos adaptadores basta con heredar la interfase, agregar la información adicional de exportación para describirle a MEF el contenedor con el que es compatible, y luego crear eventos como el registro (apuntar la vista a una región), activación (hacer que la vista aparezca en la región) y desactivación (remover la vista). A veces tuvimos que manipular el contenido, o algunos de los elementos secundarios, pero en general el administrador de estados visuales (Visual State Manager) se encarga de esa tarea.

El contenedor es definido como una región usando una propiedad adjunta y cada vista contiene metainformación describiendo a cuál región ha de ser asignada. El administrador de regiones agrupa y coordina todos estos componentes. Si necesito activar una vista, el administrador de regiones usa el patrón de cadena de responsabilidades para encontrar el adaptador de la región a la que pertenece la vista y luego le pide que la despliegue.

 

[RegionManagerExport(typeof(ContentControl))]
public class ContentRegion : RegionBase<ContentControl>

 

El artículo Usando MEF en vez de Prism – Parte 2 da una explicación detallada sobre el manejo de regiones. También pueden ver el artículo en inglés Advanced Silverlight Applications using MEF para una infraestructura de muestra.

 

Integración de servicios

Es común que en aplicaciones empresariales hayan diferentes modelos u objetos del dominio del problema a resolver que comparten operaciones de datos. Por ejemplo, su creación, actualización, borrado y consulta. Por esta razón, pude crear una interfase abstracta IServiceFor<T> que define operaciones de este tipo. Las implementaciones de la interfase son exportadas de manera explícita y el direccionador de servicios se encarga de asignarlas a las entidades correspondientes.

Esto quiere decir que puedo crear un modelo de vista para una entidad, y simplemente solicitar al direccionador de servicios que me provea un servicio que ofrezca las operaciones genéricas, sin tener que preocuparme de cómo fue implementado el servicio. Se hace fácil entonces simular los servicios mientras se desarrollan las operaciones en sí. Por ejemplo, algunos servicios usaron el almacenamiento aislado mientras que otros enviaban mensajes por la red, pero a fin todos fueron desacoplados del modelo de vista y agrupados por MEF.

 

[Import]
public IServiceLocator Locator { get; set; }

var service = Locator.GetServiceFor<T>();

 

Mensajería

Otro aspecto importantísimo en cualquier sistema es la mensajería. Por ejemplo, imaginen que tenemos un modelo de vista que ofrece filtrado de datos y que puede ser usado por otros modelos de vista. ¿Cómo pueden enterarse estos otros modelos de vista cuando cambia la especificación del filtro, o cómo pueden obtener el filtro en sí?

En este proyecto lo hicimos de dos formas.

 

Composición de vistas

Aunque sea aplicado de manera global, un filtro puede ser considerado como parte del modelo de vista que lo use. Si bien por sí mismo no es de utilidad, al añadirlo como un componente de otro modelo de vista puede entonces ser usado en ese contexto. MEF fue muy útil para aplicar este patrón ya que cualquier modelo de vista que necesite usar otro no más necesita importarlo:

 

[Import]
public OtherViewModel Other { get; set; }

 

Por supuesto, todavía no podemos saber cuando el filtro ha cambiado a menos que nos suscribamos al evento de propiedad cambiada. Puede que eso sea razonable algunas veces, pero hay otras en que un mensaje de ese tipo es lo suficientemente importante como para que varios componentes necesiten escucharlos. Para ese fin usamos el patrón agrupador de eventos.

 

Agrupador de eventos (EventAggregator)

Mi versión del agrupador de eventos se basó en un artículo que describe una implementación basada en extensiones reactivas (RX). Le hice algunas modificaciones para que fuera compatible con Silverlight y trabajó de maravilla.

Otro caso común fue usar mensajes para causar cambios en la aplicación. Por ejemplo, para avisar sobre un nuevo filtro o una nueva selección por parte del usuario. Para eso hice una clase dedicada a difundirlos, llevando cuenta del tipo, el mensaje y una enumeración. De esa manera, si quiero anunciar una nueva selección entonces puedo crear una instancia de esta clase e insertar el elemento elegido junto con su tipo y usar la enumeración para indicar que es “select” para luego publicar el evento.

Quien esté interesado en este tipo de mensaje puede entonces escucharlo de esta manera:

 

var query = from evt in EventAggregator.GetEvent<MyEntityEvent>()
  where evt.Type.Equals(typeof(MyWatchedEntity)) &&
        evt.Action.Equals(Actions.Select)
  select evt.Cast<MyWatchedEntity>(); 

query.Subscribe(myEntity=>{/*hacer algo*/});

 

 

Direccionamiento XAPs modulares

El hecho de que las aplicaciones de Silverlight son transferidas a través de la red y almacenadas en la máquina del usuario hacen que el cargado dinámico de módulos XAP sea de gran importancia. Hay dos razones principales: el tiempo de descarga y la cantidad de memoria requerida en el cliente. Mediante usar cargado dinámico, podemos fragmentar la aplicación en módulos XAP y diferir su uso hasta que sean necesarios. Como resultado, se reduce el tiempo inicial de carga de la aplicación y la memoria requerida al principio.

Un reto que experimentan algunas bibliotecas es cuándo y cómo iniciar el cargado del módulo. Por mi parte, yo prefiero un método al que he llamado “direccionamiento dinámico de XAPs.” Es una idea sencilla: asignar etiquetas a las vistas, ya sea usando nombres explicativos o el espacio de nombres completo.  En mi proyecto principal, donde radica la infraestructura, tengo un conjunto de constantes globales que representan las diferentes vistas.  Aunque el módulo no sabe nada sobre la vista, las constantes sirven para referirse a ella. Otro conjunto de constantes describen los archivos XAP disponibles.

Con estas dos pistas puedo entonces usar un servicio de inicialización que encamine de manera dinámica los módulos usando catálogos de distribución para cargarlos. Una ruta de direccionamiento apunta hacia la vista y el archivo XAP que la contiene y es exportada para uso de cualquier componente que necesite cargarla. Cuando se solicita una vista, el administrador de regiones verifica que exista la ruta correspondiente y luego le pide al servicio de direccionamiento que cargue el archivo XAP, demorando la navegación a la vista hasta que haya sido cargada. Esto hace fácil separar la navegación de los módulos, haciendo posible que cualquier área de la aplicación solicite vistas, mientras que la infraestructura se preocupa de cargar los módulos necesarios y proveer las rutas requeridas.

 

Direccionamiento de modelos de vista

Un problema común en MVVM es cómo atar el modelo de vista a la vista. Ninguna de las bibliotecas que he usado provee correlaciones uno-a-uno entre ambos debido a que a veces un modelo de vista sirve a varias vistas. Aunque las vistas dependen de él (se le puede considerar como el contrato de ligado de la vista), el modelo de vista no sabe nada sobre las vistas a las que está asociado. Las vistas son exportadas a la región de destino usando su tipo o con una etiqueta, de igual manera que los modelos de vista son exportados con una etiqueta.

Las rutas son implementadas con un simple objeto que contiene dos etiquetas, una para la vista y otra para el modelo de vista. Ya sea que la vista exporte la etiqueta o que lo haga una clase para direccionamiento, la exportación es al final manejada por un direccionador centralizado. Este direccionador es capaz de extraer vistas y, como todo es cargado de manera diferida, puede determinar si la vista necesita ser atada al modelo de vista. Por otra parte, también provee una referencial al modelo de vista que, por cierto, es creado si es la primera vez que se usa.

El resultado es que la asociación entre la vista y el modelo de vista es completamente desacoplada, y es descrita en otra parte de la aplicación. También significa que el sistema de enrutamiento conoce cuando una vista está siendo desplegada por primera vez de manera que puede llamar métodos de inicialización en el modelo de vista tanto al ser creada como cuando es activada posteriormente. Esto permite que mis vistas reaccionen a cambios o mantenimiento que sea necesario antes de ser desplegadas de nuevo sin tener que depender de la jerarquía visual.

 

[ExportAsViewModel(typeof (DialogViewModel))]
public class DialogViewModel : BaseViewModel

...

[ExportAsView(typeof(MyDialog),Regions.DialogRegion)]
public partial class MyDialog
{
  public MyDialog()
  {
    InitializeComponent();
  }     

  [Export]
  public ViewModelRoute DialogBinding
  {
    get
    {
      return ViewModelRoute.Create<DialogViewModel, MyDialog>();
    }
  }
}

 

Para más detalles pueden ver los artículos Atando el modelo de vista con MEF (en inglés) y Otro patrón de localización de modelos de vista.

 

Desacoplando las vistas

Hay situaciones en las que es necesario hacer referencia a un tipo específico de vista que no es conocida de antemano. Por ejemplo, puede que una infraestructura de navegación dependa del tipo de vista (como en la sección siguiente). Cuando la vista se encuentra en un XAP que todavía no ha sido cargado, no hay manera de obtener una referencia directa a la vista. En este caso, MEF fue muy útil para manipular tipos diferidos, es decir, es posible importar una propiedad de un tipo especifico usando tan sólo una etiqueta (como “MainView” o “DashboardView”) y exportar esa etiqueta en otra parte. Así pudimos separar completamente la interacción con la vista y su tipo.

 

Navegación

MEF provee una infraestructura de navegación muy flexible. Lo que yo quería es un tipo de navegación capaz de entender entidades compuestas, de manera que un evento de navegación no estuviese limitado a páginas en la aplicación sino más bien a vistas, incluso cuando hay vistas dentro de otras.

La navegación coopera con el direccionador de modelos de vista para estar al tanto de cambios en estos últimos. En la infraestructura que desarrollé, el agrupador de eventos basado en MEF simplemente genera un evento de navegación de vista que encapsula el tipo de la vista y un indicador de activación o desactivación. El direccionador, que ha importado todas las vistas y modelos de vista, encuentra la vista y se comunica con el administrador de regiones para activarla o desactivarla. Si es activada entonces también llama el método correspondiente de inicialización en el modelo de vista.

Al generalizar cuadros de diálogo, páginas, y hasta asistentes (Wizards) dentro del concepto de eventos navegación, la tarea se hace increíblemente simple. En este contexto, el que una ventana sea emergente es tan sólo un detalle de implementación independiente del evento en sí, y el modelo de vista simplemente solicita que se navegue a esa vista sin tener que preocuparse de modelos, regiones, o ningún otro detalle propio de la navegación.

 

Autorización

La mayoría de las aplicaciones de negocios requieren servicios de autenticación y autorización. Por lo general, el usuario tiene propiedades como roles y credenciales que son verificados a la hora de obtener acceso a diferentes partes de la aplicación. MEF hace fácil el difundir las credenciales ya que el el usuario puede ser exportado como un objeto concreto, o como una interfase, y luego importado o consultado por las áreas de la aplicación que requieran saber el nivel de autorización.

 

Comandos

Los comandos pueden ir desde los que están fuertemente entrelazados con la interfase al usuario, hasta los que son tan independientes que pueden residir en módulos aún no cargados. Por ejemplo, comandos como volver al inicio (“home”) o expandir requieren eventos de navegación que conozcan la vista de destino.

 

Comandos de enlace diferido

El mecanismo de exportación e importación de MEF nos ayudó a implementar este tipo de enlace, también conocido como enlace tardío. En algunos casos el modelo de vista importa el comando usando una etiqueta predeterminada (por ejemplo “home”) aun si la vista se encuentra en un módulo diferente. Cuando es cargado, el módulo exporta el comando haciéndolo disponible para ser enlazado al modelo de vista. De esta manera es diferido, pues el comando no se puede usar hasta que el módulo que lo contiene es cargado.

 

[Import(Contracts.COMMAND_GOHOME, AllowDefault = true)]
public DelegateCommand<object> HomeCommand { get; set; }
...
[Export(Contracts.COMMAND_GOHOME)]
public DelegateCommand<object> HomeCommandExport
{
  get
  {
    return
      new DelegateCommand<object>(obj =>
            EventAggregator.Publish(
                typeof(HomeView).AsViewNavigationArgs()));
  }
}

 

Comandos globales

Hay otros comandos que son de naturaleza general, por lo que es mejor crear una sola implementación global en vez de duplicarlo por todas partes. Los comandos globales pueden ser exportados y luego importados donde sean necesarios, de manera que una copia única es usada, en vez de múltiples instancias locales. Usando de nuevo el ejemplo “home”, los modelos de vista pueden ofrecer ese servicio a la vista sin conocer el módulo o ensamblaje que contiene su implementación dejando que MEF importe el comando cuando ya está disponible.

Para más detalles pueden consultar el artículo Sharing Commands with MEF.

 

Cadena de responsabilidades

El patrón cadena de responsabilidades es una poderosa herramienta para administrar mensajes comunes que son procesados de manera diferente dependiendo de sus características. En este caso, usé MEF para proveer un mecanismo de administración que define un contrato o interfase estándar que consume ciertos tipos específicos de mensajes. Cada administrador exporta el contrato junto con metainformación describiendo el tipo de mensajes con los que es compatible. Un enrutador central se encarga de importar los administradores y escuchar los mensajes despachándolos a los administradores apropiados hasta que alguno responda diciendo que lo pudo procesar correctamente. En combinación con los comandos globales, este mecanismo provee un alto grado de extensibilidad a la aplicación.

Por ejemplo, imaginemos un comando “imprimir” que está disponible a toda la aplicación pero está enlazado al elemento que va a ser impreso. Hay varios controladores que han exportado el contrato de impresión y que saben cómo manejar distintos tipos de datos. Cuando el controlador que maneja el objeto de destino es invocado, formatea la información para poder ser impresa y envía el resultado a la impresora. Para manejar un nuevo tipo de datos, basta con crear un nuevo controlador y exportar el contrato o interfase de impresión.

 

Fábricas de clases con resolución de dependencias

Gran cantidad de clases tienen dependencias en varios niveles. Un caso puede ser una vista compuesta donde se presenta una lista de artículos, y cada artículo es expuesto mediante un modelo de vista asociado que tiene dependencias en el agrupador de eventos, el bus de servicios, y otros contratos que son importados usando MEF. Tal escenario es simplificado gracias a ExportFactory. Con esa herramienta se puede crear un modelo de vista con todo y resolución de dependencias, mientras iteramos una lista de datos. Los datos son incorporados al modelo de vista y éste es enlazado con la vista correspondiente; en todo esto, MEF se encarga de resolver las dependencias.

 

Vistas compatibles con la fase de diseño

Si se trabaja en paralelo con un equipo de diseñadores, es sumamente importante tener vistas que se comporten adecuadamente en la fase o tiempo de diseño. Es fácil crear vistas que pueden ser asociadas con modelos de vista diferentes durante la etapa de diseño usando los atributos d:Design y d:DesignInstance, mientras que MEF asocia el modelo de vista real durante la ejecución. Para este fin se puede usar la interfase IPartImportsSatisfied, evitando que en las vistas y modelos de vista se invoque código destinado al tiempo de ejecución.

Para más detalles pueden ver el artículo (en inglés) Usando MEF para crear modelos de vista compatibles con la fase de diseño.

 

Motor de validación

Los motores de validación por lo general exponen formas de verificar si se han violado reglas específicas. Con MEF fue fácil crear un mecanismo de validación flexible y extensible, exportando un contrato de uso para cada regla de validación. El motor entonces importa todas las reglas y provee acceso usando una interfase fluida. Pueden leer el artículo (en inglés) Validación fluida con MEF y PRISM para más detalles.

 

Pruebas unitarias

MEF fue de importancia crítica en nuestro éxito con las pruebas unitarias, principalmente por exponer como contratos todas las dependencias importadas. Durante las pruebas lo que hicimos fue no invocar a MEF, dejando que el sistema fuera inicializado usando componentes simulados o parciales.  Más información en el artículo (en inglés) Pruebas unitarias avanzadas en Silverlight usando Moq.

 

Conclusión

Obviamente muchos de estos ejemplos podrían ser implementados usando otras herramientas aparte de la Infraestructura de extensibilidad administrada. Una ventaja de MEF es que viene incluida en la plataforma de ejecución: no es una biblioteca provista por terceros, sino que es parte fundamental de .NET y de Silverlight 4.0. Basándome en mi experiencia al diseñar varias aplicaciones de gran escala en Silverlight, diría que MEF no sólo facilitó el hacerlas modulares, sino que también, al incluir tantas utilidades, redujo el tiempo de desarrollo. En especial con la habilidad de etiquetar los componentes exportados con metainformación y poder importar múltiples componentes usando el mismo contrato. Espero que esto les ayude con algunas ideas sobre cómo usar MEF en sus propias aplicaciones, y que también sirva para demostrar de que MEF está siendo usando con gran éxito en la producción de aplicaciones empresariales.

 

Jeremy Likness

 

Etiquetas asignadas:
 

Responder



Licencia de uso

El contenido de las traducciones está sujeto a los términos de protección de derechos de uso de los autores originales quienes han autorizado su publicación en este blog. Asegúrese de entender los terminos de la licencia de cada autor antes de usar tal contenido.

Mis propios artículos son publicados bajo los términos de la Licencia Reconocimiento-Compartir bajo la misma licencia 3.0 Estados Unidos de Creative Commons:

Creative Commons License
Blog de Maromas Digitales by Maromas Digitales, LLC is licensed under a Creative Commons Reconocimiento-Compartir bajo la misma licencia 3.0 Estados Unidos License.

License

The contents of all translated articles is subject to the copyright and licensing terms of the original authors and has been published here with their express permission. Verify the original author's licensing terms before using the contents of these articles.

My own articles are licensed under the Creative Commons Attribution-Share Alike 3.0 United States License:

Creative Commons License
Blog de Maromas Digitales by Maromas Digitales, LLC is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.