我是领域驱动设计的新手,我对一些概念有所怀疑(希望这是一个正确的问题)。 我知道在DDD中我应该避免使用贫血模型,所以考虑一个社交网络模型谁应该(保存)两个朋友之间的友谊? 我想象有一个代表用户的类(使用类似Java的语法):
class User{
String username
List<User> friends
}
那么,它应该有一个添加朋友的方法
class User{
void friendship(User friend)
}
或者我应该使用服务吗?
class UserService{
void friendship(User user, User user2)
}
答案 0 :(得分:6)
我的想法是,这是关系数据库理论被称为“弱实体”的一个例子。 Friendship
只能通过友谊中涉及的两个User
的标识符来识别,但可以拥有自己的属性,例如何时创建它以及它是什么类型的关系。
我会把它作为自己的实体,并且可能将它隐藏在由User
对象暴露的外观后面:
class User {
protected List<Friendship> _friendships { get; private set; }
public IEnumerable<User> Friends {
get { return _friendships.Select( /* get other user */ ); }
}
public void AddFriend(User otherUser) {
// check to see if friendship exists
// if not, create friendship
// do other friendshippy things
// make sure the other user knows about our friendship
// and gets to do its friendshippy things
otherUser.AddFriend(this);
}
}
答案 1 :(得分:5)
我会使用像
这样的东西public sealed class Username : IEquatable<Username> { /* string wrap here */ }
public class User
{
private readonly Username _username;
private readonly HashSet<Username> _friends;
public User(Username username)
{
if (null == username) throw new ArgumentNullException("username");
_username = username;
_friends = new HashSet<Username>();
}
public Username Name { get {return _username; } }
public void Befriend(User user)
{
if (null == user) throw new ArgumentNullException("user");
_friends.Add(user.Name);
}
public bool IsFriendsOf(User user)
{
if (null == user) throw new ArgumentNullException("user");
return _friends.Contains(user.Name);
}
}
请注意,根据Law Of Demeter,用户不会公开任何集合。如果您确实需要它们,我会为朋友公开IEnumerable<Username>
。
此外,在DDD中,所有query and commands都应该成为ubiquituous language的一部分(这就是我使用Befriend
代替AddFriend
)的原因。
但是,我要说这看起来有点CRUD要求DDD。 如果您(至少)不需要域专家来理解域名,则根本不需要DDD 。如果您不需要DDD,它将成为您项目中最昂贵的错误。
修改强>
让我们假设领域专家说“友谊永远是互惠的”(根据 guillaume31 的建议):通过建模idempotent命令,您可以非常轻松地确保这样的业务规则。 Befriend
命令变为:
public void Befriend(User user)
{
if (null == user) throw new ArgumentNullException("user");
if(_friends.Add(user.Name))
{
user.Befriend(this);
}
}
您可以始终使用此类属性对幂等命令进行建模,但有时需要进行更多分析以确保其参数及其内部状态提供所需的一切。
答案 2 :(得分:4)
我想;友谊是一个集合根本身。它可以直接在应用程序服务中创建,也可以将创建委托给域服务。
如果用户特定的验证需要,域服务可以要求用户聚合进行验证。或者将用户聚合分发给友谊聚合创建者/构造函数。
然后,友情repositoy可以轻松返回给定用户的朋友列表。
即使是友谊聚合也没有丰富的模特或行为;它有一个单独的一致性边界。
此外,如果我们使用事件采购;一个人可以听友谊治疗事件;并通知双方用户情况。
答案 3 :(得分:2)
这类似于银行域中的transferFunds()
问题 - 您是否应该在源帐户或目标帐户上调用该方法?并且Account
首先应该能够操纵另一个Account
的{{1}}吗?
当某些行为似乎不适合现有实体或操纵多个实体时,DDD域服务会派上用场。这不会使您的域贫血,因为服务是域的一部分,并且拥有有限数量的没有行为的实体并不意味着您的整个模型无论如何都是贫血。
如果友谊关系从一开始就是互惠的,那么将友谊创建委托给域服务可能更合乎逻辑,而不是让Balance
修改另一个User
的朋友列表。
否则,User
方法可以正常工作。
答案 4 :(得分:2)
弱域或贫血域模型只是意味着您的“域对象”是DTO,没有任何行为。您基本上得到的是事务脚本模式,其中DTO被加载,修改并再次保存。 CRUD,换句话说。对于许多应用程序来说这很好,这些应用程序没有足够复杂的规则可以从DDD方法中受益。
域对象应封装行为。这是他们的本质。任何公共状态(可能没有公共getter或setter)都应该是readonly,如果你想改变状态,你可以调用一个与用例/业务需求相关的方法。
您的所有“域逻辑”都应该在这些类中。域逻辑只是一个名称,用于描述所选域的规则和操作参数。无论是银行,零售,人力资源等。当您的域名专家解释“按卡付款”用户案例并告诉您“我们无法打开直到PDC机器联系银行。”,这是一个商业规则/不变/域逻辑。
您通常会将域对象(由实体和值对象组成)组合成聚合,聚合定义了必须满足给定规则集的边界。作为此域对象图的根的实体称为聚合根,并且仅对其他对象可以包含引用的聚合根进行聚合。在您的情况下,User
是一个实体,因为它是聚合中唯一的对象,它也是聚合根。例如:
public class User // Entity and also the Aggregate Root
{
private readonly IList<Friendship> _friends = new List<Friendship>();
public void Befriend(User user)
{
_friends.Add(new Friendship(/* Date? */, user));
}
public class Friendship // Entity
{
// ... Date?
private User _friend { get; private set; }
public Friendship(/* Date? */, User friend)
{
_friend = friend;
}
}
}
这不是一个很好的例子,因为理论上你需要为一对中的两个朋友中的每一个调用它,但任何事务应该只执行一个操作,而不是两个。在这种情况下,您将介绍流程管理器的概念。这些更多的对象涉及协调本质上是一个长期运行的事务(两个朋友彼此成为朋友)。你可能会创建一个友谊(作为聚合根),它的创建会产生某种事件驱动的过程,其中涉及的朋友被加载,成为朋友并保存。
答案 5 :(得分:1)
两个用户之间的友谊应该是一个单独的聚合(AR)。作为该AR的ID,我建议使用用户的ID。通过这种方式,您可以使用低压力的小骨料进行缩放/分割。用户之间发送的消息也是如此。每条消息都是一个聚合。
这就是我们设计它的方式,并且它是成功的。
祝你好运答案 6 :(得分:0)
考虑到一个用户可以拥有的无限量的友谊,我认为不应该将其建模为用户AR中的值对象。 AR应该尽可能小,每次想要改变状态时加载10k的朋友并不理想。
我可以选择将Friendship
和FriendRequest
作为单独的根。 FriendRequest
是包含from_user_id和to_user_id的单一方向。它可以被忽略,拒绝或接受。在接受时,建立双向友谊。友情可通过物化视图(例如弹性搜索)进行搜索,并通过用户配置文件更新事件最终更新的用户名进行增强。
对于用户UI,如果需要有条件的befriend-button或友情请求指示符,可以使用Friendship
和/或FriendRequest
的ID维护链接表/搜索视图。