【问题标题】:How to break circular dependencies between repositories如何打破存储库之间的循环依赖关系
【发布时间】:2012-03-09 16:23:04
【问题描述】:

首先,不,我没有使用 ORM,也不允许使用。我必须使用 ADO.NET 手动滚动我的存储库。

我有两个对象:

public class Firm
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public virtual IEnumerable<User> Users { get; set; }
}

public class User
{
    public Guid Id { get; set; }
    public string Username { get; set; }
    public Firm Firm { get; set; }
}

注意彼此的引用,一个公司有一个用户列表,每个用户只有一个公司。

现在我想设计我的存储库:

public interface IFirmRepository
{
    IEnumerable<Firm> FindAll();
    Firm FindById(Guid id);
}

public interface IUserRepository
{
    IEnumerable<User> FindAll();
    IEnumerable<User> FindByFirmId(Guid firmId);
    User FindById(Guid id);
}

到目前为止,一切都很好。我想从我的 UserRepository 为每个用户加载公司。 FirmRepository 知道如何通过持久性创建一个 Firm,所以我想把这些知识保存在 FirmRepository 中。

public class UserRepository : IUserRepository
{
    private IFirmRepository _firmRepository;

    public UserRepository(IFirmRepository firmRepository)
    {
        _firmRepository = firmRepository;
    }

    public User FindById(Guid id)
    {
        User user = null;
        using (SqlConnection connection = new SqlConnection(_connectionString))
        {
            SqlCommand command = connection.CreateCommand();
            command.CommandType = CommandType.Text;
            command.CommandText = "select id, username, firm_id from users where u.id = @ID";
            SqlParameter userIDParam = new SqlParameter("@ID", id);
            command.Parameters.Add(userIDParam);
            connection.Open();
            using (SqlDataReader reader = command.ExecuteReader())
            {
                if (reader.HasRows)
                {
                    user = CreateListOfUsersFrom(reader)[0];
                }
            }
        }
        return user;
    }

    private IList<User> CreateListOfUsersFrom(SqlDataReader dr)
    {
       IList<User> users = new List<User>();
       while (dr.Read())
       {
           User user = new User();
           user.Id = (Guid)dr["id"];
           user.Username = (string)dr["username"];
           //use the injected FirmRepository to create the Firm for each instance of a User being created
           user.Firm = _firmRepository.FindById((Guid)dr["firm_id"]);
       }
       dr.Close();
       return users;
    }

}

现在,当我通过 UserRepository 加载任何用户时,我可以要求 FirmRepository 为我构建用户的公司。到目前为止,这里没有什么太疯狂的事情。

现在的问题。

我想从我的 FirmRepository 加载用户列表。 UserRepository 知道如何从持久性中创建用户,因此我想将这些知识与 UserRepository 一起保存。因此,我将对 IUserRepository 的引用传递给 FirmRepository:

public class FirmRepository : IFirmRepository
{
    private IUserRepository
    public FirmRepository(IUserRepository userRepository)
    {

    }
}

但是现在我们遇到了一个问题。 FirmRepository 依赖于 IUserRepository 的实例,而 UserRepository 现在依赖于 IFirmRepository 的实例。因此,如果没有另一个存储库的实例,就无法创建一个存储库。

如果我将 IoC 容器排除在这个等式之外(我应该这样做,b/c 单元测试不应该使用 IoC 容器),我就无法完成我想要做的事情。

不过,没问题,我将创建一个 FirmProxy 来延迟加载来自 Firm 的用户集合!这是一个更好的主意,b/c 当我去获取公司或公司列表时,我不想一直加载所有用户。

public class FirmProxy : Firm
{
    private IUserRepository _userRepository;
    private bool _haveLoadedUsers = false;
    private IEnumerable<User> _users = new List<User>();

    public FirmProxy(IUserRepository userRepository)
        : base()
    {
        _userRepository = userRepository;
    }

    public bool HaveLoadedUser()
    {
        return _haveLoadedUsers;
    }

    public override IEnumerable<User> Users
    {
        get
        {
            if (!HaveLoadedUser())
            {
                _users = _userRepository.FindByFirmId(base.Id);
                _haveLoadedUsers = true;
            }
            return _users;
        }
    }

}

所以,现在我有一个很好的代理对象来促进延迟加载。因此,当我从持久性中在 FirmRepository 中创建一个 Firm 时,我改为返回一个 FirmProxy。

public class FirmRepository : IFirmRepository
{

    public Firm FindById(Guid id)
    {
        Firm firm = null;
        using (SqlConnection connection = new SqlConnection(_connectionString))
        {
            SqlCommand command = connection.CreateCommand();
            command.CommandType = CommandType.Text;
            command.CommandText = "select id, name from firm where id = @ID";
            SqlParameter firmIDParam = new SqlParameter("@ID", id);
            command.Parameters.Add(firmIDParam);
            connection.Open();
            using (SqlDataReader reader = command.ExecuteReader())
            {
                if (reader.HasRows)
                {
                    firm = CreateListOfFirmsFrom(reader)[0];
                }
            }
        }
        return firm;
    }

private IList<Firm> CreateListOfFirmsFrom(SqlDataReader dr)
{
    IList<FirmProxy> firms = new List<FirmProxy>([need an IUserRepository instance here!!!!]);
    while (dr.Read())
    {

    }
    dr.Close();
    return firms;
}

但这还是不行!!!

为了返回 FirmProxy 而不是 Firm,我需要能够在我的 FirmRepository 类中新建一个 FirmProxy。好吧,FirmProxy 需要一个 IUserRepository 实例 b/c UserRepository 包含如何从持久性创建用户对象的知识。由于 FirmProxy 需要一个 IUserRepository,我的 FirmRepository 现在也需要一个 IUserRepository,我马上回到原点!

因此,鉴于这个冗长的解释/源代码,我如何才能在没有以下情况的情况下从 FirmRepository 创建 User 实例和从 UserRepository 创建 Firm 实例:

  1. 将用户创建代码放入 FirmRepository。我不喜欢这个。为什么 FirmRepository 应该知道有关创建用户实例的任何信息?对我来说,这违反了 SoC。
  2. 不使用服务定位器模式。如果我走这条路,我觉得这是出了名的难以测试。此外,具有显式依赖关系的对象的构造函数使这些依赖关系显而易见。
  3. 属性注入而不是构造函数注入。这并不能解决问题,我仍然需要一个 IUserRepository 实例来新建一个 FirmProxy,无论依赖项如何注入到 FirmProxy 中。
  4. 必须“简化”公司或用户对象,并在用户上公开一个公司 ID,例如,而不是公司。如果我只是在做 id,那么从 UserRepository 加载 Firm 的要求就消失了,但随之而来的是能够在给定 User 实例的上下文中向 Firm 对象询问任何内容的丰富性。
  5. 求助于 ORM。再一次,我想这样做,但我做不到。没有ORM。这就是规则(是的,这是一条糟糕的规则)
  6. 将我所有的可注入依赖项保留为从应用程序的最低级别注入的依赖项,即 UI(在我的例子中,是一个 .NET Web 项目)。没有作弊,并在 FirmProxy 中使用 IoC 代码为我新建适当的依赖项。无论如何,这基本上是使用服务定位器模式。

我想到了 NHiberante 和 Enitity Framework,对于我提供的一个简单示例,他们似乎没有问题弄清楚如何生成 sql。

有没有其他人有任何其他想法/方法/等...可以帮助我在没有 ORM 的情况下实现我想要做的事情?

或者也许有不同/更好的方法来解决这个问题?我不想失去的是从用户访问公司的能力,或者获取给定公司的用户列表

【问题讨论】:

标签: c# .net dependency-injection repository-pattern circular-dependency


【解决方案1】:

您需要更清楚地考虑聚合根对象,即每个存储库的焦点。您不会为数据库中的每个表创建存储库,这不是目标。目的是识别您需要使用的聚合根对象,包括子表/记录,即用户(公司、地址、访问权限)。这就是您的存储库将返回到您的应用程序的内容。

您遇到的困难是表明您的存储库结构不正确。每当我发现我的代码变得太难时,它都会提醒我我可能做错了,我以此为生;)

【讨论】:

  • +1 - 在这种情况下,似乎公司和用户都是聚合根。在这些场景中,我通常在两个根之间有一个软引用(通过引用键),如果我需要一个将它们连接在一起的数据结构,我会明确地这样做(可能是查询对象或视图模型等)。跨度>
  • 我看到公司是聚合根,用户属于该根。但与此同时,我不认为公司是一个聚合根,这证明我可能想与用户合作,并且根本不打扰用户的公司。然而,当我将用户持久化回数据库时,在 agg root 示例中,我必须持久化所有公司,以及所有用户。有趣的是,MattDavey 说他们都是 agg 根。他带我回到软引用,我希望我可以远离它,我喜欢使用来自每个实体的对象引用。
  • 我通常认为聚合根的方式是遵循“级联删除”的规则。如果您删除聚合根,则下面的所有内容也应该被删除。如果你删除一个公司,所有的用户都应该被删除吗?
  • 现在,是的。但这意味着我需要通过我的 Agg Root,即公司来处理我的所有用户交互。这也意味着 FirmRepository 在插入和更新方面有很多繁重的工作要做。它现在必须潜在地处理对用户列表中的一个或多个用户的更新。这意味着我需要开始对自加载公司以来已添加/更改的用户实体进行脏跟踪。我认为 ORM 可以为我处理这个问题,但在我的情况下,听起来我必须编写这个跟踪,并在我自己持久化时处理我的 agg 根中要做的事情。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-23
  • 2020-01-29
  • 1970-01-01
  • 2021-12-21
  • 2021-02-21
  • 2016-09-24
相关资源
最近更新 更多