¿Cómo se debe diseñar una clase `Empleado`?

11

Estoy tratando de crear un programa para administrar empleados. Sin embargo, no puedo entender cómo diseñar la Employeeclase. Mi objetivo es poder crear y manipular datos de empleados en la base de datos utilizando un Employeeobjeto.

La implementación básica que pensé fue esta simple:

class Employee
{
    // Employee data (let's say, dozens of properties).

    Employee() {}
    Create() {}
    Update() {}
    Delete() {}
}

Usando esta implementación, me encontré con varios problemas.

  1. La IDbase de datos proporciona la información de un empleado, por lo que si uso el objeto para describir a un nuevo empleado, todavía no habrá IDque almacenar, mientras que un objeto que represente a un empleado existente tendrá un ID. Entonces, tengo una propiedad que a veces describe el objeto y otras no (¿Qué podría indicar que violamos SRP ? Ya que usamos la misma clase para representar a los empleados nuevos y existentes ...).
  2. Se Createsupone que el método crea un empleado en la base de datos, mientras que Updatey Deletese supone que actúa sobre un empleado existente (Nuevamente, SRP ...).
  3. ¿Qué parámetros debe tener el método 'Crear'? ¿Docenas de parámetros para todos los datos de los empleados o tal vez un Employeeobjeto?
  4. ¿Debería la clase ser inmutable?
  5. ¿Cómo va el Updatetrabajo? ¿Tomará las propiedades y actualizará la base de datos? ¿O tal vez tomará dos objetos, uno "antiguo" y uno "nuevo", y actualizará la base de datos con las diferencias entre ellos? (Creo que la respuesta tiene que ver con la respuesta sobre la mutabilidad de la clase).
  6. ¿Cuál sería la responsabilidad del constructor? ¿Cuáles serían los parámetros que toma? ¿Recuperaría los datos de los empleados de la base de datos utilizando un idparámetro y estos completarían las propiedades?

Entonces, como pueden ver, tengo un poco de confusión en mi cabeza y estoy muy confundido. ¿Podrías ayudarme a entender cómo debería ser esa clase?

Tenga en cuenta que no quiero opiniones, solo para comprender cómo generalmente se diseña una clase tan utilizada.

Sipo
fuente
3
Su violación actual del SRP es que tiene una clase que representa tanto una entidad como responsable de la lógica CRUD. Si lo separa, las operaciones CRUD y la estructura de la entidad serán clases diferentes, entonces 1. y 2. no rompen el SRP. 3. debe tomar un Employeeobjeto para proporcionar abstracción, las preguntas 4. y 5. generalmente no tienen respuesta, dependen de sus necesidades, y si separa la estructura y las operaciones CRUD en dos clases, entonces está bastante claro, el constructor de la Employeeno puede obtener datos de db nunca más, así que eso responde 6.
Andy
@DavidPacker - Gracias. ¿Podrías poner eso en una respuesta?
Sipo
55
No, repito, no haga que su ctor llegue a la base de datos. Al hacerlo, el código se acopla a la base de datos y hace que las cosas sean terriblemente difíciles de probar (incluso las pruebas manuales se vuelven más difíciles). Mira en el patrón del repositorio. Piénselo por un segundo, ¿es usted Updateun empleado o actualiza un registro de empleado? ¿Usted Employee.Delete()o hace un Boss.Fire(employee)?
RubberDuck
1
Además de lo que ya se ha mencionado, ¿tiene sentido que necesite un empleado para crear un empleado? En el registro activo, podría tener más sentido renovar un Empleado y luego llamar a Guardar en ese objeto. Sin embargo, incluso entonces, ahora tiene una clase que es responsable de la lógica empresarial, así como de su propia persistencia de datos.
Sr. Cochese

Respuestas:

10

Esta es una transcripción más bien formada de mi comentario inicial bajo su pregunta. Las respuestas a las preguntas abordadas por el OP se pueden encontrar al final de esta respuesta. También revise la nota importante ubicada en el mismo lugar.


Lo que estás describiendo actualmente, Sipo, es un patrón de diseño llamado registro activo . Como con todo, incluso este ha encontrado su lugar entre los programadores, pero se ha descartado en favor del repositorio y los patrones del mapeador de datos por una simple razón, la escalabilidad.

En resumen, un registro activo es un objeto que:

  • representa un objeto en su dominio (incluye reglas comerciales, sabe cómo manejar ciertas operaciones en el objeto, como si puede o no puede cambiar un nombre de usuario, etc.),
  • sabe cómo recuperar, actualizar, guardar y eliminar la entidad.

Abordas varios problemas con tu diseño actual y el problema principal de tu diseño se aborda en el último, sexto punto (último pero no menos importante, supongo). Cuando tiene una clase para la que está diseñando un constructor y ni siquiera sabe qué debe hacer el constructor, la clase probablemente esté haciendo algo mal. Eso sucedió en tu caso.

Pero arreglar el diseño es bastante simple al dividir la representación de la entidad y la lógica CRUD en dos (o más) clases.

Así es como se ve su diseño ahora:

  • Employee- contiene información sobre la estructura del empleado (sus atributos) y métodos sobre cómo modificar la entidad (si decide ir por el camino mutable), contiene lógica CRUD para la Employeeentidad, puede devolver una lista de Employeeobjetos, acepta un Employeeobjeto cuando lo desee actualizar a un empleado, puede devolver uno a Employeetravés de un método comogetSingleById(id : string) : Employee

Wow, la clase parece enorme.

Esta será la solución propuesta:

  • Employee - contiene información sobre la estructura del empleado (sus atributos) y métodos sobre cómo modificar la entidad (si decide seguir el camino mutable)
  • EmployeeRepository- contiene lógica CRUD para la Employeeentidad, puede devolver una lista de Employeeobjetos, acepta un Employeeobjeto cuando desea actualizar a un empleado, puede devolver uno a Employeetravés de un método comogetSingleById(id : string) : Employee

¿Has oído hablar de la separación de las preocupaciones ? No, lo harás ahora. Es la versión menos estricta del Principio de Responsabilidad Única, que dice que una clase debería tener una sola responsabilidad, o como dice el Tío Bob:

Un módulo debe tener una y solo una razón para cambiar.

Está bastante claro que si pude dividir claramente su clase inicial en dos que todavía tienen una interfaz bien redondeada, la clase inicial probablemente estaba haciendo demasiado, y así fue.

Lo bueno del patrón de repositorio es que no solo actúa como una abstracción para proporcionar una capa intermedia entre la base de datos (que puede ser cualquier cosa, archivo, noSQL, SQL, una orientada a objetos), sino que ni siquiera necesita ser una capa concreta. clase. En muchos lenguajes OO, puede definir la interfaz como real interface(o una clase con un método virtual puro si está en C ++) y luego tener múltiples implementaciones.

Esto eleva completamente la decisión de si un repositorio es una implementación real de usted, simplemente depende de la interfaz al confiar realmente en una estructura con la interfacepalabra clave. Y el repositorio es exactamente eso, es un término elegante para la abstracción de la capa de datos, es decir, el mapeo de datos a su dominio y viceversa.

Otra gran cosa de separarlo en (al menos) dos clases es que ahora la Employeeclase puede administrar claramente sus propios datos y hacerlo muy bien, porque no necesita ocuparse de otras cosas difíciles.

Pregunta 6: Entonces, ¿qué debe hacer el constructor en la Employeeclase recién creada ? Es simple. Debe tomar los argumentos, verificar si son válidos (como una edad probablemente no debería ser negativa o el nombre no debería estar vacío), generar un error cuando los datos no son válidos y si la validación pasada asigna los argumentos a variables privadas de la entidad. Ahora no puede comunicarse con la base de datos, porque simplemente no tiene idea de cómo hacerlo.


Pregunta 4: No se puede responder en absoluto, en general no, porque la respuesta depende en gran medida de lo que necesita exactamente.


Pregunta 5: Ahora que ha separado la clase hinchada en dos, puede tener varios métodos de actualización directamente en la Employeeclase, al igual changeUsername, markAsDeceasedque manipulará los datos de la Employeeclase solamente en la memoria RAM y entonces se podría introducir un método como el registerDirtyde la Patrón de unidad de trabajo a la clase de repositorio, a través del cual le haría saber al repositorio que este objeto ha cambiado de propiedades y que deberá actualizarse después de llamar al commitmétodo.

Obviamente, para una actualización, un objeto requiere tener una identificación y, por lo tanto, ya estar guardado, y es la responsabilidad del repositorio detectar esto y generar un error cuando no se cumplen los criterios.


Pregunta 3: Si decide seguir el patrón de la Unidad de trabajo, el createmétodo será ahora registerNew. Si no lo hace, probablemente lo llamaría en su savelugar. El objetivo de un repositorio es proporcionar una abstracción entre el dominio y la capa de datos, por lo que le recomendaría que este método (sea registerNewo no save) acepte el Employeeobjeto y depende de las clases que implementan la interfaz del repositorio, qué atributos deciden sacar de la entidad. Pasar un objeto completo es mejor, por lo que no necesita tener muchos parámetros opcionales.


Pregunta 2: Ambos métodos ahora serán parte de la interfaz del repositorio y no violarán el principio de responsabilidad única. La responsabilidad del repositorio es proporcionar operaciones CRUD para los Employeeobjetos, eso es lo que hace (además de Leer y Eliminar, CRUD se traduce tanto en Crear como en Actualizar). Obviamente, podría dividir el repositorio aún más al tener un EmployeeUpdateRepositoryetcétera, pero eso rara vez es necesario y una sola implementación generalmente puede contener todas las operaciones CRUD.


Pregunta 1: Terminaste con una Employeeclase simple que ahora (entre otros atributos) tendrá id. Si la identificación está llena o vacía (o null) depende de si el objeto ya se ha guardado. No obstante, una identificación sigue siendo un atributo que posee la entidad y la responsabilidad de la Employeeentidad es cuidar sus atributos, por lo tanto, cuidar su identificación.

Si una entidad tiene o no una identificación, generalmente no importa hasta que intente hacer algo de lógica de persistencia en ella. Como se menciona en la respuesta a la pregunta 5, es responsabilidad del repositorio detectar que no está tratando de salvar una entidad que ya se ha guardado o que está tratando de actualizar una entidad sin una identificación.


Nota IMPORTANTE

Tenga en cuenta que, aunque la separación de las preocupaciones es excelente, en realidad diseñar una capa de repositorio funcional es un trabajo bastante tedioso y, en mi experiencia, es un poco más difícil de corregir que el enfoque de registro activo. Pero terminará con un diseño que es mucho más flexible y escalable, lo que puede ser algo bueno.

Andy
fuente
Hmm, igual que mi respuesta, pero no como 'edgey' se pone sombras
Ewan
2
@Ewan No rechacé tu respuesta, pero puedo ver por qué algunos lo han hecho. No responde directamente a algunas de las preguntas del OP y algunas de sus sugerencias parecen infundadas.
Andy
1
Buena y comprensiva respuesta. Golpea el clavo en la cabeza con la separación de la preocupación. Y me gusta la advertencia que señala la elección importante entre un diseño complejo perfecto y un buen compromiso.
Christophe
Es cierto que su respuesta es superior
Ewan
Cuando cree por primera vez un nuevo objeto de empleado, no habrá valor para la ID. ¿El campo id puede salir con un valor nulo pero hará que el objeto empleado esté en estado no válido?
Susantha7
2

Primero cree una estructura de empleado que contenga las propiedades del empleado conceptual.

Luego cree una base de datos con una estructura de tabla coincidente, por ejemplo, mssql

Luego cree un repositorio de empleados para esa base de datos EmployeeRepoMsSql con las diversas operaciones CRUD que necesita.

Luego cree una interfaz IEmployeeRepo exponiendo las operaciones CRUD

Luego expanda su estructura de empleado a una clase con un parámetro de construcción de IEmployeeRepo. Agregue los diversos métodos de Guardar / Eliminar, etc. que necesite y use el EmployeeRepo inyectado para implementarlos.

Cuando se trata de Id, le sugiero que use un GUID que se puede generar a través del código en el constructor.

Para trabajar con objetos existentes, su código puede recuperarlos de la base de datos a través del repositorio antes de llamar a su Método de actualización.

Alternativamente, puede optar por el modelo de objeto de dominio anémico mal visto (pero en mi opinión superior) donde no agrega los métodos CRUD a su objeto, y simplemente pasa el objeto al repositorio para que se actualice / guarde / elimine

La inmutabilidad es una opción de diseño que dependerá de sus patrones y estilo de codificación. Si va todo funcional, intente ser inmutable también. Pero si no está seguro, un objeto mutable es probablemente más fácil de implementar.

En lugar de Crear (), iría con Guardar (). Crear funciona con el concepto de inmutabilidad, pero siempre me resulta útil poder construir un objeto que aún no está 'Guardado', por ejemplo, tiene una interfaz de usuario que le permite poblar un objeto u objetos de empleados y luego verificar nuevamente algunas reglas antes guardar en la base de datos.

***** código de ejemplo

public class Employee
{
    public string Id { get; set; }

    public string Name { get; set; }

    private IEmployeeRepo repo;

    //with the OOP approach you want the save method to be on the Employee Object
    //so you inject the IEmployeeRepo in the Employee constructor
    public Employee(IEmployeeRepo repo)
    {
        this.repo = repo;
        this.Id = Guid.NewGuid().ToString();
    }

    public bool Save()
    {
        return repo.Save(this);
    }
}

public interface IEmployeeRepo
{
    bool Save(Employee employee);

    Employee Get(string employeeId);
}

public class EmployeeRepoSql : IEmployeeRepo
{
    public Employee Get(string employeeId)
    {
        var sql = "Select * from Employee where Id=@Id";
        //more db code goes here
        Employee employee = new Employee(this);
        //populate object from datareader
        employee.Id = datareader["Id"].ToString();

    }

    public bool Save(Employee employee)
    {
        var sql = "Insert into Employee (....";
        //db logic
    }
}

public class MyADMProgram
{
    public void Main(string id)
    {
        //with ADM don't inject the repo into employee, just use it in your program
        IEmployeeRepo repo = new EmployeeRepoSql();
        var emp = repo.Get(id);

        //do business logic
        emp.Name = TextBoxNewName.Text;

        //save to DB
        repo.Save(emp);

    }
}
Ewan
fuente
1
El modelo de dominio anémico tiene muy poco que ver con la lógica CRUD. Es un modelo que, aunque pertenece a la capa de dominio, no tiene funcionalidad y toda la funcionalidad se sirve a través de servicios, a los cuales este modelo de dominio se pasa como parámetro.
Andy
Exactamente, en este caso, el repositorio es el servicio y las funciones son las operaciones CRUD.
Ewan
@DavidPacker, ¿estás diciendo que el modelo de dominio anémico es algo bueno?
candied_orange
1
@CandiedOrange No he expresado mi opinión en el comentario, pero no, si decide ir tan lejos como sumergir su aplicación en capas donde una capa es responsable solo de la lógica de negocios, estoy con el Sr. Fowler que es un modelo de dominio anémico es de hecho un antipatrón. ¿Por qué debería necesitar un UserUpdateservicio con un changeUsername(User user, string newUsername)método, cuando también puedo agregar el changeUsernamemétodo Userdirectamente a la clase ? Crear un servicio para eso no tiene sentido.
Andy
1
Creo que en este caso inyectar el repositorio solo para poner la lógica CRUD en el modelo no es óptimo.
Ewan
1

Revisión de tu diseño

Tu Employeees en realidad una especie de proxy para un objeto gestionado de forma persistente en la base de datos.

Por lo tanto, sugiero pensar en la ID como si fuera una referencia a su objeto de base de datos. Con esta lógica en mente, puede continuar su diseño como lo haría para los objetos que no son de la base de datos, la identificación le permite implementar la lógica de composición tradicional:

  • Si se establece la ID, tiene un objeto de base de datos correspondiente.
  • Si la ID no está configurada, no hay ningún objeto de base de datos correspondiente: es Employeeposible que aún no se haya creado o que simplemente se haya eliminado.
  • Necesita algún mecanismo para iniciar la relación para los empleados existentes y los registros de bases de datos existentes que aún no están cargados en la memoria.

También necesitaría administrar un estado para el objeto. Por ejemplo:

  • cuando un empleado aún no está vinculado con un objeto de base de datos, ya sea mediante creación o recuperación de datos, no debería poder realizar actualizaciones o eliminaciones
  • ¿Los datos del empleado en el objeto están sincronizados con la base de datos o se realizan cambios?

Con esto en mente, podríamos optar por:

class Employee
{
    ...
    Employee () {}       // Initialize an empty Employee
    Load(IDType ID) {}   // Load employee with known ID from the database
    bool Create() {}     // Create an new employee an set its ID 
    bool Update() {}     // Update the employee (can ID be changed?)
    bool Delete() {}     // Delete the employee (and reset ID because there's no corresponding ID. 
    bool isClean () {}   // true if ID empty or if all properties match database
}

Para poder administrar el estado de su objeto de manera confiable, debe garantizar una mejor encapsulación al hacer que las propiedades sean privadas y dar acceso solo a través de getters y setters que actualizan el estado.

Tus preguntas

  1. Creo que la propiedad ID no viola el SRP. Su única responsabilidad es referirse a un objeto de base de datos.

  2. Su empleado en su conjunto no cumple con el SRP, porque es responsable del enlace con la base de datos, sino también de mantener los cambios temporales y de todas las transacciones que suceden con ese objeto.

    Otro diseño podría ser mantener los campos modificables en otro objeto que se cargaría solo cuando sea necesario acceder a los campos.

    Puede implementar las transacciones de la base de datos en el Empleado utilizando el patrón de comando . Este tipo de diseño también facilitaría el desacoplamiento entre sus objetos comerciales (Empleado) y su sistema de base de datos subyacente, al aislar modismos y API específicos de la base de datos.

  3. No agregaría una docena de parámetros Create(), porque los objetos comerciales podrían evolucionar y hacer que todo esto sea muy difícil de mantener. Y el código se volvería ilegible. Aquí tiene 2 opciones: pasar un conjunto minimalista de parámetros (no más de 4) que son absolutamente necesarios para crear un empleado en la base de datos y realizar los cambios restantes a través de la actualización, o pasar un objeto. Por cierto, en su diseño entiendo que ya ha elegido: my_employee.Create().

  4. ¿Debería la clase ser inmutable? Vea la discusión anterior: en su diseño original no. Optaría por una identificación inmutable pero no por un empleado inmutable. Un empleado evoluciona en la vida real (nuevo puesto de trabajo, nueva dirección, nueva situación matrimonial, incluso nuevos nombres ...). Creo que será más fácil y más natural trabajar con esta realidad en mente, al menos en la capa de lógica de negocios.

  5. Si considera usar un comando para actualizar y un objeto distinto para (¿GUI?) Para guardar los cambios deseados, puede optar por un enfoque antiguo / nuevo. En todos los demás casos, optaría por actualizar un objeto mutable. Atención: la actualización podría activar el código de la base de datos, por lo que debe asegurarse después de una actualización, el objeto todavía está realmente sincronizado con la base de datos.

  6. Creo que buscar un empleado de DB en el constructor no es una buena idea, porque buscar puede salir mal y, en muchos idiomas, es difícil hacer frente a la construcción fallida. El constructor debe inicializar el objeto (especialmente el ID) y su estado.

Christophe
fuente