No te preocupes por el principio de responsabilidad única. No va a ayudarlo a tomar una buena decisión aquí porque puede elegir subjetivamente un concepto particular como "responsabilidad". Podría decir que la responsabilidad de la clase es administrar la persistencia de datos en la base de datos, o podría decir que su responsabilidad es realizar todo el trabajo relacionado con la creación de un usuario. Estos son solo niveles diferentes del comportamiento de la aplicación, y ambos son expresiones conceptuales válidas de una "responsabilidad única". Por lo tanto, este principio no es útil para resolver su problema.
El principio más útil para aplicar en este caso es el principio de menor sorpresa . Entonces, hagamos la pregunta: ¿es sorprendente que un repositorio con la función principal de datos persistentes en una base de datos también envíe correos electrónicos?
Sí, es muy sorprendente. Estos son dos sistemas externos completamente separados, y el nombre SaveChanges
no implica también enviar notificaciones. El hecho de que delegue esto en un evento hace que el comportamiento sea aún más sorprendente, ya que alguien que lee el código ya no puede ver fácilmente qué comportamientos adicionales se invocan. La indirección perjudica la legibilidad. A veces, los beneficios valen los costos de legibilidad, pero no cuando se invoca automáticamente un sistema externo adicional que tiene efectos observables para los usuarios finales. (El registro puede excluirse aquí ya que su efecto es esencialmente el mantenimiento de registros con fines de depuración. Los usuarios finales no consumen el registro, por lo que no hay ningún daño en el registro siempre.) Aún peor, esto reduce la flexibilidad en el tiempo de enviar el correo electrónico, lo que hace imposible intercalar otras operaciones entre guardar y la notificación.
Si su código generalmente necesita enviar una notificación cuando un usuario se crea con éxito, puede crear un método que lo haga:
public void AddUserAndNotify(IUserRepository repo, IEmailNotification notifier, MyUser user)
{
repo.Add(user);
repo.SaveChanges();
notifier.SendUserCreatedNotification(user);
}
Pero si esto agrega valor depende de los detalles de su aplicación.
De hecho, desalentaría la existencia del SaveChanges
método. Este método presumiblemente confirmará una transacción de la base de datos, pero otros repositorios podrían haber modificado la base de datos en la misma transacción . El hecho de que los compromete a todos nuevamente es sorprendente, ya que SaveChanges
está específicamente relacionado con esta instancia del repositorio de usuarios.
El patrón más directo para administrar una transacción de base de datos es un using
bloque externo :
using (DataContext context = new DataContext())
{
_userRepository.Add(context, user);
context.SaveChanges();
notifier.SendUserCreatedNotification(user);
}
Esto le da al programador un control explícito sobre cuándo se guardan los cambios para todos los repositorios, obliga al código a documentar explícitamente la secuencia de eventos que deben ocurrir antes de una confirmación, asegura que se emita una reversión por error (suponiendo que se DataContext.Dispose
emita una reversión) y evita que se oculte conexiones entre clases con estado.
También preferiría no enviar el correo electrónico directamente en la solicitud. Sería más robusto registrar la necesidad de una notificación en una cola. Esto permitiría un mejor manejo de fallas. En particular, si se produce un error al enviar el correo electrónico, se puede volver a intentar más tarde sin interrumpir el guardado del usuario, y se evita el caso en el que se crea el usuario pero el sitio devuelve un error.
using (DataContext context = new DataContext())
{
_userRepository.Add(context, user);
_emailNotificationQueue.AddUserCreateNotification(user);
_emailNotificationQueue.Commit();
context.SaveChanges();
}
Es mejor confirmar primero la cola de notificaciones, ya que el consumidor de la cola puede verificar que el usuario existe antes de enviar el correo electrónico, en caso de que context.SaveChanges()
falle la llamada. (De lo contrario, necesitará una estrategia de compromiso de dos fases completa para evitar errores de seguridad).
El resultado final es ser práctico. Realmente piense en las consecuencias (tanto en términos de riesgo como de beneficio) de escribir código de una manera particular. Me parece que el "principio de responsabilidad única" no me ayuda mucho a hacerlo, mientras que el "principio de menor sorpresa" a menudo me ayuda a meterme en la cabeza de otro desarrollador (por así decirlo) y pensar en lo que podría suceder.