dimanche 5 septembre 2021

Fixing cyclic dependencies by adding a new interface

Below is some code from a book which shows how cyclic dependencies:

public interface IAuditTrailAppender {
   void Append(Entity changedEntity);
}

public class SqlAuditTrailAppender : IAuditTrailAppender {
   private readonly IUserContext userContext;
   private readonly CommerceContext context;
   private readonly ITimeProvider timeProvider;

   public SqlAuditTrailAppender(IUserContext userContext, CommerceContext context, ITimeProvider timeProvider) {
      this.userContext = userContext;
      this.context = context;
      this.timeProvider = timeProvider;
   }

   public void Append(Entity changedEntity) {
      AuditEntry entry = new AuditEntry {
         UserId = this.userContext.CurrentUser.Id,
         TimeOfChange = this.timeProvider.Now, 
         EntityId = entity.Id,
         EntityType = entity.GetType().Name
      };

      this.context.AuditEntries.Add(entry);
   }
}

public class AspNetUserContextAdapter : IUserContext {
   private static HttpContextAccessor Accessor = new HttpContextAccessor();

   private readonly IUserRepository repository; 

   public AspNetUserContextAdapter(IUserRepository repository) {
      this.repository = repository;
   }

   public User CurrentUser {
      get {
         var user = Accessor.HttpContext.User;
         string userName = user.Identity.Name;
         return this.repository.GetByName(userName);
      }
   }
}

public class SqlUserRepository : IUserRepository {
   public SqlUserRepository(CommerceContext context, IAuditTrailAppender appender) {
      this.appender = appender;
      this.context = context;
   }

   public void Update(User user) {
      this.appender.Append(user);
   }

   public User GetById(Guid id) { ... } 

   public User GetByName(string name) { ... }   // <--- used by CurrentUser property of AspNetUserContextAdapter
}

You can see cyclic dependencies exist as the picture below shows:

enter image description here

The author says "these kind of dependency cycles are typically caused by single-responsibility principle (SRP) violation. To fix it, the author adds a new interface IUserByNameRetriever:

public interface IUserByNameRetriever {
   User GetByName(string name);
}

public class SqlUserByNameRetriever : IUserByNameRetriever {
   public SqlUserByNameRetriever(CommerceContext context) {
      this.context = context;
   }

   public User GetByName(string name) { ... }
}

public class SqlUserRepository : IUserRepository {
   public SqlUserRepository(CommerceContext context, IAuditTrailAppender appender) {
      this.appender = appender;
      this.context = context;
   }

   public void Update(User user) {
      this.appender.Append(user);
   }

   public User GetById(Guid id) { ... } 

   // public User GetByName(string name) { ... }  don't need this method anymore
}

public class AspNetUserContextAdapter : IUserContext {
   private static HttpContextAccessor Accessor = new HttpContextAccessor();

   private readonly IUserByNameRetriever retriever; 

   public AspNetUserContextAdapter(IUserByNameRetriever retriever) {
      this.retriever = retriever;
   }

   public User CurrentUser {
      get {
         var user = Accessor.HttpContext.User;
         string userName = user.Identity.Name;
         return this.retriever.GetByName(userName);
      }
   }
}

enter image description here

I can understand how the introduce of IAuditTrailAppender stops the dependency cycles, but I feel like it is just a workaround. I don't understand why the author said this dependency cycle is caused by SRP violation. Because if you look at SqlUserRepository, its GetByName method is a nature to have it in the class just like GetById method (consumers can search a user by its id, and of course, it is nature for consumers to search a user by name), I can't see why having GetById method in SqlUserRepository is a SRP violation?

Aucun commentaire:

Enregistrer un commentaire