为个人/客户建模联系人详细信息

本文关键字:建模 联系人 详细信息 客户 | 更新日期: 2023-09-27 18:30:41

我想知道是否有更优雅的方式来管理个人的联系方式。暂时忘记SQL方面的事情,我很好奇如何尝试通过DDD方法推动这一点。

我正在玩弄一些代码,以努力适应整个DDD,并提出了以下看起来很糟糕的方法。

首先,我有一个名为Person的对象(为本文的目的进行了简化),我设想了添加和基本上管理不同的个人沟通方法的方法。

public class Person
{
    public Person()
    {
        this.ContactDetails = new List<ContactDetails>();
    }
    public void AssociateContactDetails(ContactDetails contactDetails)
    {
        var existingContactDetails = this.ContactDetails.FirstOrDefault(x => x.ContactType == contactDetails.ContactType);
        if (existingContactDetails != null)
        {
            this.ContactDetails.Remove(existingContactDetails);
        }
        this.ContactDetails.Add(contactDetails);
    }
    public IList<ContactDetails> ContactDetails { get; private set; }
}

两种方法浮现在脑海中。一个我有一个相当简单的对象,如下所示,它非常通用(松散地使用术语)。

public enum ContactType
{
    Email, Telephone, Mobile, Post
}   
public class ContactDetails
{
    private readonly ContactType contactType;
    private readonly string value;
    public ContactDetails(ContactType contactType, string value)
    {
        this.contactType = contactType;
        this.value = value;
    }
    public ContactType ContactType
    {
        get { return this.contactType; }
    }
    public string Value
    {
        get { return this.value; }
    }
}   

后来我用这种方法把自己逼到了墙角,因为尽管它适用于电子邮件和电话等琐碎项目,但当涉及到邮政之类的事情时,字符串并不能完全削减它。因此,在此之后,我将朝着让每种通信机制由其自己的类型表示的方法迈进,即:

public class Post
{
    public Address PostalAddress { get; set; }
}
public class Mobile
{
    public string MobileNo { get; set; }
}
public class Telephone
{
    public string AreaCode { get; set; }
    public string TelephoneNo { get; set; }
}
public class Email
{
    public string EmailAddress { get; set; }
}

然后,每种类型都可以在 Person 类中表示为集合或单个实例吗?似乎冗长,但可能更具可读性和可维护性。

我想的问题是,是否有一种更优雅的方式来实现这样的功能,以及是否有人可以指出我一个类似的例子的方向。我想这是一个需要克服的常见事情/问题。

干杯,DS。

为个人/客户建模联系人详细信息

我们确定什么是联系方法"电子邮件","电话"和"地址",因此在确定了这些之后,我们首先要做的是对这些概念进行建模,同时考虑到它们的真实含义。让我们以"电子邮件"为例,看看它到底是什么,以便正确建模。它是一个值对象(一个不可变的对象),一旦创建它就永远不会改变,就像整数也是一个不可变的对象一样。不同之处在于,为了对整数进行建模,我们可以使用任何编程语言提供的 int 类型,但问题是我们使用什么类来建模 en 电子邮件?大多数人会使用 String 实例来对电子邮件进行建模,但这可以吗?为了回答它,让我们看看字符串对象知道响应的协议(消息集)是什么:"charAt(anIndex),replace(aString,anotherString)等......"。想象一下,如果我们使用 String 类对电子邮件进行建模,我们可以要求电子邮件"replace(aString, otherString)"。这听起来很奇怪,该消息不应该是电子邮件应向其他对象公开的行为的一部分。同样如此重要,我们说电子邮件是不可变的,它不能暴露最终改变它状态的行为。因此,很明显我们需要创建一个全新的抽象来对电子邮件进行建模,它是什么?电子邮件类终于进来了!!我知道你建议过,但我只是想让你明白为什么我们需要创建一个电子邮件类。首先,这是DDD(面向对象),所以忘记避免设置器和getter。在您创建的电子邮件类中,您公开了一个 setter 方法,这意味着您可以更改电子邮件,它与电子邮件的性质(不可变)相矛盾。电子邮件从创建之时起就是不可变的:

Email.fromString("monicalewinsky@gmail.com");

这和做一样

new Email("monicalewinsky@gmail.com");

fromString 方法是一个工厂方法,它为我们的领域模型添加了语义。这在 smalltalk 中很常见,而不是直接调用构造函数。我们完成了吗???一点也不。只要电子邮件实例有效,就应该创建该实例,因此电子邮件类应断言从中创建的字符串有效:

Email(String anEmailStringRepresentation) {
    assertIsValid(anEmailStringRepresentation);
}

断言是否有效,应验证它实际上是电子邮件字符串表示形式。这是只有一个@字符,它的本地部分是有效的,然后它的域部分是有效的。您可以查看维基百科的电子邮件地址,以更好地了解它的组成方式。永远记住,编程是一个学习的过程,只要我们越来越了解一个领域,我们就会在代码中反映该领域,并且它必须始终与现实世界保持一致!我们的电子邮件类应该或多或少看起来像:

class Email {
    String value;
    Email(aString) {
        value = aString;
 }
 public String getLocalPart()
 public String getDomainPart()
 public String asString()
 public boolean equals(anObject)
 public static Email fromString(aString)
}

就是这样。这种情况与电话号码相同。它也是一个不可变的对象,您应该使用自己的协议创建一个类。请记住,如果我们在做 DDD,永远不要使用你出现的设置/获取。我认为您不需要电话和移动两个值对象,因为它们是多态对象,您可以使用电话号码抽象对移动电话号码或家庭电话号码进行建模。这就像为信用卡建模一样。最后,您最终会明白信用卡类就足够了,并且比拥有多个类别(例如Visa,万事达卡等)更好的设计。让我们跳过 Address 类,现在让我们回到您的问题。到目前为止,我们已经正确识别并创建了我们需要的所有值对象。现在我们需要创建一个抽象来表示电子邮件、电话号码、地址作为联系方式,如果我们忠于域语言,我们可以说:

ContactMethod.for(Email.fromString("monica@gmail.com"));

ContactMethod.for(PhoneNumber("34234234234"));

所以我们的联系方式看起来像:

class ContactMethod {
 static EMAIL = 1;
 static PHONE_TYPE = 2;
 static ADDRESS_TYPE = 3;
 String type;
 String value;
 ContactMethod(int aType, String aValue) {
     type = aType;
     value = aValue;
 }
 String getType()
 String getValue()
 public static ContactMethod at(Email anEmail) {
     return new ContactMethod(EMAIL, anEmail.asString());
 }
 public static ContactMethod at(PhoneNumber aPhoneNumber) {
     return new ContactMethod(PHONE_TYPE, aPhoneNumber.asString());
 }
 public static ContactMethod at(Address anAddress) {
     return new ContactMethod(ADDRESS_TYPE, anAddress.asString());
 }
}

看到 ContactMethod 也是一个不可变的类,实际上经验法则是聚合根应该理想地只有值对象的聚合。最后,这就是您的 Person 类的样子:

class Person {
    List<ContactMethod> contactMethods;
    contactedAt(Email anEmail) {
        contactMethods.add(ContactMethod.at(anEmail));
    }
    contactedAt(PhoneNumber aPhoneNumber) {
        contactMethods.add(ContactMethod.at(aPhoneNumber));
    }
    contactedAt(Address anAddress) {
        contactMethods.add(ContactMethod.at(anAddress));
    }
}

在我学习DDD的旅程中,有时我会看到模式而不是问题......一个有趣的例子 一切似乎都是聚合根是我提供的关于菜单的另一个答案,菜单有不同的类别,如开胃菜、主菜、沙漠等。

我已将其隐式建模为类别字符串。在我发布之后,有第二个答案,有人建议将这些建模为明确的列表:

Menu {
List<Food> starters;
List<Food> entrees;
List<Food> desserts;
List<Food> drinks;
}

通过这种方式,删除了食物类别的整个概念,这对我来说很有启发性,并看到了一种不同的建模方式,在这种情况下降低了复杂性。

我的观点是尝试对代码进行建模,以便如果我与业务专家(不是开发人员)坐下来向他们展示高层次的用例代码person.SetMobileNumber("078321411", Countries.UK)他们将能够理解它:

public void HandleUpdateMobileCommand(UpdateMobileNumber command)
{
    // repositories, services etc are provided in this handler class constructor
    var user = this.UserRepository.GetById(command.UserId);
    user.SetMobile(command.number, command.country);
    this.UserRepository.Save(user);
    // send an SMS, this would get the number from user.Mobile
    this.SmsService.SendThankYouMessage(user);  
}

或者更好的是,当您更新用户手机时,您可能会触发一个MobileUpdated事件,其他地方的某些代码(这是发送 SMS 消息的专家,没有别的)正在侦听这些事件 - 对我来说,这就是 DDD 将代码分解为专家系统的真正力量。

所以总而言之,我认为你的第二个建议是用PostMobileLandlineEmail进行显式建模是最有意义的。

我不会说这是一个DDD域,因为没有足够的关于你需要的任何复杂逻辑(或多用户竞争条件)的信息,只是要提到不要忘记,如果在这种情况下更有意义,你可能最好编写一个CRUD应用程序。

DDD 中有一个中心思想,即领域建模必须通过与领域专家的讨论来形成。如果你是凭空编造这些类名的,它们很可能不完全匹配你的真实域名。诸如电子邮件或电话之类的琐碎内容应该是正确的,但对于其他人来说,您可能首先希望专家提供反馈。

一般来说,与基元类型相比,使用专用值对象进行语义丰富的建模确实是一个好主意。在 C# 中,这是有代价的,因为所需的样板代码量很大(例如与 F# 不同)。这就是为什么我通常只喜欢在类型具有多个属性或存在特定的构造规则或不变量时才这样做。

你可以

做的一件好事是将你的类型建模为不可变的Value Objects。 所以像这样:

public class Telephone
{
    public string AreaCode { get; set; }
    public string TelephoneNo { get; set; }
}

可能变成:

public class TelephoneNumber
{
    private string areaCode;
    private string subscriberNumber;
    private TelephoneNumber()
    {
    }
    public TelephoneNumber(string areaCode, string subscriberNumber)
    {
        this.AreaCode = areaCode;
        this.SubscriberNumber = subscriberNumber;
    }
    public string AreaCode
    {
        get
        {
            return this.areaCode;
        }
        private set
        {
            if (value == null)
            {
                throw new ArgumentNullException("AreaCode");
            }
            if ((value.Length <= 0) || (value.Length > 5))
            {
                throw new ArgumentOutOfRangeException("AreaCode");
            }
            this.areaCode = value;
        }
    }
    // Etc.
}