【问题标题】:Dependency Injection Architectural Design - Service classes circular references依赖注入架构设计 - 服务类循环引用
【发布时间】:2012-12-26 15:33:44
【问题描述】:

我有以下服务类别:

public class JobService {
  private UserService us;

  public JobService (UserService us) {
    this.us = us;
  }

  public void addJob(Job job) {
    // needs to make a call to user service to update some user info
    // similar dependency to the deleteUser method
  }
}

public class UserService {
  private JobService js;
  public UserService(JobService js) {
    this.js = js;
  }

  public void deleteUser(User u) {
    using (TransactionScope scope = new TransactionScope()) {
      List<IJob> jobs = jobService.findAllByUser(u.Id);
      foreach (IJob job in jobs) {
        js.deleteJob(job);
      }
      userDao.delete(user);
      scope.Complete();
    }
  }
}        

这些服务类中的每一个都由 IoC 容器实例化,并且没有功能问题,但在我看来,这种方法存在潜在的设计缺陷,我想知道是否有另一种方法可以使更有意义。

【问题讨论】:

  • 这是我喜欢基于属性的 DI 而不是基于构造函数的 DI 的原因之一。

标签: c# dependency-injection inversion-of-control circular-dependency


【解决方案1】:

正如有人已经指出的那样,问题不在于 DI 容器的限制,而在于您的设计。

我明白你有一个单独的UserService 和一个JobService 的原因,它们包含对彼此的引用。这是因为UserServiceJobService 都包含一些需要其他服务作为参考的逻辑(添加作业需要添加用户等)。但是,我认为您不应该从另一项服务中引用一项服务。相反,您应该在服务后面有另一个抽象层,这些服务将用于公共逻辑。因此,服务将包含不能(不应该)重用的逻辑,而助手将包含共享逻辑。

例如:

    public class UserHelper{
      //add all your common methods here
    }
    public class JobService {
      private UserHelper us;

      public JobService (UserHelper us) {
        this.us = us;
      }

      public void addJob(Job job) {
 // calls helper class
      }
    }

    public class UserService {

      public UserService(UserHelper js) {
        this.js = js;
      }

      public void deleteUser(User u) {
        // calls helper class
      }
    }   

通过这种方式,循环引用不会有任何问题,并且您将拥有一个包含需要由不同服务重用的逻辑的地方。

另外,我更喜欢彼此完全隔离的服务。

【讨论】:

  • 您的概念在这里很可靠,但是我通常认为“Helper”类是根本不与持久层交互的类。在调用服务的服务层之上,是否存在人们通常使用的其他概念/术语?通常那只是我的客户。
  • @AdamLevitt,对我来说也是如此,通常那是我的客户。我同意,“帮助”类用于与持久层进行通信有点尴尬。另一种可能性是从客户端调用不同的服务方法。因此,例如,控制器(Web 应用程序)将首先调用用户服务,然后调用作业服务。不过,这有点破坏了服务的原子性质,因为一项服务将不负责执行的整个操作(添加具有作业的用户)。但是,我认为我更喜欢它而不是循环依赖。
  • 我想我只是要创建另一个第三个服务,并尽我所能想出一个恰当描述它的形容词。我注意到 UserService 只有一个地方指的是 JobService --> 在注册时。我正在考虑 MembershipService。
  • @AdamLevitt,我会考虑后一种方法(从客户端调用不同的服务),以防您可能需要添加用户而不向其添加工作。在这种情况下,您可能希望能够调用用户服务而不需要引用作业服务。另外,在我之前的评论中,我认为“原子”一词的使用并不准确。
  • 是的,我认为我最喜欢这个方向......我赞成你的 cmets :) 我很高兴我的问题促成了这样的讨论。
【解决方案2】:

您遇到的问题实际上与您的 DI 容器的限制无关,但这是一个普遍的问题。即使没有任何容器,也无法创建这些类型:

var job = new JobService([what goes here???]);
var user = new UserService(job);

因此,一般的答案是将其中一个依赖项提升为属性。这将打破依赖循环:

var job = new JobService();
var user = new UserService(job);

// Use property injection
job.User = user;

但要避免使用比严格需要更多的属性。这些依赖循环应该是非常少见的,这使得将类型连接在一起或验证 DI 配置的正确性变得更加困难。构造函数注入使这变得更加容易。

【讨论】:

  • 同意...这只是一般的设计问题。克服这个问题的好模式是什么?
  • @AdamLevitt:我认为没有通用的设计模式。如果确实需要循环依赖,请仔细查看您的设计。您也许可以从两个类所依赖的现有类中提取一个新类。或者你可以像explained here这样把东西连在一起。
  • 我正在努力寻找,确实存在依赖关系。我正在讨论将调用拉到一个新创建的服务类中,但我无法理解它。
  • 通常构造函数注入应该优先于属性注入。 fascinatedwithsoftware.com/blog/post/2012/02/01/…
【解决方案3】:

这在 Autofac 中不起作用。请参阅文档的circular dependencies 部分。

Constructor/Constructor Dependencies 有循环的两种类型 不支持构造函数依赖项。你会得到一个例外 当您尝试解析以这种方式注册的类型时。

您可能会使用relationship typesFunc&lt;&gt;Lazy&lt;&gt;)来打破循环。

您的代码有点过于笼统,无法提出适当的解决方案,但无论您使用什么 IoC 容器,您都应该考虑更改依赖关系的方向。

public class JobService {
  private UserService us;

  public JobService (UserService us) {
    this.us = us;
  }

  public void addJob(Job job) {
    // needs to make a call to user service to update some user info
  }
}

public class UserService {
  private JobService js;
  public UserService(Func<JobService> jsFactory) {
    this.js = jsFactory(this);
  }

  public void deleteUser(User u) {
    // needs to call the job service to delete all the user's jobs
  }
}        

或者,在您的示例中,您可以移动 deleteUser 并创建一个方法,删除作业服务上的所有作业,而不是使用 id 来引用用户。这通过使用 id 打破了依赖关系。

另一种选择是将作业服务作为参数传递给deleteUser

【讨论】:

  • 我想知道回调作为一种解决方案是否有意义,问题是我的工作都在一个事务范围内完成。
  • 你在说什么?
  • 让我对我的原始帖子进行编辑,使其更加具体。
  • 戴夫,关于您的最后一条评论,关键是我需要确保当用户被删除时,他们的所有工作都在用户之前被删除,以保持数据完整性.这就是我努力消除 UserService 对 JobService 的依赖的地方。我会在上面放一些实现代码,让它更清楚。
  • @AdamLevitt:考虑在您的数据库中使用级联删除:ALTER TABLE Job ADD CONSTRAINT fk_job_user FOREIGN KEY (userID) REFERENCES User (userID) ON DELETE CASCADE;
【解决方案4】:

您可以使用事件来解耦服务。当一个动作被执行时,不是调用另一个服务的依赖方法,而是引发一个事件。然后,集成商可以通过事件连接服务。一个服务甚至不知道另一个服务的存在。

public class JobService
{
    public event Action<User, Job> JobAdded;

    public void AddJob(User user, Job job)
    {
        //TODO: Add job.
        // Fire event
        if (JobAdded != null) JobAdded(user, job);
    }

    internal void DeleteJobs(int userID)
    {
        //TODO: Delete jobs
    }
}

public class UserService
{
    public event Action<User> UserDeleted;

    public void DeleteUser(User u)
    {
        //TODO: Delete User.
        // Fire event
        if (UserDeleted != null) UserDeleted(u);
    }

    public void UpdateUser(User user, Job job)
    {
        //TODO: Update user
    }
}

集成商连接服务

public static class Services
{
    public static JobService JobService { get; private set; }
    public static UserService UserService { get; private set; }

    static Services( )
    {
        JobService = new JobService();
        UserService = new UserService();

        JobService.JobAdded += JobService_JobAdded;
        UserService.UserDeleted += UserService_UserDeleted;
    }

    private static void UserService_UserDeleted(User user)
    {
        JobService.DeleteJobs(user.ID);
    }

    private static void JobService_JobAdded(User user, Job job)
    {
        UserService.UpdateUser(user, job);
    }
}

(注意:我稍微简化了事件引发。这样不是线程安全的。但您可以假设事件是提前订阅的,以后不会更改。)

【讨论】:

  • 嗯...这非常有趣。什么会使这个线程安全?
  • 另外,客户会在哪里调用集成商?
  • 问题是在测试if (UserDeleted != null) 和实际调用事件之间,另一个线程可以取消订阅该事件。因此,您首先将其分配给一个变量(这是有效的,因为事件是不可变的)。 var eh = UserDeleted; if (eh != null) eh(...);
  • @AdamLevitt:我将Integrator 类更改为静态Services 类,使用静态构造函数自动进行初始化并公开服务实例。上一堂课只是为了展示接线,而不是真实世界的课。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-02-24
相关资源
最近更新 更多