从域访问存储库


14

假设我们有一个任务记录系统,当记录一个任务时,用户指定一个类别,并且该任务默认为“未完成”状态。在这种情况下,假定类别和状态必须作为实体实施。通常我会这样做:

应用层:

public class TaskService
{
    //...

    public void Add(Guid categoryId, string description)
    {
        var category = _categoryRepository.GetById(categoryId);
        var status = _statusRepository.GetById(Constants.Status.OutstandingId);
        var task = Task.Create(category, status, description);
        _taskRepository.Save(task);
    }
}

实体:

public class Task
{
    //...

    public static void Create(Category category, Status status, string description)
    {
        return new Task
        {
            Category = category,
            Status = status,
            Description = descrtiption
        };
    }
}

我这样做是因为我一直被告知实体不应该访问存储库,但是如果这样做的话,对我来说更有意义:

实体:

public class Task
{
    //...

    public static void Create(Category category, string description)
    {
        return new Task
        {
            Category = category,
            Status = _statusRepository.GetById(Constants.Status.OutstandingId),
            Description = descrtiption
        };
    }
}

无论如何,状态存储库都是依赖注入的,因此没有真正的依赖关系,在我看来,是由域决定任务默认为未完成。以前的版本感觉像是由应用程序层做出该决定。为什么这不是一个可能的原因,为什么存储合同通常在域中?

这是一个更极端的示例,在此域决定紧急程度:

实体:

public class Task
{
    //...

    public static void Create(Category category, string description)
    {
        var task = new Task
        {
            Category = category,
            Status = _statusRepository.GetById(Constants.Status.OutstandingId),
            Description = descrtiption
        };

        if(someCondition)
        {
            if(someValue > anotherValue)
            {
                task.Urgency = _urgencyRepository.GetById
                    (Constants.Urgency.UrgentId);
            }
            else
            {
                task.Urgency = _urgencyRepository.GetById
                    (Constants.Urgency.SemiUrgentId);
            }
        }
        else
        {
            task.Urgency = _urgencyRepository.GetById
                (Constants.Urgency.NotId);
        }

        return task;
    }
}

您没有办法传递所有可能的Urgency版本,也没有办法在应用程序层中计算此业务逻辑,因此,这肯定是最合适的方法吗?

那么这是从域访问存储库的有效理由吗?

编辑:非静态方法也可能是这种情况:

public class Task
{
    //...

    public void Update(Category category, string description)
    {
        Category = category,
        Status = _statusRepository.GetById(Constants.Status.OutstandingId),
        Description = descrtiption

        if(someCondition)
        {
            if(someValue > anotherValue)
            {
                Urgency = _urgencyRepository.GetById
                    (Constants.Urgency.UrgentId);
            }
            else
            {
                Urgency = _urgencyRepository.GetById
                    (Constants.Urgency.SemiUrgentId);
            }
        }
        else
        {
            Urgency = _urgencyRepository.GetById
                (Constants.Urgency.NotId);
        }

        return task;
    }
}

Answers:


8

你在混

实体不应该访问存储库

(这是一个很好的建议)

域层不应访问存储库

(只要您的存储库是域层的一部分,而不是应用程序层的一部分,这可能是一个错误的建议)。实际上,您的示例没有显示实体访问存储库的情况,因为您使用的是不属于任何实体的静态方法。

如果您不想将创建逻辑放入实体类的静态方法中,则可以引入单独的工厂类(作为域层的一部分!),然后将创建逻辑放在那里。

编辑:以您的Update示例为例:给定_urgencyRepositorystatusRepository 是类的成员Task,定义为某种接口,现在您需要将它们注入到任何Task实体中,然后才能使用Update(例如在Task构造函数中)。或者,您将它们定义为静态成员,但是要当心,这很容易导致多线程问题,或者当您同时为不同的Task实体需要不同的存储库时,就容易引起问题。

这种设计使创建独立的Task实体变得更加困难,从而为Task实体编写单元测试变得更加困难,根据Task实体编写自动测试也变得更加困难,并且由于每个Task实体现在都需要花费更多的内存开销,因此认为有两个对仓库的引用。当然,您的情况可能可以接受。另一方面,创建一个单独的实用程序类TaskUpdater来保留对正确存储库的引用可能是一个更好的解决方案,至少在某些时候是这样。

重要的部分是:TaskUpdater仍将是域层的一部分!仅仅因为将更新或创建代码放在单独的类中并不意味着您必须切换到另一层。


我进行了编辑,以显示此方法与静态方法一样,也适用于非静态方法。我从未真正想到过工厂方法不属于实体。
Paul T Davies 2012年

@PaulTDavies:看到我的编辑
布朗博士

我同意您在这里所说的内容,但是我要添加一个简洁明了的内容,Status = _statusRepository.GetById(Constants.Status.OutstandingId)业务规则,您可以将其读为“业务决定所有任务的初始状态将是杰出的”,这就是为什么该代码行不属于存储库内部,其唯一关注的是通过CRUD操作进行数据管理。
Jimmy Hoffa 2012年

@JimmyHoffa:嗯,这里没有人建议将这种类型的行放入一个存储库类中,无论是OP还是我都没有-那么您的意思是?
Doc Brown

我非常喜欢TaskUpdater作为domian服务的想法。某种程度上,似乎只是为了保留DDD原理而已,似乎有点a昧,但这确实意味着我可以避免每次使用Task时都注入存储库。
Paul T Davies 2012年

6

我不知道您的状态示例是真实代码还是仅出于演示目的,但是让我感到奇怪的是,当其ID是一个常量定义时,您应该将Status实现为一个实体(更不用说聚合根)在代码-中Constants.Status.OutstandingId。这是否破坏了您可以在数据库中添加任意数量的“动态”状态的目的?

我要补充一点,在您的情况下,a的构造Task(包括在需要时从StatusRepository获取正确状态的工作)可能值得TaskFactory保留而不是停留在其Task本身中,因为这是对象的简单组合。

但是:

我一直被告知实体不应该访问存储库

这种说法充其量是不精确和过分简单的,最坏的情况是令人误解和危险的。

在域驱动的体系结构中,一个实体不应该知道如何存储自身已被普遍接受-这就是持久性无知原则。因此,无需调用其存储库即可将自身添加到存储库。它应该知道如何(以及何时)存储其他实体吗?同样,这种责任似乎属于另一个对象-可能是一个知道当前用例的执行上下文和总体进度的对象,例如应用程序层服务。

实体可以使用存储库来检索另一个实体吗?90%的时间不是必须的,因为它所需的实体通常在其聚合范围内,或者可以通过遍历其他对象来获得。但是有时候它们不是。例如,如果采用分层结构,则实体经常需要访问其所有祖先,特定孙子等,作为其内在行为的一部分。他们没有直接提及这些异地亲戚。将这些亲戚作为操作参数传递给他们将是不便的。那么,为什么不使用存储库来获取它们-前提是它们是聚合根?

还有其他一些例子。问题是,有时您无法将其放置在域服务中,因为它似乎非常适合现有实体。但是,此实体需要访问存储库来水合根或无法传递给根的根集合。

因此,从实体访问存储库本身并不是一件坏事,它可以采取各种形式,这些形式是从灾难性到可接受的各种设计决策得出的。


我不同意实体应该使用存储库来访问它已经与之有关系的实体-您应该能够遍历对象图来访问该实体。以这种方式使用存储库绝对不能。我在这里讨论的是实体尚未引用的实体,但需要在某些业务条件下创建实体。
Paul T Davies 2012年

好吧,如果您读得很好,我们完全同意...
guillaume31

2

这是我在域内不使用枚举或纯查询表的原因之一。紧急状态和状态都是状态,并且存在与状态直接相关联的逻辑(例如,我可以将哪些状态转换为给定的当前状态)。同样,通过将状态记录为纯值,您会丢失信息,例如任务处于给定状态的时间。我将状态表示为类层次结构。(在C#中)

public class Interval
{
  public Interval(DateTime start, DateTime? end)
  {
    Start=start;
    End=end;
  }

  //To be called by internal framework
  protected Interval()
  {
  }

  public void End(DateTime? when=null)
  {
    if(when==null)
      when=DateTime.Now;
    End=when;
  }

  public DateTime Start{get;protected set;}

  public DateTime? End{get; protected set;}
}

public class TaskStatus
{
  protected TaskStatus()
  {
  }
  public Long Id {get;protected set;}

  public string Name {get; protected set;}

  public string Description {get; protected set;}

  public Interval Duration {get; protected set;}

  public virtual TNewStatus TransitionTo<TNewStatus>()
    where TNewStatus:TaskStatus
  {
    throw new NotImplementedException();
  }
}

public class OutStandingTaskStatus:TaskStatus
{
  protected OutStandingTaskStatus()
  {
  }

  public OutStandingTaskStatus(bool initialize)
  {
    Name="Oustanding";
    Description="For tasks that need to be addressed";
    Duration=new Interval(DateTime.Now,null);
  }

  public override TNewStatus TransitionTo<TNewStatus>()
  {
    if(typeof(TNewStatus)==typeof(CompletedTaskStatus))
    {
      var transitionDate=DateTime.Now();
      Duration.End(transitionDate);
      return new CompletedTaskStatus(true);
    }
    return base.TransitionTo<TNewStatus>();
  }
}

CompletedTaskStatus的实现几乎相同。

这里有几件事要注意:

  1. 我使默认构造函数受到保护。这样,框架就可以在从持久性中拉出对象时调用它(EntityFramework Code-first和NHibernate都使用从您的域对象派生的代理来发挥作用)。

  2. 出于相同的原因,许多属性设置器也受到保护。如果要更改时间间隔的结束日期,则必须调用Interval.End()函数(这是“域驱动设计”的一部分,提供有意义的操作,而不是“贫血域对象”。

  3. 我在这里没有显示它,但是任务同样会隐藏其如何存储其当前状态的详细信息。我通常有一个历史状态的受保护列表,我可以让公众查询它们是否感兴趣。否则,我将当前状态公开为查询HistoricalStates.Single(state.Duration.End == null)的获取方法。

  4. TransitionTo函数很重要,因为它可以包含有关哪些状态对过渡有效的逻辑。如果您只有一个枚举,则该逻辑必须位于其他位置。

希望这可以帮助您更好地了解DDD方法。


1
如果不同的状态在您的状态模式示例中有不同的行为,那么这肯定是正确的方法,它当然也解决了所讨论的问题。但是,如果每个州只有不同的值而不是不同的行为,那么我将很难为每个州证明一个类的合理性。
Paul T Davies 2012年

1

我一直试图解决相同的问题,我决定我希望能够像这样调用Task.UpdateTask(),尽管我宁愿它是特定于域的,但在您的情况下,我可能将其称为Task.ChangeCategory (...)表示操作,而不仅仅是CRUD。

无论如何,我尝试了您的问题,并想到了这个……吃我的蛋糕,也一起吃。这个想法是动作在实体上发生,但没有注入所有依赖项。相反,工作是在静态方法中完成的,因此它们可以访问实体的状态。工厂将所有工作放在一起,通常将拥有完成实体需要完成的工作所需的一切。客户端代码现在看起来干净清晰,您的实体不依赖于任何存储库注入。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace UnitTestProject2
{
    public class ClientCode
    {
        public void Main()
        {
            TaskFactory factory = new TaskFactory();
            Task task = factory.Create();
            task.UpdateTask(new Category(), "some value");
        }

    }
    public class Category
    {
    }

    public class Task
    {
        public Action<Category, String> UpdateTask { get; set; }

        public static void UpdateTaskAction(Task task, Category category, string description)
        {
            // do the logic here, static can access private if needed
        }
    }

    public class TaskFactory
    {      
        public Task Create()
        {
            Task task = new Task();
            task.UpdateTask = (category, description) =>
                {
                    Task.UpdateTaskAction(task, category, description);
                };

            return task;
        }

    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.