Estoy tratando de crear un programa para administrar empleados. Sin embargo, no puedo entender cómo diseñar la Employee
clase. Mi objetivo es poder crear y manipular datos de empleados en la base de datos utilizando un Employee
objeto.
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.
- La
ID
base 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áID
que almacenar, mientras que un objeto que represente a un empleado existente tendrá unID
. 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 ...). - Se
Create
supone que el método crea un empleado en la base de datos, mientras queUpdate
yDelete
se supone que actúa sobre un empleado existente (Nuevamente, SRP ...). - ¿Qué parámetros debe tener el método 'Crear'? ¿Docenas de parámetros para todos los datos de los empleados o tal vez un
Employee
objeto? - ¿Debería la clase ser inmutable?
- ¿Cómo va el
Update
trabajo? ¿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). - ¿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
id
pará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.
Employee
objeto 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 laEmployee
no puede obtener datos de db nunca más, así que eso responde 6.Update
un empleado o actualiza un registro de empleado? ¿UstedEmployee.Delete()
o hace unBoss.Fire(employee)
?Respuestas:
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:
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 laEmployee
entidad, puede devolver una lista deEmployee
objetos, acepta unEmployee
objeto cuando lo desee actualizar a un empleado, puede devolver uno aEmployee
travé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 laEmployee
entidad, puede devolver una lista deEmployee
objetos, acepta unEmployee
objeto cuando desea actualizar a un empleado, puede devolver uno aEmployee
travé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:
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
interface
palabra 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
Employee
clase 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
Employee
clase 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
Employee
clase, al igualchangeUsername
,markAsDeceased
que manipulará los datos de laEmployee
clase solamente en la memoria RAM y entonces se podría introducir un método como elregisterDirty
de 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 alcommit
mé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
create
método será ahoraregisterNew
. Si no lo hace, probablemente lo llamaría en susave
lugar. 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 (searegisterNew
o nosave
) acepte elEmployee
objeto 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
Employee
objetos, 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 unEmployeeUpdateRepository
etcétera, pero eso rara vez es necesario y una sola implementación generalmente puede contener todas las operaciones CRUD.Pregunta 1: Terminaste con una
Employee
clase simple que ahora (entre otros atributos) tendrá id. Si la identificación está llena o vacía (onull
) 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 laEmployee
entidad 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.
fuente
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
fuente
UserUpdate
servicio con unchangeUsername(User user, string newUsername)
método, cuando también puedo agregar elchangeUsername
métodoUser
directamente a la clase ? Crear un servicio para eso no tiene sentido.Revisión de tu diseño
Tu
Employee
es 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:
Employee
posible que aún no se haya creado o que simplemente se haya eliminado.También necesitaría administrar un estado para el objeto. Por ejemplo:
Con esto en mente, podríamos optar por:
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
Creo que la propiedad ID no viola el SRP. Su única responsabilidad es referirse a un objeto de base de datos.
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.
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()
.¿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.
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.
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.
fuente