2011-04-21 29 views
22

Recientemente comencé a leer el libro de diseño impulsado por el dominio de Evans y comencé un pequeño proyecto de muestra para adquirir cierta experiencia en DDD. Al mismo tiempo, quería aprender más sobre MongoDB y comencé a reemplazar mis repositorios SQL EF4 con MongoDB y el último controlador oficial C#. Ahora esta pregunta es sobre el mapeo de MongoDB. Veo que es bastante fácil mapear objetos simples con captadores y decodificadores públicos, sin dolor. Pero tengo dificultades para mapear entidades de dominio sin setters públicos. Como aprendí, el único enfoque realmente limpio para construir una entidad válida es pasar los parámetros requeridos al constructor. Considere el siguiente ejemplo:C# MongoDB: ¿Cómo correlacionar correctamente un objeto de dominio?

public class Transport : IEntity<Transport> 
{ 
    private readonly TransportID transportID; 
    private readonly PersonCapacity personCapacity; 

    public Transport(TransportID transportID,PersonCapacity personCapacity) 
    { 
     Validate.NotNull(personCapacity, "personCapacity is required"); 
     Validate.NotNull(transportID, "transportID is required"); 

     this.transportID = transportID; 
     this.personCapacity = personCapacity; 
    } 

    public virtual PersonCapacity PersonCapacity 
    { 
     get { return personCapacity; } 
    } 

    public virtual TransportID TransportID 
    { 
     get { return transportID; } 
    } 
} 


public class TransportID:IValueObject<TransportID> 
{ 
    private readonly string number; 

    #region Constr 

    public TransportID(string number) 
    { 
     Validate.NotNull(number); 

     this.number = number; 
    } 

    #endregion 

    public string IdString 
    { 
     get { return number; } 
    } 
} 

public class PersonCapacity:IValueObject<PersonCapacity> 
{ 
    private readonly int numberOfSeats; 

    #region Constr 

    public PersonCapacity(int numberOfSeats) 
    { 
     Validate.NotNull(numberOfSeats); 

     this.numberOfSeats = numberOfSeats; 
    } 

    #endregion 

    public int NumberOfSeats 
    { 
     get { return numberOfSeats; } 
    } 
} 

Obviamente, la automatización no funciona aquí. Ahora puedo mapear esas tres clases a mano a través del BsonClassMaps y se guardarán bien. El problema es que, cuando quiero cargarlos desde el DB, tengo que cargarlos como BsonDocuments y analizarlos en mi objeto de dominio. Intenté muchas cosas, pero finalmente no conseguí una solución limpia. ¿Realmente tengo que producir DTOs con getters/setters públicos para MongoDB y asignarlos a mis objetos de dominio? Tal vez alguien me puede dar algunos consejos sobre esto.

Respuesta

14

Es posible serializar/deserializar clases donde las propiedades son de solo lectura. Si intenta mantener la persistencia de objetos de su dominio ignorante, no querrá usar BsonAttributes para guiar la serialización, y como señaló que AutoMapping requiere propiedades de lectura/escritura, entonces tendría que registrar los mapas de clase usted mismo. Por ejemplo, la clase:

public class C { 
    private ObjectId id; 
    private int x; 

    public C(ObjectId id, int x) { 
     this.id = id; 
     this.x = x; 
    } 

    public ObjectId Id { get { return id; } } 
    public int X { get { return x; } } 
} 

se puede mapear usando el siguiente código de inicialización:

BsonClassMap.RegisterClassMap<C>(cm => { 
    cm.MapIdField("id"); 
    cm.MapField("x"); 
}); 

Tenga en cuenta que los campos privados no pueden ser de sólo lectura. Tenga en cuenta también que la deserialización puentea su constructor e inicializa directamente los campos privados (la serialización .NET también funciona de esta manera).

Aquí hay un ejemplo de programa completo que prueba esto:

http://www.pastie.org/1822994

+0

¡Eso funciona! No puedo creer que me haya perdido esto. Supongo que para las aplicaciones que no son a gran escala, la eliminación de "solo lectura" de los campos privados es una compensación aceptable para no tener que crear una capa adicional de DTO. Como señaló, hay que tener cuidado de no construir objetos no válidos evitando el ctor. Sin embargo, me gustaría agradecer a Bryan y Niels por sus respuestas también. Todos ustedes me hicieron un poco más inteligente, gracias. – hoetz

+2

Pequeño punto: eliminar el campo de solo lectura lo hace equivalente a 'public Id Id del objeto {get; conjunto privado; } 'auto-property y el campo se puede eliminar por completo. –

+1

Si bien esta es una posible solución, ** aún influye en su modelo **, en el ejemplo concreto que dio al requerir que los campos no sean 'readonly'. Cada equipo debe decidir por sí mismo si esto es aceptable para su proyecto. – theDmi

3

Me gustaría ir con el análisis de los documentos BSON y mover la lógica de análisis a una fábrica.

Primero defina una clase base de fábrica, que contiene una clase de constructor. La clase de constructor actuará como DTO, pero con una validación adicional de los valores antes de construir el objeto de dominio.

public class TransportFactory<TSource> 
{ 
    public Transport Create(TSource source) 
    { 
     return Create(source, new TransportBuilder()); 
    } 

    protected abstract Transport Create(TSource source, TransportBuilder builder); 

    protected class TransportBuilder 
    { 
     private TransportId transportId; 
     private PersonCapacity personCapacity; 

     internal TransportBuilder() 
     { 
     } 

     public TransportBuilder WithTransportId(TransportId value) 
     { 
      this.transportId = value; 

      return this; 
     } 

     public TransportBuilder WithPersonCapacity(PersonCapacity value) 
     { 
      this.personCapacity = value; 

      return this; 
     } 

     public Transport Build() 
     { 
      // TODO: Validate the builder's fields before constructing. 

      return new Transport(this.transportId, this.personCapacity); 
     } 
    } 
} 

Ahora, cree una subclase de fábrica en su repositorio. Esta fábrica construirá objetos de dominio a partir de los documentos BSON.

public class TransportRepository 
{ 
    public Transport GetMostPopularTransport() 
    { 
     // Query MongoDB for the BSON document. 
     BsonDocument transportDocument = mongo.Query(...); 

     return TransportFactory.Instance.Create(transportDocument); 
    } 

    private class TransportFactory : TransportFactory<BsonDocument> 
    { 
     public static readonly TransportFactory Instance = new TransportFactory(); 

     protected override Transport Create(BsonDocument source, TransportBuilder builder) 
     { 
      return builder 
       .WithTransportId(new TransportId(source.GetString("transportId"))) 
       .WithPersonCapacity(new PersonCapacity(source.GetInt("personCapacity"))) 
       .Build(); 
     } 
    } 
} 

Las ventajas de este enfoque:

  • El constructor es responsable de construir el objeto de dominio. Esto le permite mover alguna validación trivial del objeto de dominio, especialmente si el objeto de dominio no expone ningún constructor público.
  • La fábrica es responsable de analizar los datos de origen.
  • El objeto de dominio puede centrarse en reglas de negocio. No le molesta el análisis ni la validación trivial.
  • La clase abstracta de fábrica define un contrato genérico, que se puede implementar para cada tipo de fuente de datos que necesite. Por ejemplo, si tiene que interactuar con un servicio web que devuelve XML, que acaba de crear una nueva subclase de fábrica:

    public class TransportWebServiceWrapper 
    { 
        private class TransportFactory : TransportFactory<XDocument> 
        { 
         protected override Transport Create(XDocument source, TransportBuilder builder) 
         { 
          // Construct domain object from XML. 
         } 
        } 
    } 
    
  • La lógica de análisis de los datos de origen se encuentra cerca de donde se origina de datos, es decir, el análisis sintáctico de documentos BSON está en el repositorio, el análisis de XML está en el contenedor del servicio web. Esto mantiene la lógica relacionada agrupada.

Algunas desventajas:

  • no he probado este enfoque en proyectos grandes y complejos, sin embargo, sólo en proyectos de pequeña escala. Puede haber algunas dificultades en algunos escenarios que aún no he encontrado.
  • Es bastante código para algo aparentemente simple. Especialmente los constructores pueden crecer bastante grandes.Puede reducir la cantidad de código en los constructores convirtiendo todos los métodos WithXxx() a propiedades simples.
+0

Concepto muy interesante, supongo que realmente no hay otra manera que agregar una capa. En la aplicación de demostración C# DDD del libro de Evans, usaron nHibernate y me encanta el concepto de NO tener que hacer esto.Lanzas tus objetos de dominio inalterados y los recuperas sin ninguna clase adicional (excepto el mapeo xml, por supuesto) – hoetz

-1

Niels tiene una solución interesante, pero propongo un enfoque muy diferente: simplificar su modelo de datos.

Lo digo porque está tratando de convertir entidades de estilo RDBMS a MongoDB y no se correlaciona muy bien, como ha encontrado.

Una de las cosas más importantes a tener en cuenta al usar cualquier solución NoSQL es su modelo de datos. Necesita liberar su mente de gran parte de lo que sabe sobre SQL y las relaciones y pensar más acerca de los documentos integrados.

Y recuerde, MongoDB no es la respuesta correcta para cada problema, así que trate de no forzarlo. Los ejemplos que está siguiendo pueden funcionar muy bien con servidores SQL estándar, pero no te mates tratando de descubrir cómo hacer que funcionen con MongoDB, probablemente no. En cambio, creo que un buen ejercicio sería tratar de encontrar la forma correcta de modelar los datos de ejemplo con MongoDB.

+4

Pero con DDD, no debería preocuparme por la persistencia. No diseño las entidades de mi dominio con una cierta tecnología de almacenamiento en mente. – hoetz

+0

@Malkier: Correcto, no debes dejar que la persistencia influya en el diseño de tu dominio. Es por eso que creo que MongoDB es realmente una muy buena opción, porque su modelo de almacenamiento es mucho más natural. Con RDBMS, debe separar sus entidades para almacenarlas en un modelo de datos normalizado. Con MongoDB, puede simplemente almacenar una raíz agregada completa en un solo documento, quizás con algunas referencias a otras entidades secundarias. –

+0

No podría estar más de acuerdo con @Niels. Pero creo que vale la pena mencionar que, aunque en teoría no debería preocuparse por su capa de persistencia, en el mundo real, definitivamente es algo en lo que pensar, sobre todo cuando se considera una solución NoSQL. –

2

Un mejor enfoque para el manejo de este momento está usando MapCreator (que posiblemente fue añadido después de la mayoría de estos las respuestas fueron escritas).

p. Ej. Tengo una clase llamada Time con tres propiedades de solo lectura: Hour, Minute y Second. Así es como obtengo almacenar esos tres valores en la base de datos y construir nuevos objetos Time durante la deserialización.

BsonClassMap.RegisterClassMap<Time>(cm => 
{ 
    cm.AutoMap(); 
    cm.MapCreator(p => new Time(p.Hour, p.Minute, p.Second)); 
    cm.MapProperty(p => p.Hour); 
    cm.MapProperty(p => p.Minute); 
    cm.MapProperty(p => p.Second); 
} 
+2

¿Por qué usa 'AutoMap()' si todas las propiedades están mapeadas explícitamente? –

Cuestiones relacionadas