¿Guardar un objeto mediante un método propio o mediante otra clase?

38

Si quiero guardar y recuperar un objeto, ¿debería crear otra clase para manejarlo, o sería mejor hacerlo en la clase misma? ¿O tal vez mezclando ambos?

¿Cuál se recomienda según el paradigma de OOD?

Por ejemplo

Class Student
{
    public string Name {set; get;}
    ....
    public bool Save()
    {
        SqlConnection con = ...
        // Save the class in the db
    }
    public bool Retrieve()
    {
         // search the db for the student and fill the attributes
    }
    public List<Student> RetrieveAllStudents()
    {
         // this is such a method I have most problem with it 
         // that an object returns an array of objects of its own class!
    }
}

Versus. (Sé que se recomienda lo siguiente, sin embargo, me parece un poco en contra de la cohesión de la Studentclase)

Class Student { /* */ }
Class DB {
  public bool AddStudent(Student s)
  {

  }
  public Student RetrieveStudent(Criteria)
  {
  } 
  public List<Student> RetrieveAllStudents()
  {
  }
}

¿Qué tal mezclarlos?

   Class Student
    {
        public string Name {set; get;}
        ....
        public bool Save()
        {
            /// do some business logic!
            db.AddStudent(this);
        }
        public bool Retrieve()
        {
             // build the criteria 
             db.RetrieveStudent(criteria);
             // fill the attributes
        }
    }
Ahmad
fuente
3
Debería investigar un poco sobre el patrón de registro activo, como esta pregunta o esta
Eric King,
@gnat, pensé en preguntar cuál es mejor en función de la opinión, luego agregué "pros y contras" y no tengo ningún problema para eliminarlo, pero de hecho me refiero al paradigma de OOD que se recomienda
Ahmad,
3
No hay una respuesta correcta. Depende de sus necesidades, sus preferencias, cómo se utilizará su clase y hacia dónde ve su proyecto. Lo único correcto de OO es que puede guardar y recuperar como objetos.
Dunk

Respuestas:

34

Principio de responsabilidad única , separación de preocupaciones y cohesión funcional . Si lees sobre estos conceptos, la respuesta que obtienes es: sepáralos .

Una razón simple para separar la Studentclase de la "DB" (o StudentRepository, para seguir las convenciones más populares) es permitirle cambiar sus "reglas de negocios", presentes en la Studentclase, sin afectar el código responsable de la persistencia, y viceversa. viceversa

Este tipo de separación es muy importante, no solo entre las reglas comerciales y la persistencia, sino también entre las muchas preocupaciones de su sistema, para permitirle realizar cambios con un impacto mínimo en módulos no relacionados (mínimo porque a veces es inevitable). Ayuda a construir sistemas más robustos, más fáciles de mantener y más confiables cuando hay cambios constantes.

Al mezclar las reglas de negocio y la persistencia, ya sea una sola clase como en su primer ejemplo, o DBcomo una dependencia de Student, está acoplando dos preocupaciones muy diferentes. Puede parecer que pertenecen juntos; parecen ser coherentes porque usan los mismos datos. Pero aquí está la cosa: la cohesión no puede medirse únicamente por los datos que se comparten entre los procedimientos, también debe considerar el nivel de abstracción en el que existen. De hecho, el tipo ideal de cohesión se describe como:

La cohesión funcional es cuando las partes de un módulo se agrupan porque todas contribuyen a una única tarea bien definida del módulo.

Y claramente, realizar validaciones Studentdurante un tiempo también persistiendo no forma "una sola tarea bien definida". De nuevo, las reglas de negocio y los mecanismos de persistencia son dos aspectos muy diferentes del sistema, que según muchos principios de buen diseño orientado a objetos, deben mantenerse separados.

Recomiendo leer sobre Arquitectura limpia , ver esto habla sobre el Principio de responsabilidad única (donde se usa un ejemplo muy similar), y ver esto también sobre Arquitectura limpia . Estos conceptos resumen las razones detrás de tales separaciones.

MichelHenrich
fuente
11
Ah, pero la Studentclase debería estar correctamente encapsulada, ¿sí? ¿Cómo puede una clase externa gestionar la persistencia del estado privado? La encapsulación tiene que equilibrarse con SoC y SRP, pero simplemente elegir uno sobre el otro sin sopesar cuidadosamente las compensaciones probablemente sea incorrecto. Una posible solución a este enigma es usar accesos privados de paquete para que el código de persistencia lo use.
amon
1
@amon Estoy de acuerdo en que no es tan simple, pero creo que es una cuestión de acoplamiento, no de encapsulación. Por ejemplo, puede hacer que la clase Student sea abstracta, con métodos abstractos para cualquier dato que la empresa necesite, que luego implementa el repositorio. De esta manera, la estructura de datos de la base de datos está completamente oculta detrás de la implementación del repositorio, por lo que incluso se encapsula desde la clase Student. Pero, al mismo tiempo, el repositorio está profundamente acoplado a la clase de Estudiantes: si se cambia algún método abstracto, la implementación también tiene que cambiar. Se trata de compensaciones. :)
MichelHenrich
44
La afirmación de que SRP hace que los programas sean más fáciles de mantener es dudosa en el mejor de los casos. El problema que SRP crea es que, en lugar de tener una colección de clases bien nombradas, donde el nombre dice todo lo que la clase puede y no puede hacer (en otras palabras, fácil de entender, lo que lleva a un mantenimiento fácil) termina con cientos / miles de clases que las personas necesitaban para usar un diccionario de sinónimos con el fin de elegir un nombre único, muchos nombres son similares pero no del todo y clasificar toda esa paja es una tarea. IOW, no se puede mantener en absoluto, IMO.
Dunk
3
@Dunk hablas como si las clases fueran la única unidad de modularización disponible. He visto y escrito muchos proyectos después de SRP, y estoy de acuerdo en que su ejemplo sucede, pero solo si no hace un uso correcto de los paquetes y las bibliotecas. Si separa las responsabilidades en paquetes, dado que el contexto lo implica, puede nombrar las clases libremente nuevamente. Luego, para mantener un sistema como este, todo lo que se necesita es perforar el paquete correcto antes de buscar la clase que necesita cambiar. :)
MichelHenrich
55
Los principios de @Dunk OOD se pueden expresar como: "En principio, hacer esto es mejor debido a X, Y y Z". No significa que siempre deba aplicarse; las personas tienen que ser pragmáticas y sopesar las compensaciones. En los grandes proyectos, los beneficios son tales que generalmente superan la curva de aprendizaje de la que hablas, pero, por supuesto, esa no es la realidad para la mayoría de las personas, por lo que no debería aplicarse tanto. Sin embargo, incluso en las aplicaciones SRP más livianas, creo que ambos podemos estar de acuerdo en que separar la persistencia de las reglas comerciales siempre produce beneficios más allá de su costo (excepto tal vez para el código desechable).
MichelHenrich
10

Ambos enfoques violan el principio de responsabilidad única. Su primera versión otorga a la Studentclase muchas responsabilidades y la vincula a una tecnología de acceso a base de datos específica. El segundo conduce a una gran DBclase que será responsable no solo de los estudiantes, sino de cualquier otro tipo de objeto de datos en su programa. EDITAR: su tercer enfoque es el peor, ya que crea una dependencia cíclica entre la clase DB y la Studentclase.

Entonces, si no vas a escribir un programa de juguetes, no uses ninguno. En su lugar, use una clase diferente como StudentRepositorypara proporcionar una API para cargar y guardar, suponiendo que va a implementar el código CRUD por su cuenta. También puede considerar usar un marco ORM , que puede hacer el trabajo duro por usted (y el marco generalmente impondrá algunas decisiones en las que se deben colocar las operaciones de Cargar y Guardar).

Doc Brown
fuente
Gracias, modifiqué mi respuesta, ¿qué pasa con el tercer enfoque? de todos modos, creo que aquí se trata de hacer capas distintas
Ahmad
Algo también me sugiere usar StudentRepository en lugar de DB (¡no sé por qué! Quizás otra vez responsabilidad única) pero todavía no puedo entender por qué un repositorio diferente para cada clase. si es solo una capa de acceso a datos, entonces hay más funciones estáticas de utilidad y pueden estar juntas, ¿no? tal vez entonces se volvería una clase tan sucia y abarrotada (perdón por mi inglés)
Ahmad
1
@ Ahmad: hay un par de razones y un par de ventajas y desventajas para tener en cuenta. Esto podría llenar un libro completo. El razonamiento detrás de esto se reduce a la capacidad de prueba, mantenibilidad y capacidad de evolución a largo plazo de sus programas, especialmente cuando tienen un ciclo de vida duradero.
Doc Brown
¿Alguien ha trabajado en un proyecto donde hubo un cambio significativo en la tecnología DB? (¿Sin una reescritura masiva?) No lo he hecho. Si es así, ¿ayudó significativamente el siguiente SRP, como se sugiere aquí para evitar estar atado a ese DB?
user949300
@ user949300: Creo que no me expresé claramente. La clase de estudiantes no se vincula a una tecnología de base de datos específica, se vincula a una tecnología de acceso a base de datos específica, por lo que espera algo como una base de datos SQL para la persistencia. Mantener a la clase Student completamente inconsciente del mecanismo de persistencia permite una reutilización mucho más fácil de la clase en diferentes contextos. Por ejemplo, en un contexto de prueba o en un contexto donde los objetos se almacenan en un archivo. Y eso es algo que hice en el pasado muy a menudo. No es necesario cambiar toda la pila de bases de datos de su sistema para obtener beneficios de esto.
Doc Brown
3

Existen bastantes patrones que pueden usarse para la persistencia de datos. Existe el patrón de Unidad de Trabajo , hay un patrón de Repositorio , hay algunos patrones adicionales que pueden usarse como la Fachada Remota, y así sucesivamente.

La mayoría de ellos tienen sus admiradores y sus críticos. A menudo se trata de elegir lo que parece adaptarse mejor a la aplicación y apegarse a ella (con todas sus ventajas y desventajas, es decir, no usar ambos patrones al mismo tiempo ... a menos que esté realmente seguro de eso).

Como nota al margen: en su ejemplo, RetrieveStudent, AddStudent debe ser un método estático (porque no depende de la instancia).

Otra forma de tener métodos de guardar / cargar en clase es:

class Animal{
    public string Name {get; set;}
    public void Save(){...}
    public static Animal Load(string name){....}
    public static string[] GetAllNames(){...} // if you just want to list them
    public static Animal[] GetAllAnimals(){...} // if you actually need to retrieve them all
}

Personalmente, usaría este enfoque solo en aplicaciones bastante pequeñas, posiblemente herramientas para uso personal o donde pueda predecir de manera confiable que no tendré casos de uso más complicados que solo guardar o cargar objetos.

También personalmente, vea el patrón de la Unidad de Trabajo. Cuando lo conoces, es realmente bueno tanto en casos pequeños como grandes. Y es compatible con muchos frameworks / apis, por ejemplo, para nombrar EntityFramework o RavenDB.

Gerino
fuente
2
Acerca de su nota al margen: aunque, conceptualmente, solo hay una base de datos mientras se ejecuta la aplicación, no significa que deba hacer que los métodos RetriveStudent y AddStudent sean estáticos. Los repositorios generalmente se hacen con interfaces para permitir que los desarrolladores cambien las implementaciones sin afectar el resto de la aplicación. Esto incluso puede permitirle distribuir su aplicación con soporte para diferentes bases de datos, por ejemplo. Esta misma lógica, por supuesto, se aplica a muchas otras áreas además de la persistencia, como, por ejemplo, la IU.
MichelHenrich
Definitivamente tienes razón, he hecho muchas suposiciones basadas en la pregunta original.
Gerino
gracias, creo que el mejor enfoque es usar capas como se menciona en el patrón de repositorio
Ahmad
1

Si es una aplicación muy simple donde el objeto está más o menos vinculado al almacén de datos y viceversa (es decir, puede considerarse como una propiedad del almacén de datos), entonces tener un método .save () para esa clase podría tener sentido.

Pero creo que eso sería bastante excepcional.

Por el contrario, generalmente es mejor dejar que la clase administre sus datos y su funcionalidad (como un buen ciudadano OO), y externalizar el mecanismo de persistencia a otra clase o conjunto de clases.

La otra opción es usar un marco de persistencia que defina la persistencia de manera declarativa (como con las anotaciones), pero que todavía está externalizando la persistencia.

Robar
fuente
-1
  • Defina un método en esa clase que serialice el objeto y devuelva una secuencia de bytes que puede colocar en una base de datos o archivo o lo que sea
  • Tener un constructor para ese objeto que tome una secuencia de bytes como entrada

Sobre el RetrieveAllStudents()método, su sensación es correcta, de hecho es probable que esté fuera de lugar, porque podría tener múltiples listas distintas de estudiantes. ¿Por qué no simplemente mantener la (s) lista (s) fuera de la Studentclase?

philfr
fuente
Sabes que he visto esa tendencia en algunos framework PHP, por ejemplo, Laravel si lo sabes. El modelo está vinculado a la base de datos e incluso puede devolver una serie de objetos.
Ahmad