El objetivo de mi tarea es diseñar un sistema pequeño que pueda ejecutar tareas recurrentes programadas. Una tarea recurrente es algo así como "enviar un correo electrónico al administrador cada hora de 8:00 a. M. A 5:00 p. M., De lunes a viernes".
Tengo una clase base llamada RecurringTask .
public abstract class RecurringTask{
// I've already figured out this part
public bool isOccuring(DateTime dateTime){
// implementation
}
// run the task
public abstract void Run(){
}
}
Y tengo varias clases que se heredan de RecurringTask . Uno de ellos se llama SendEmailTask .
public class SendEmailTask : RecurringTask{
private Email email;
public SendEmailTask(Email email){
this.email = email;
}
public override void Run(){
// need to send out email
}
}
Y tengo un servicio de correo electrónico que me puede ayudar a enviar un correo electrónico.
La última clase es RecurringTaskScheduler , es responsable de cargar tareas desde la caché o la base de datos y ejecutar la tarea.
public class RecurringTaskScheduler{
public void RunTasks(){
// Every minute, load all tasks from cache or database
foreach(RecuringTask task : tasks){
if(task.isOccuring(Datetime.UtcNow)){
task.run();
}
}
}
}
Aquí está mi problema: ¿dónde debo poner EmailService ?
Opción 1 : inyectar EmailService en SendEmailTask
public class SendEmailTask : RecurringTask{
private Email email;
public EmailService EmailService{ get; set;}
public SendEmailTask (Email email, EmailService emailService){
this.email = email;
this.EmailService = emailService;
}
public override void Run(){
this.EmailService.send(this.email);
}
}
Ya hay algunas discusiones sobre si debemos inyectar un servicio en una entidad, y la mayoría de la gente está de acuerdo en que no es una buena práctica. Ver este artículo .
Opción 2: si ... lo demás en RecurringTaskScheduler
public class RecurringTaskScheduler{
public EmailService EmailService{get;set;}
public class RecurringTaskScheduler(EmailService emailService){
this.EmailService = emailService;
}
public void RunTasks(){
// load all tasks from cache or database
foreach(RecuringTask task : tasks){
if(task.isOccuring(Datetime.UtcNow)){
if(task is SendEmailTask){
EmailService.send(task.email); // also need to make email public in SendEmailTask
}
}
}
}
}
Me han dicho si ... De lo contrario, el elenco como el anterior no es OO, y traerá más problemas.
Opción 3: cambie la firma de Ejecutar y cree ServiceBundle .
public class ServiceBundle{
public EmailService EmailService{get;set}
public CleanDiskService CleanDiskService{get;set;}
// and other services for other recurring tasks
}
Inyecte esta clase en RecurringTaskScheduler
public class RecurringTaskScheduler{
public ServiceBundle ServiceBundle{get;set;}
public class RecurringTaskScheduler(ServiceBundle serviceBundle){
this.ServiceBundle = ServiceBundle;
}
public void RunTasks(){
// load all tasks from cache or database
foreach(RecuringTask task : tasks){
if(task.isOccuring(Datetime.UtcNow)){
task.run(serviceBundle);
}
}
}
}
El método Run de SendEmailTask sería
public void Run(ServiceBundle serviceBundle){
serviceBundle.EmailService.send(this.email);
}
No veo grandes problemas con este enfoque.
Opción 4 : Patrón de visitante.
La idea básica es crear un visitante que encapsule servicios al igual que ServiceBundle .
public class RunTaskVisitor : RecurringTaskVisitor{
public EmailService EmailService{get;set;}
public CleanDiskService CleanDiskService{get;set;}
public void Visit(SendEmailTask task){
EmailService.send(task.email);
}
public void Visit(ClearDiskTask task){
//
}
}
Y también necesitamos cambiar la firma del método Run . El método Run de SendEmailTask es
public void Run(RecurringTaskVisitor visitor){
visitor.visit(this);
}
Es una implementación típica del Patrón de visitante, y el visitante será inyectado en RecurringTaskScheduler .
En resumen: entre estos cuatro enfoques, ¿cuál es el mejor para mi escenario? ¿Y hay alguna gran diferencia entre la Opción 3 y la Opción 4 para este problema?
¿O tienes una mejor idea sobre este problema? ¡Gracias!
Actualización 22/05/2015 : Creo que la respuesta de Andy resume muy bien mi intención; Si todavía está confundido sobre el problema en sí, sugiero leer su publicación primero.
Me acabo de enterar que mi problema es muy similar al problema de Despacho de mensajes , que lleva a la Opción 5.
Opción 5 : Convertir mi problema a Despacho de mensajes .
Hay un mapeo uno a uno entre mi problema y el problema de envío de mensajes :
Despachador de mensajes : reciba IMessage y envíe subclases de IMessage a sus controladores correspondientes. → RecurringTaskScheduler
IMessage : una interfaz o una clase abstracta. → Tarea recurrente
Mensaje A : se extiende desde IMessage y tiene información adicional. → SendEmailTask
MensajeB : Otra subclase de IMessage . → CleanDiskTask
MessageAHandler : cuando reciba MessageA , trátelo → SendEmailTaskHandler, que contiene EmailService, y enviará un correo electrónico cuando reciba SendEmailTask
MessageBHandler : igual que MessageAHandler , pero maneja MessageB en su lugar. → CleanDiskTaskHandler
La parte más difícil es cómo enviar diferentes tipos de IMessage a diferentes manejadores. Aquí hay un enlace útil .
Realmente me gusta este enfoque, no contamina mi entidad con el servicio, y no tiene ninguna clase de Dios .
SendEmailTask
me parece más un servicio que una entidad. Iría por la opción 1 sin dudarlo.accept
s visitantes. La motivación para Visitor es que tiene muchos tipos de clases en algunos agregados que necesitan visitas, y no es conveniente modificar su código para cada nueva funcionalidad (operación). Todavía no veo cuáles son esos objetos agregados, y creo que Visitor no es apropiado. Si es el caso, debe editar su pregunta (que se refiere al visitante).Respuestas:
Yo diría que la opción 1 es la mejor ruta a seguir. La razón por la que no debe descartarlo es que no
SendEmailTask
es una entidad. Una entidad es un objeto relacionado con la retención de datos y estado. Tu clase tiene muy poco de eso. De hecho, no es una entidad, pero contiene una entidad: el objeto que está almacenando. Eso significa que no debe tomar un servicio, o tener un método. En cambio, debe tener servicios que tomen entidades, como su . Entonces ya está siguiendo la idea de mantener los servicios fuera de las entidades.Email
Email
#Send
EmailService
Como
SendEmailTask
no es una entidad, por lo tanto, está perfectamente bien inyectar el correo electrónico y el servicio, y eso debe hacerse a través del constructor. Al hacer la inyección del constructor, podemos estar seguros de queSendEmailTask
siempre está listo para realizar su trabajo.Ahora veamos por qué no hacer las otras opciones (específicamente con respecto a SOLID ).
opcion 2
Te han dicho con razón que ramificarte en ese tipo traerá más dolores de cabeza en el futuro. Veamos por qué. Primero,
if
s tienden a agruparse y crecer. Hoy es una tarea enviar correos electrónicos, mañana, cada tipo diferente de clase necesita un servicio diferente u otro comportamiento. Gestionar esaif
declaración se convierte en una pesadilla. Dado que estamos ramificando en tipo (y en este caso tipo explícito ), estamos subvirtiendo el sistema de tipos integrado en nuestro idioma.La opción 2 no es la responsabilidad única (SRP) porque lo que antes era reutilizable
RecurringTaskScheduler
ahora tiene que saber sobre todos estos diferentes tipos de tareas y sobre los diferentes tipos de servicios y comportamientos que podrían necesitar. Esa clase es mucho más difícil de reutilizar. Tampoco es Abierto / Cerrado (OCP). Debido a que necesita saber sobre este tipo de tarea o aquella (o este tipo de servicio o ese), los cambios dispares en las tareas o servicios podrían forzar cambios aquí. Agregar una nueva tarea? Agregar un nuevo servicio? ¿Cambiar la forma en que se maneja el correo electrónico? CambioRecurringTaskScheduler
. Debido a que el tipo de tarea es importante, no se adhiere a la sustitución de Liskov (LSP). No puede simplemente obtener una tarea y hacerse. Tiene que pedir el tipo y, según el tipo, haga esto o aquello. En lugar de encapsular las diferencias en las tareas, estamos metiendo todo eso en elRecurringTaskScheduler
.Opción 3
La opción 3 tiene algunos grandes problemas. Incluso en el artículo al que se vincula , el autor desalienta hacer esto:
Estás creando un localizador de servicios con tu
ServiceBundle
clase. En este caso, no parece ser estático, pero aún tiene muchos de los problemas inherentes a un localizador de servicios. Sus dependencias ahora están ocultas debajo de estoServiceBundle
. Si te doy la siguiente API de mi nueva y genial tarea:¿Cuáles son los servicios que estoy usando? ¿Qué servicios se deben burlar en una prueba? ¿Qué es lo que me impide usar todos los servicios del sistema, solo porque?
Si quiero usar su sistema de tareas para ejecutar algunas tareas, ahora dependo de todos los servicios de su sistema, incluso si solo uso algunos o ninguno.
El
ServiceBundle
realmente no es SRP porque necesita saber acerca de cada servicio en su sistema. Tampoco es OCP. Agregar nuevos servicios significa cambios en elServiceBundle
, y cambios en elServiceBundle
pueden significar cambios dispares en las tareas en otros lugares.ServiceBundle
no segrega su interfaz (ISP). Tiene una interfaz en expansión de todos estos servicios, y debido a que es solo un proveedor de esos servicios, podríamos considerar que su interfaz abarca las interfaces de todos los servicios que proporciona también. Las tareas ya no se adhieren a la Inversión de dependencia (DIP), porque sus dependencias están ocultas detrás deServiceBundle
. Esto tampoco se adhiere al Principio de Menos Conocimiento (también conocido como la Ley de Deméter) porque las cosas saben muchas más cosas de las que tienen que saber.Opcion 4
Anteriormente, tenía muchos objetos pequeños que podían funcionar de forma independiente. La opción 4 toma todos estos objetos y los junta en un solo
Visitor
objeto. Este objeto actúa como un objeto de dios sobre todas sus tareas. Reduce susRecurringTask
objetos a sombras anémicas que simplemente llaman a un visitante. Todo el comportamiento se mueve alVisitor
. ¿Necesitas cambiar el comportamiento? ¿Necesita agregar una nueva tarea? CambioVisitor
.La parte más desafiante es que, debido a que todos los diferentes comportamientos están en una sola clase, cambiar algunos polimórficos arrastra a todos los demás comportamientos. Por ejemplo, queremos tener dos formas diferentes de enviar correos electrónicos (¿tal vez deberían usar servidores diferentes?). ¿Como podríamos hacerlo? Podríamos crear una
IVisitor
interfaz e implementar ese código potencialmente duplicado, como el#Visit(ClearDiskTask)
de nuestro visitante original. Luego, si encontramos una nueva forma de borrar un disco, tenemos que implementar y duplicar nuevamente. Entonces queremos ambos tipos de cambios. Implementar y duplicar nuevamente. Estos dos comportamientos diferentes y dispares están inextricablemente vinculados.¿Tal vez podríamos subclasificar
Visitor
? Subclase con nuevo comportamiento de correo electrónico, subclase con nuevo comportamiento de disco. ¡Sin duplicación hasta ahora! ¿Subclase con ambos? Ahora uno u otro necesita ser duplicado (o ambos si así lo prefiere).Comparemos con la opción 1: necesitamos un nuevo comportamiento de correo electrónico. Podemos crear un nuevo
RecurringTask
que haga el nuevo comportamiento, inyectar en sus dependencias y agregarlo a la colección de tareas en elRecurringTaskScheduler
. Ni siquiera necesitamos hablar sobre la limpieza de discos, porque esa responsabilidad está en otro lugar por completo. También tenemos toda la gama de herramientas OO a nuestra disposición. Podríamos decorar esa tarea con el registro, por ejemplo.La opción 1 le causará menos dolor y es la forma más correcta de manejar esta situación.
fuente
SendEmailTask
base de datos, entonces esa configuración debería ser una clase de configuración separada que también debería inyectarse en suSendEmailTask
. Si está generando datos desde suSendEmailTask
, debe crear un objeto de recuerdo para almacenar el estado y ponerlo en su base de datos.EMailTaskDefinitions
yEmailService
enSendEmailTask
? Luego, en elRecurringTaskScheduler
, necesito inyectar algo comoSendEmailTaskRepository
cuya responsabilidad es cargar la definición y el servicio e inyectarlosSendEmailTask
. Pero ahora diría que esRecurringTaskScheduler
necesario conocer el Repositorio de cada tarea, comoCleanDiskTaskRepository
. Y necesito cambiarRecurringTaskScheduler
cada vez que tengo una nueva tarea (para agregar el repositorio en el Programador).RecurringTaskScheduler
solo debe tener en cuenta el concepto de un repositorio de tareas generalizado y aRecurringTask
. Al hacer esto, puede depender de abstracciones. Los repositorios de tareas se pueden inyectar en el constructor deRecurringTaskScheduler
. Entonces, los diferentes repositorios solo necesitan saber dóndeRecurringTaskScheduler
se instancia (o podrían ocultarse en una fábrica y llamarse desde allí). Debido a que solo depende de las abstracciones,RecurringTaskScheduler
no necesita cambiar con cada nueva tarea. Esa es la esencia de la inversión de dependencia.¿Ha echado un vistazo a las bibliotecas existentes, por ejemplo, cuarzo de primavera o lote de primavera (no estoy seguro de qué se adapta mejor a sus necesidades)?
A su pregunta:
Supongo que el problema es que desea conservar algunos metadatos de la tarea de forma polimórfica, por lo que una tarea de correo electrónico tiene direcciones de correo electrónico asignadas, una tarea de registro a nivel de registro, etc. Puede almacenar una lista de aquellos en la memoria o en su base de datos, pero para separar las preocupaciones, no desea que la entidad esté contaminada con el código de servicio.
Mi solución propuesta:
Separaría la parte de ejecución y la parte de datos de la tarea, para tener, por ejemplo,
TaskDefinition
y aTaskRunner
. TaskDefinition tiene una referencia a un TaskRunner o una fábrica que crea uno (por ejemplo, si se requiere alguna configuración como smtp-host). La fábrica es específica: solo puede manejarEMailTaskDefinition
sy devuelve solo instancias deEMailTaskRunner
s. De esta forma, es más OO y seguro: si introduce un nuevo tipo de tarea, debe introducir una nueva fábrica específica (o reutilizar una), si no lo hace, no puede compilar.De esta manera, terminaría con una dependencia: capa de entidad -> capa de servicio y viceversa, porque el Runner necesita información almacenada en la entidad y probablemente quiera actualizar su estado en la base de datos.
Puede romper el círculo utilizando una fábrica genérica, que toma una definición de tarea y devuelve un TaskRunner específico , pero eso requeriría muchos ifs. Usted podría utilizar la reflexión para encontrar un corredor que se nombra de manera similar como su definición, pero tenga cuidado este enfoque puede costar un poco de rendimiento, y puede dar lugar a errores de ejecución.
PD: estoy asumiendo Java aquí. Creo que es similar en .net. El principal problema aquí es el doble enlace.
Al patrón visitante
Creo que estaba destinado a ser utilizado para intercambiar un algoritmo para diferentes tipos de objetos de datos en tiempo de ejecución, que para fines de doble enlace puro. Por ejemplo, si tiene diferentes tipos de seguros y diferentes tipos de cálculo, por ejemplo, porque diferentes países lo requieren. Luego, elige un método de cálculo específico y lo aplica a varios seguros.
En su caso, elegiría una estrategia de tarea específica (por ejemplo, correo electrónico) y la aplicaría a todas sus tareas, lo cual está mal porque no todas son tareas de correo electrónico.
PD: No lo probé, pero creo que su Opción 4 tampoco funcionará, porque es de doble enlace nuevamente.
fuente
Estoy completamente en desacuerdo con ese artículo. Los servicios (concretamente su "API") son parte importante del dominio comercial y, como tal, existirán dentro del modelo de dominio. Y no hay ningún problema con las entidades en el dominio empresarial que hacen referencia a otra cosa en el mismo dominio empresarial.
Es una regla de negocios. Y para hacer eso, se necesita un servicio que envíe correo. Y la entidad que maneja
When X
debe saber sobre este servicio.Pero hay algunos problemas con la implementación. Debe ser transparente para el usuario de la entidad, que la entidad está utilizando un servicio. Por lo tanto, agregar el servicio en constructor no es algo bueno. Esto también es un problema cuando deserializa la entidad de la base de datos, porque necesita establecer tanto los datos de la entidad como las instancias de los servicios. La mejor solución que se me ocurre es utilizar la inyección de propiedades después de crear la entidad. Tal vez forzando a cada instancia recién creada de cualquier entidad a pasar por el método "inicializar" que inyecta todas las entidades que la entidad necesita.
fuente
Esa es una gran pregunta y un problema interesante. Le propongo que use una combinación de cadena de responsabilidad y patrones de doble distribución (ejemplos de patrones aquí ).
Primero definamos la jerarquía de tareas. Tenga en cuenta que ahora hay varios
run
métodos para implementar Double Dispatch.A continuación, definamos la
Service
jerarquía. UsaremosService
s para formar la Cadena de Responsabilidad.La pieza final es la
RecurringTaskScheduler
que organiza el proceso de carga y ejecución.Ahora, aquí está la aplicación de ejemplo que demuestra el sistema.
Ejecutando los resultados de la aplicación:
EmailService ejecutando SendEmailTask con contenido 'aquí viene el primer correo electrónico'
EmailService ejecutando SendEmailTask con contenido 'aquí está el segundo correo electrónico'
ExecuteService ejecutando ExecuteTask con contenido '/ root / python'
ExecuteService ejecutando ExecuteTask con contenido '/ bin / cat'
EmailService ejecutando SendEmailTask con contenido 'aquí está el tercer correo electrónico'
ExecuteService que ejecuta ExecuteTask con contenido '/ bin / grep'
fuente