Febrero de 2019

Volumen 34, número 2

[El programador ocupado]

Programación con Naked: oradores en Naked

Por Ted Neward| Febrero de 2019

Ted NewardEn el último artículo, presenté el marco de objetos Naked (NOF), un marco que pretende esconder todo el trabajo relacionado con la persistencia de los datos y la UI de los objetos, y dejar que el desarrollador se centre únicamente en la creación de objetos de dominio tal y como les enseñaron, antes de aprender a trabajar con MVC, API web, HTTP, SQL, administración de conexiones de base de datos, inyección de dependencias y el resto de cosas que consumen la mayor parte de nuestro tiempo y energía. Es una buena idea, si funciona, pero el último artículo no estaba cargado de características, precisamente. En este artículo, examinaré en mayor profundidad lo que se puede definir en una clase con NOF y cómo eso se transforma en persistencia de base de datos y UI.

Conceptos de Naked

Antes de adentrarme demasiado en el código, familiaricémonos con ciertos términos de NOF, ya que ofrecen conclusiones sobre la forma de ver el mundo de los responsables de la creación y el mantenimiento.

En primer lugar, el centro de toda la experiencia son los objetos de dominio: los objetos que representan el estado y el comportamiento del sistema. En NOF, cualquier "antiguo objeto C# sencillo" puede ser un objeto de dominio, siempre que siga unas reglas para que NOF pueda proporcionar la compatibilidad que necesita: las propiedades deben ser virtuales, es necesario inicializar los miembros de cualquier colección, y NOF no recomienda el uso de constructores, ya que el objeto en memoria puede tener un ciclo de vida distinto al que conoce. Recuperaré este tema más adelante.

Sin embargo, no todo encaja bien en el paradigma de objetos. A menudo hay comportamientos que funcionan en objetos, pero no se ajustan especialmente bien dentro de uno. En lugar de intentar forzar comportamientos en objetos de forma arbitraria, NOF proporciona compatibilidad con servicios que ofrecen comportamientos "fuera" de los objetos, como crear, recuperar o realizar otras operaciones con objetos de dominio. Los servicios también pueden actuar como puerta de enlace a servicios externos del sistema, como servidores de correo electrónico.

Desde la perspectiva del usuario, la UI debe proporcionar comportamientos que los usuarios puedan desencadenar (denominados, colectivamente, acciones) y que, a menudo, se corresponden con los menús de la UI. Las acciones se pueden detectar (o aportar) desde diversos orígenes, pero lo más frecuente es que se encuentren mediante reflexión en servicios u objetos de dominio y que se muestren en la UI como se han encontrado. Puede usar atributos personalizados para adaptar gran parte de la detección y presentación cuando y donde sea necesario. Iremos profundizando en el tema sobre la marcha.

Todo esto se verá más claro con el código, así que... ¿empezamos?

Conferencia de Naked

En la serie sobre MEAN, usé un sencillo ejemplo de dominio (oradores que dan charlas en una conferencia) como fondo para generar código. En este caso, parece razonable utilizar el mismo ejemplo, simplemente, para comparar manzanas con manzanas. En cuanto a los modelos de dominio, simplificaré: los oradores tienen nombre, apellido, edad, temas (C#, VB, Java, JavaScript, TypeScript y C++) y clasificación media (de cero a cinco), para empezar. Más tarde, agregaré Presentaciones, que pueden tener un título y una descripción, una vez que vea cómo configurar las relaciones entre objetos, pero, de momento, simplifiquemos.

Como mencioné en el último artículo, para comenzar, usaré la plantilla de NOF .zip como inicialización desde la que empezar, así que deberá descargarla del vínculo del archivo README del proyecto de GitHub (bit.ly/2PMojiQ), descomprimirla en un subdirectorio recién creado y abrir la solución en Visual Studio. Ahora, abra el nodo del proyecto Template.Model y elimine los archivos Student.cs y ExampleService.cs. Después de todo, no quiere almacenar alumnos. Lo que quiere almacenar son oradores y, más tarde, presentaciones, pero dejaré eso para luego, para la refactorización, solo para mostrar lo bien que admite la refactorización NakedObjects.

Es necesario aclarar que debo hacer un par de cosas para admitir oradores como tipo de dominio en un proyecto de objetos Naked: debo crear el tipo de dominio, debo asegurarme de tener un objeto de repositorio para conseguirlos y crearlos, tengo que asegurarme de que haya un repositorio disponible para el marco de objetos Naked desde el que se generarán los menús y, finalmente, necesito (o, en realidad, quiero) inicializar la base de datos con algunos oradores.

Objetos de orador

En realidad, crear la clase Speaker es el más sencillo de los cuatro pasos. En el proyecto Template.Model, cree un archivo Speaker.cs y, en ese archivo, dentro del espacio de nombres Template.Model, cree una clase pública denominada Speaker (orador). De momento, los oradores se definen por nombre, apellido y edad, así que debe crear tres propiedades para ellos, pero marcarlos como "virtuales" para que, en tiempo de ejecución, NakedObjects pueda agregar comportamientos (a través de una subclase) que obren el milagro deseado.

Ahora vamos a agregar cierta complejidad a la clase Speaker. En primer lugar, a menudo es recomendable usar un identificador no relacionado con el dominio para los objetos almacenados en una base de datos que se puede usar como clave principal, sin permitir que los usuarios utilicen o modifiquen ese identificador. En el sistema NakedObjects, puedo marcar cualquier propiedad con un atributo personalizado "NakedObjectsIgnore" para que no se muestre en la UI, así que vamos a agregar una propiedad "Id" de entero (de nuevo, virtual) marcada con este atributo. Además, no es poco frecuente querer crear una propiedad que se procese con otros elementos de datos, así que vamos a agregar una propiedad "FullName" de solo lectura que concatene el nombre y el apellido. Dado que NakedObjects no será responsable de la edición de la UI en este caso, no hace falta que sea virtual.

Al acabar, debe tener algo parecido a esto:

public class Speaker
{
  [NakedObjectsIgnore]
  public virtual int Id { get; set; }

  public virtual string FirstName { get; set; }
  public virtual string LastName { get; set; }
  public virtual int Age { get; set; }

  [Title]
  public string FullName { get { return FirstName + " " + LastName; } }
}

Ningún problema por el momento.

Repositorio de oradores

Un objeto de repositorio, para aquellos que no estén familiarizados con el patrón, es, básicamente, un objeto que representa el acceso a la base de datos y proporciona llamadas al método sencillas para las solicitudes de base de datos estándar. En el sistema NakedObjects, un objeto de repositorio es una forma de servicio, que es un objeto que NakedObjects reconoce específicamente como un objeto que no es un objeto de dominio, pero puede proporcionar comportamientos y elementos de UI en nombre del sistema. En este caso, el repositorio proporcionará algunos elementos de menú para sacar a los oradores de la base de datos, ordenados (por ejemplo) por apellido.

En términos prácticos, un servicio NakedObject es, simplemente, otra clase, pero tendrá algo que la clase Speaker no tiene: una referencia a un objeto IDomainObjectContainer, que es un objeto que proporciona el marco NakedObjects que, a su vez, proporciona acceso al resto del sistema. La lista de servicios del sistema disponibles se proporciona en la documentación de NakedObjects, pero, en concreto, me interesan dos elementos: el método NewTransientInstance, que construye un nuevo objeto Speaker y lo conecta con el resto de la infraestructura de NakedObjects para usarlo; y el método Instances, que devuelve el conjunto completo de objetos Speaker de la base de datos. De este modo, una clase SpeakerRepository mínima tiene el aspecto que se muestra en la figura 1.

Figura 1 Clase SpeakerRepository

public class SpeakerRepository
{
  public IDomainObjectContainer Container { set; protected get; }

  public Speaker CreateNewSpeaker()
  {
    // 'Transient' means 'unsaved' - returned to the user
    // for fields to be filled in and the object saved.
    return Container.NewTransientInstance<Speaker>();
  }

  public IQueryable<Speaker> AllSpeakers()
  {
    return Container.Instances<Speaker>();
  }
}

Tenga en cuenta que los métodos NewTransientInstance e Instances son funciones genéricas y utilizan el tipo de objeto con el que trabajan como parámetro de tipo para la llamada a la función. Esto es
necesario porque DomainObjectContainer puede usarse, y se usará, para distintos tipos de objeto de dominio, no solo Speaker.

Un repositorio que, simplemente, crea un nuevo orador y obtiene una lista de todos ellos no resulta especialmente útil. Sin embargo, puede que quiera recuperar objetos con otros filtrados u ordenados de un modo concreto. Aquí es donde se manifiestan las posibilidades de LINQ y Entity Framework dentro el marco NakedObjects. Dado que el método AllSpeakers devuelve un valor IQueryable, puedo usar los métodos de LINQ para filtrar y ordenar los objetos devueltos, por lo que, si quiero buscar oradores por apellido, puedo, simplemente, agregar ese método a la clase de repositorio usando LINQ como sea necesario para filtrar los resultados:

public class SpeakerRepository
{
  // ... as before

  public IQueryable<Speaker> FindSpeakerByLastName(string name)
  {
    return AllSpeakers().
      Where(c => c.LastName.ToUpper().Contains(name.ToUpper()));
  }
}

Cualquier cosa que pueda hacer LINQ se puede expresar como un método en el repositorio.

Contexto de orador

Hay dos detalles más de registro que debo tener en cuenta: Necesito configurar DbContext de Entity Framework para que pueda usarlo el motor de persistencia de NakedObject, y tengo que enlazar SpeakerRepository a la UI para tener elementos de menú que permitan crear y buscar oradores, etc.

El primero se encuentra en el proyecto Template.DataBase, en el archivo ExampleDbContext.cs (cuyo nombre puede cambiar libremente si quiere), en la clase ExampleDbContext. Como propiedad pública de la clase, es necesario parametrizar un elemento DbSet con el tipo de orador denominado "Speakers" como se indica a continuación:

namespace Template.DataBase
{
  public class ExampleDbContext : DbContext
  {
    public ExampleDbContext(string dbName,
      IDatabaseInitializer<ExampleDbContext> initializer) : base(dbName)
    {
      Database.SetInitializer(initializer);
    }

    public DbSet<Speaker> Speakers { get; set; }
  }
}

A medida que se agregan más tipos de objeto de dominio de nivel superior al sistema, necesitaré agregar más propiedades que devuelvan un elemento DbSet, pero, por ahora, esto es todo lo que necesito.

Si quiere que la base de datos se inicialice con algunos oradores para empezar, debe abrir el proyecto Template.SeedData, buscar el archivo ExampleDbInitializer.cs y escribir el método Seed (que ya está definido en ese archivo) para agregar algunos objetos Speaker a ExampleDbContext (el objeto que acabo de refactorizar en el párrafo anterior) mediante el elemento DbSet de oradores, como se muestra en la figura 2.

Figura 2 Adición de objetos Speaker a DbContex

public class ExampleDbInitializer : 
  DropCreateDatabaseIfModelChanges<ExampleDbContext>
{
  private ExampleDbContext Context;
  protected override void Seed(ExampleDbContext context)
    {
    this.Context = context;

    Context.Speakers.Add(new Speaker() {
      FirstName = "Ted", LastName = "Neward", Age = 47
    });
  }
}

Tenga en cuenta que se trata de código directo de Entity Framework, por lo que cualquier explicación más detallada acerca de cómo cambiar la directiva de colocación o creación de la base de datos correspondería al conjunto de documentación de Entity Framework.

El último sitio en que debo escribir código es en el proyecto Template.Server, en el archivo NakedObjectsRunSettings.cs. Este código es una colección de enlaces de registro o sugerencias para el sistema NakedObjects que describen lo que el marco de objetos Naked debe saber. Por ejemplo, si decide que "Template.Model" es un espacio de nombres terrible para los objetos del modelo para esta aplicación de conferencia, puede cambiarlo libremente, siempre y cuando el espacio de nombres del modelo de este elemento NakedObjectsRunSettings también refleje el cambio del nombre.

Con respecto a SpeakerRepository, necesito que el elemento NakedObjects lo conozca. Para ello, lo agrego a la matriz de tipos "Servicios". Si quiero que los métodos SpeakerRepository estén accesible a través de una opción de menú, también debo indicar a NakedObjects que examine SpeakerRepository en busca de acciones que se puedan usar como elementos de menú, por lo que debo incluirlos como parte de la matriz de elementos de menú que se devuelve desde el método MainMenus, como se muestra en la figura 3.

Figura 3 Configuración de ejecución de NakedObjects

public class NakedObjectsRunSettings
{
  // ...

  private static Type[] Services
  {
    get
    {
      return new Type[] {
        typeof(SpeakerRepository)
      };
    }
  }

  // ...

  public static IMenu[] MainMenus(IMenuFactory factory)
  {
    return new IMenu[] {
      factory.NewMenu<SpeakerRepository>(true, "Menu")
    };
  }
}
}

Una vez aplicados estos cambios, puedo activar los proyectos, usar el cliente y crear algunos oradores. Tenga en cuenta que se muestra la propiedad FullName, construida a partir de FirstName y LastName, pero no aparece el identificador. Además, gracias al método FindSpeakersByLastName en SpeakerRepository, puedo buscar oradores por apellido y, si se devuelve más de uno, el cliente sabe automáticamente cómo presentarlos en una lista que permite realizar selecciones para el trabajo individual.

Resumen

Tenga en cuenta que no hice nada con la plantilla en ningún punto del artículo. Proyecto de cliente: esa es la idea. La UI se construye a partir de servicios y objetos de dominio, con información complementaria que proporcionan los atributos personalizados (en los que se profundizará en columnas posteriores). De un modo similar, la persistencia se crea a partir de la estructura y los metadatos que el framework detecta a partir de los tipos.

Lo que necesita ver, a partir de ahora, es cómo NOF gestiona escenarios de dominio más complejos, como oradores con una lista de temas sobre los que pueden hablar y una colección de presentaciones que pueden hacer, y cómo presenta una UI para ellos. En el siguiente artículo, me centraré en algunos de estos temas. Pero, mientras tanto… ¡Que disfrute programando!


Ted Neward es un consultor de politecnología, orador y mentor residente en Seattle. Ha escrito una gran cantidad de artículos, es autor y coautor de una docena de libros y realiza conferencias por todo el mundo. Puede ponerse en contacto con él en ted@tedneward.com o leer su blog en blogs.tedneward.com.


Comente este artículo en el foro de MSDN Magazine