【问题标题】:Modelling contact details for a person / customer为个人/客户建模联系方式
【发布时间】:2014-11-17 15:21:11
【问题描述】:

我想知道是否有更优雅的方式来管理个人的联系方式。暂时忘掉 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。

【问题讨论】:

  • 不确定为每种类型的联系信息设置不同的类会获得什么价值,只需在您的主要 ContactInfo 类上创建属性,如果您愿意,可以将 Address 设为自己的类重新使用它。
  • 是的,我有一个地址对象,它是一个值类型。但是,只看它是如何建模的。例如,我拥有的 Contact 对象我可以使用泛型,其中 T 可以是任何有意义的东西。

标签: c# domain-driven-design software-design


【解决方案1】:

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

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

这和做的一样

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

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

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

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

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,千万不要像你出现的那样使用 set/get。我认为您不需要两个值对象 Telephone 和 Mobile,因为它们是多态对象,您可以使用 TelephoneNumber 抽象来建模手机号码或家庭电话号码。这就像为信用卡建模。最后你会明白,类 CreditCard 就足够了,并且比拥有几个类(如 Visa、MasterCard 等)更好。 让我们跳过 Address 类,让我们现在回到您的问题。 到目前为止,我们已经正确识别并创建了我们需要的所有值对象。现在我们需要创建一个抽象来将电子邮件、电话号码、地址表示为联系方式,如果我们忠于领域语言,我们可以说:

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

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

所以我们的 ContactMethod 看起来像:

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 也是一个不可变类,实际上经验法则是 Aggregate 根在理想情况下应该只有值对象的聚合。 这最终是您的 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));
    }
}

【讨论】:

  • 这是一个很棒的解释,非常详细,而且我同意你的大部分观点。尽管在 Contact 方法对象上传递类型似乎有点令人困惑,并且可能不是自我解释的 :)
  • 你是对的,联系类型可以更好地建模为ContactMethod边界内的枚举
【解决方案2】:

在我学习 DDD 的过程中,有时我看到的是模式而不是问题......一个有趣的例子 Everything seems to be an Aggregate Root 是我提供的另一个关于菜单的答案,它有不同的类别,例如开胃菜、主菜、甜点等。

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

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 应用程序。

【讨论】:

    【解决方案3】:

    DDD 的中心思想是,必须通过与领域专家的讨论来形成领域建模。如果您凭空编造这些类名,它们很可能与您的真实域不完全匹配。诸如电子邮件或电话之类的琐碎问题应该是正确的,但对于其他人,您可能需要先从专家那里获得反馈。

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

    【讨论】:

      【解决方案4】:

      您可以做的一件好事是将您的类型建模为不可变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.
      }
      

      【讨论】:

      • 当然,这就是我的想法。当您想到它时,当有人更改他们的号码时,他们并没有对其进行修改,而是分配了一个全新的号码,就像有人要搬家一样。这就是我的 DDD 出现的地方,因此不仅仅是一个 CRUD 应用程序。
      • 我在代码示例中遗漏了一些东西,我在 setter 中添加了一个私有修饰符,因此在构造后无法更改它。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-07-26
      • 1970-01-01
      • 2021-12-08
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多