2019 年 5 月

第 34 卷,第 5 期

[孜孜不倦的程序员]

裸编码:裸集合

作者 Ted Neward | 2019 年 5 月

Ted Neward欢迎回来,使用 NOF 的开发人员们。上次,我使用了许多属性扩展了 Speaker 域类型,以及关于这些属性的许多注释和约定,向 UI 提供关于如何验证或如何向用户呈现这些属性的提示(更确切地说,方向)。然而,有一件事我没有讨论,那就是给定的域对象如何才能引用多个内容。例如,演讲者通常会进行多个演讲,或者可以在一个或多个主题中表达自己的专业知识。NOF 将这些称为“集合”,围绕它们的工作方式涉及一些规则,与前面的对话稍有不同。

让我们来看看为演讲者提供一些演讲和主题,好吗?

裸概念

首先,请放弃所有数组。NOF 不会将数组用于集合属性,而是完全依赖于集合对象 (IEnumerable<T>-derived) 来保留其他类型的零对多。NOF 手册强烈建议这些集合进行强类型化(使用泛型),NOF 不允许多种关联的值类型(如字符串、枚举类型等),因为 NOF 认为如果类型足够“重要”,可以保证关联,那么它应该是一个完整的域类型。

因此,举个例子,如果我想在会议系统中捕获主题的概念(如“C#”、“Java”或“分布式系统”),在该会议系统中,通过其他编程方法可能可以摆脱使用简单的“字符串列表”作为属性类型,NOF 坚持认为主题是一个完整的域对象类型(也就是说,具有属性的公共类),包括自己的域规则。但是,主题列表可能是固定集,这是合理的,所以我将在数据库中植入我的会议想要考虑的完整主题集。

同样,尽管演讲可能只是一个标题,但它实际上是一系列的内容:标题、说明、主题(它属于或引用的主题),并由一个(或多个)演讲者提供。显然,我面前还有一个小型域模型。

裸集合

在许多方面,最简单的方法是从演讲和主题本身的域类开始,它们(或演讲者)之间没有任何联系。到目前为止,我在这里为每一种方法编写的代码应该都非常简单明了,如图 1 所示。

图 1 演讲和主题的域类

public class Talk
  {
    [NakedObjectsIgnore]
    public virtual int Id { get; set; }
    [Title]
    [StringLength(100, MinimumLength = 1,
       ErrorMessage = "Talks must have an abstract")]
    public virtual string Title { get; set; }
    [StringLength(400, MinimumLength = 1,
       ErrorMessage = "Talks must have an abstract")]
    public virtual string Abstract { get; set; }
  }
  public class TalkRepository
  {
    public IDomainObjectContainer Container { set; protected get; }
    public IQueryable<Talk> AllTopics()
    {
      return Container.Instances<Talk>();
    }
  }
  [Bounded]
  public class Topic
  {
    [NakedObjectsIgnore]
    public virtual int Id { get; set; }
    [Title]
    [StringLength(100, MinimumLength = 1,
       ErrorMessage = "Topics must have a name")]
    public virtual string Name { get; set; }
    [StringLength(400, MinimumLength = 1,
       ErrorMessage = "Topics must have a description")]
    public virtual string Description { get; set; }
  }
  public class TopicRepository
  {
    public IDomainObjectContainer Container { set; protected get; }
    public IQueryable<Topic> AllTopics()
    {
      return Container.Instances<Topic>();
    }
  }

到目前为止,这些内容都非常简单。(显然还有其他内容可以和/或应该添加到这两个类中,但这很好地说明了这一点。) 所使用的一个新属性 [Bounded] 向 NOF 指示,实例的完整(且不可变)的列表可以而且应该保留在客户端的内存中,并作为下拉列表呈现给用户,用户可以从中进行选择。相应地,需要在数据库中建立完整的主题列表,这在 Seed 方法(如本系列前几卷中所讨论的)中“SeedData”项目的 DbInitializer 类中最容易实现,如图 2 所示****。

图 2 创建主题列表

protected override void Seed(ConferenceDbContext context)
{
  this.Context = context;
  Context.Topics.Add(new Topic() { Name = "C#",
    Description = "A classical O-O language on the CLR" });
  Context.Topics.Add(new Topic() { Name = "VB",
    Description = "A classical O-O language on the CLR" });
  Context.Topics.Add(new Topic() { Name = "F#",
    Description = "An O-O/functional hybrid language on the CLR" });
  Context.Topics.Add(new Topic() { Name = "ECMAScript",
    Description = "A dynamic language for browsers and servers" });
  Context.SaveChanges();
  // ...
}

这将提供一个(相当小,但很有用的)主题列表,从中可以使用。顺便说一下,如果你是玩家庭游戏并手动编写代码,请记住将 TalkRepository 添加到主菜单中,方法是将它添加到服务器项目中 Naked-ObjectsRunSettings.cs 中的 MainMenus 方法中。此外,请确保在同一个文件中的服务方法中也列出了这两种存储库类型。

从根本上讲,演讲是由一个演讲者就一个给定的主题进行的。如果演讲由两个演讲者进行,或者如果演讲将跨越多个话题,我将忽略更复杂的方案,以暂时保持简单。因此,第一步,让我们向演讲者添加一些演讲:

private ICollection<Talk> _talks = new List<Talk>();
public virtual ICollection<Talk> Talks
{
  get { return _talks; }
  set { _talks = value; }
}

如果生成并运行项目,将看到“Talk”在 UI 中显示为集合(表),但它将为空。当然,我可以在 SeedData 中添加一些演讲,但是一般情况下,演讲者需要能够将演讲添加到他们的配置文件中。

裸操作

这可以通过向 Speaker 类添加操作来实现:显示 Speaker 对象时,该方法将作为可选择项“奇迹般地”出现在“操作”菜单中。与属性一样,操作是通过反射的魔力发现的,因此需要做的就是在 Speaker 类上创建一个公共方法:

public class Speaker
{
  // ...
  public void SayHello()
  {
  }
}

现在,在生成和运行时,启动 Speaker 后将显示“操作”菜单,其中会出现“SayHello”。当前没有执行任何操作;作为起始点,最好将至少一条消息返回用户。在 NOF 世界中,这是通过使用服务来完成的,该服务是一个对象,其目的是提供一些不属于特定域对象的附加功能。在常规“消息返回给用户”的情况下,这由通用服务提供,该服务由 NOF 本身在 IDomainObjectContainer 接口中定义。但是,我需要其中一个实例来执行任何操作,而 NOF 使用依赖项注入来按需提供:在 IDomainObjectContainer 类型的 Speaker 类上声明属性,NOF 将确保每个实例都有一个:

public class Speaker
{
  public TalkRepository TalkRepository { set; protected get; }
  public IDomainObjectContainer Container { set; protected get; }

Container 对象有一个“InformUser”方法,用于将常规消息返回给用户,因此从 SayHello 操作中使用它就像下面这样简单:

public class Speaker
{
  // ...
  public void SayHello()
  {
    Container.InformUser("Hello!");
  }
}

但我开始希望让用户可以在给定演讲者的库中添加演讲;具体而言,我需要捕获演讲的标题,摘要(或说明,因为“摘要”是 C# 中的保留词),以及此演讲所属的主题。调用此方法“EnterNewTalk”,然后,我有以下实现:

public void EnterNewTalk(string title, string description, Topic topic)
{
  var talk = Container.NewTransientInstance<Talk>();
  talk.Title = title;
  talk.Abstract = description;
  talk.Speaker = this;
  Container.Persist<Talk>(ref talk);
  _talks.Add(talk);
}

这里要进行好几项操作,因此让我们一一进行分析。首先,我使用 IDomainObjectContainer 创建演讲的临时(非持久化)实例。这是必需的,因为 NOF 需要能够将“挂钩”注入到每个域对象中才能施展其魔力。(这就是所有属性必须是虚拟的原因,例如,以便 NOF 可以管理 UI 到对象的同步。) 然后,设置演讲的属性,并再次使用该容器永久保存演讲;如果不这么做,则演讲不是永久对象,并且在我将演讲添加到演讲者的演讲列表时将不会存储。

但是,可以询问用户如何将此信息指定到 EnterNewTalk 方法本身。反射的奇迹再一次发挥作用:NOF 从方法参数中挖掘参数名称和类型,并构造一个对话框来捕获这些项,包括主题本身。还记得主题是用“受限”批注的吗?这指示 NOF 将对话框中的主题列表生成为下拉列表,从而使从列表中选择主题变得异常简单。(在这一点上应该很容易推断,当我向系统添加主题时,它们都将被添加到此下拉列表中,而无需任何额外的操作。)

现在,有理由认为创建演讲应受 TalkRepository 支持,而不是在 Speaker 类本身上进行,并且,如图 3 所示,它是一个简单的重构。

图 3 支持使用 TalkRepository 创建演讲

public class Speaker
  {
    public TalkRepository TalkRepository { set; protected get; }
    public IDomainObjectContainer Container { set; protected get; }
    public void EnterNewTalk(string title, string description, Topic topic)
    {
      var talk = TalkRepository.CreateTalk(this, title, description, topic);
      _talks.Add(talk);
    }
  }
  public class TalkRepository
  {
    public IDomainObjectContainer Container { set; protected get; }
    public Talk CreateTalk(Speaker speaker, string title, string description,
                Topic topic)
    {
      var talk = Container.NewTransientInstance<Talk>();
      talk.Title = title;
      talk.Abstract = description;
      talk.Speaker = speaker;
      Container.Persist<Talk>(ref talk);
      return talk;
    }
  }

更重要的是,通过执行此操作,“Talk”菜单将会自动标有新的菜单项“CreateTalk”,这将再次通过反射的魔力自动创建一个对话框,输入创建演讲所需的数据。当然,演讲者不能只输入,因此 NOF 将使其成为“可放置”字段,这意味着 NOF 将预期 Speaker 对象被拖放到该字段中。(若要在默认的 NOF Gemini 界面中查看这一点,请启动应用,选择演讲者,然后单击“交换窗格”按钮 - 屏底部的双箭头按钮将显示。所选的演讲者将移动至屏幕右侧,将出现主页界面,可以选择“演讲/创建演讲”项。将演讲者的姓名拖动到“创建演讲”对话框中的 Speaker 字段,然后即选中了演讲者。)

了解这里的情况绝对至关重要:我现在有两个不同的 UI 路径(从顶级菜单创建演讲,或者从给定的演讲者创建演讲),允许两个不同的用户导航方案,不费吹灰之力,没有重复。TalkRepository 担心所有与“CRUD”和“演讲”相关的内容,并且演讲者使用该代码,同时将用户交互完全保留在演讲者内部(如果用户希望这样安排的话)。

总结

这不是你祖父的 UI 工具包。在几个代码行中,我有一个可行的接口(并且,考虑到数据是从标准 SQL 后端,即数据存储系统中存储和检索的),至少目前可以由用户直接使用。更重要的是,这些都没有专有格式或语言 - 而是直接的 C#,直接的 SQL Server,UI 本身就是 Angular。还有一些关于 NOF 默认接口的讨论,我将在下一篇文章中介绍,包括身份验证和授权设备。不过,在此期间,祝编码快乐!


Ted Neward 是本部位于西雅图的 Polytechnology 公司的顾问、讲师和导师。他写过大量文章,独自撰写并与人合著过十几本书,并在世界各地发表演讲。可通过 ted@tedneward.com 与他联系,也可阅读他的博客 blogs.tedneward.com

衷心感谢以下技术专家对本文的审阅:Richard Pawson


在 MSDN 杂志论坛讨论这篇文章