2019 年 2 月

第 34 卷,第 2 期

[孜孜不倦的程序员]

裸编码:裸 Speaker

作者 Ted Neward | 2019 年 2 月

Ted Neward在上一篇文章中,我介绍了裸对象框架 (NOF),这是旨在免去所有关于 UI 和对象数据暂留的工作的框架,让开发人员能够完全专注于生成域对象,就像你在学习了 MVC、Web API、HTTP、SQL、数据库连接管理、依赖关系注入和其他所有需要投入大量时间和精力去学习的内容之前,在学校一直被教导的那样。如果它能够起作用,那将是一个很不错的选择,但上一篇文章并未完全让人应接不暇地介绍功能。本文将更深入地探讨可以使用 NOF 在类上定义什么,以及如何将这转换为 UI 和数据库暂留。

裸概念

但在我深入探讨代码之前,先来熟悉一些 NOF 术语,因为这样能够在一定程度上了解创建者和维护者是怎么看世界的。

首要的是,整个体验的中心是域对象,它们是表示系统状态和行为的对象。在 NOF 中,任何“普通旧 C# 对象”都可以是域对象,只要它遵循一些规则,NOF 就能提供它需要的支持:属性必须是虚拟的;必须初始化所有集合成员;且 NOF 不推荐使用构造函数,因为内存中对象的生命周期可能与你平时见到的不同。稍后,我将对此进行详细介绍。

不过,并非所有都很好地符合对象范例。经常有行为是对对象执行的,但并不属于对象。NOF 支持在对象“外部”提供行为(如创建、检索或其他对域对象执行的操作)的服务,而不是任意尝试和强制将行为纳入对象中。服务也可以充当电子邮件服务器等外部系统服务的网关。

从用户角度来看,UI 需要提供用户可以触发的行为,这些行为统称为“操作”,它们通常对应于 UI 中的菜单。可以从各种源发现(或“参与”)操作,但操作通常是通过 Reflection 针对域对象或服务找到的,并显示在 UI 中。必要时,可使用自定义属性来定制大部分此类发现和显示过程,稍后我将对此进行详细探讨。

通过代码,这一切都会变得更清楚。那么,现在就开始,好吗?

裸会议

在 MEAN 系列中,我将一个简单的域示例(演讲者在会议上发言)用作代码生成背景,似乎在这里使用同一示例是合理的(如果原因只不过是进行同类比较的话)。随着域模型生成,情况就变得相当简单:首先,Speaker 包含名字、姓氏、年龄、主题(C#、VB、Java、JavaScript、TypeScript 和 C++)和平均分级(从零到五)。稍后,我可以添加包含标题和说明的 Presentation,只要我知道如何设置对象关系,但目前还是保持简单。

正如我在上一篇文章中所述,我将先使用 NOF 模板 .zip 作为入手的种子,这样便能从 NOF GitHub 对象自述文件 (bit.ly/2PMojiQ) 中的链接下载它,将它解压缩到刚生成的新子目录中,再在 Visual Studio 中打开解决方案。现在,打开 Template.Model 项目节点,并删除 Student.cs 和 ExampleService.cs 文件,毕竟并不想要存储 Student。确实想要存储的是 Speaker(以及之后的 Presentation,但我将保留它,以供稍后重构,只是为了说明 NakedObjects 对重构的大力支持)。

为了清楚起见,我需要执行一些操作,以支持 Speaker 作为裸对象项目中的域类型:我需要创建域类型,然后确保已有可获取和创建它们的 Repository 对象,接下来务必要向裸对象框架提供存储库以从中生成菜单,最后需要(或坦白说,我希望)播种包含一些演讲者的数据库。

Speaker 对象

生成 Speaker 类实际上是四个步骤中最简单的一步。在 Template.Model 项目中,创建 Speaker.cs 文件,并在此文件中的 Template.Model 命名空间内创建 Speaker 公共类。Speaker 暂按名字、姓氏和年龄进行定义,所以为它们创建三个属性,但要将它们标记为“虚拟”;这样做是为了在运行时,NakedObjects 可以(通过子类)置入其他一些行为,以实现所需的神奇效果。

不过,接下来将让 Speaker 变得复杂一点。首先,经常希望数据库中存储的对象有可用作主键的非域相关标识符,而且不允许用户使用或修改此标识符。在 NakedObjects 系统中,我可以使用“NakedObjectsIgnore”自定义属性来标记任何属性,让它不显示在 UI 中,所以接下来就添加使用此属性进行标记的整数“Id”属性(同样是虚拟的)。此外,还经常希望能够创建使用其他数据元素“计算”的属性,所以接下来就添加用于连接名字和姓氏的“FullName”只读属性。因为 NakedObjects 不会负责在 UI 中编辑此属性,所以它无需是虚拟属性。

完成后,应该会生成如下代码:

public class Speaker
{
  [NakedObjectsIgnore]
  public virtual int Id { get; set; }

  public virtual string FirstName { get; set; }
  public virtual string LastName { get; set; }
  public virtual int Age { get; set; }

  [Title]
  public string FullName { get { return FirstName + " " + LastName; } }
}

到目前为止一切顺利。

演讲者存储库

对于不熟悉模式的人来说,Repository 对象实质上是表示对数据库的访问权限的对象,提供了用于标准数据库请求的简单方法调用。在 NakedObjects 系统中,Repository 对象是一种服务形式,它是 NakedObjects 专门识别为对象的对象,虽不是域对象,但可以代表系统提供 UI 元素和行为。在这种情况下,存储库提供一些菜单项,用于从数据库中获取演讲者(例如,按姓氏排序)。

实际上,NakedObject 服务只是另一个类,但它有 Speaker 类没有的一些功能:引用 IDomainObjectContainer 对象,这是 NakedObjects 框架提供的对象,进而提供对系统其余部分的访问权限。NakedObjects 文档中列出了可用的系统服务,但我特别感兴趣的两个元素分别是 NewTransientInstance 方法和 Instances 方法,前者构造新的 Speaker 对象,并针对 NakedObjects 基础结构的其余部分将它关联起来以供使用,后者从数据库中返回完整的 Speaker 对象集。因此,SpeakerRepository 至少如图 1 所示。

图 1:SpeakerRepository 类

public class SpeakerRepository
{
  public IDomainObjectContainer Container { set; protected get; }

  public Speaker CreateNewSpeaker()
  {
    // 'Transient' means 'unsaved' - returned to the user
    // for fields to be filled in and the object saved.
    return Container.NewTransientInstance<Speaker>();
  }

  public IQueryable<Speaker> AllSpeakers()
  {
    return Container.Instances<Speaker>();
  }
}

请注意,NewTransientInstance 和 Instances 方法均为泛型函数,需要将自己处理的对象类型用作函数调用的类型参数。(这是
必需的,因为 DomainObjectContainer 可以且将会用于各种不同的域对象类型,而不仅仅是用于 Speaker。)

不过,只新建 Speaker 并获取所有演讲者列表的存储库并不是特别有用,你经常想要检索已筛选掉特定演讲者的对象,或按特定顺序对演讲者进行排序的对象。在这种情况下,就需要 LINQ 和 NakedObjects 框架中的 EntityFramework 发挥作用了。因为 AllSpeakers 方法返回 IQueryable,所以我可以使用 LINQ 方法对已返回的对象进行筛选和/或排序。这样一来,如果我要按姓氏查找 Speaker,我只需将此方法添加到 Repository 类,并在必要时使用 LINQ 来筛选结果即可:

public class SpeakerRepository
{
  // ... as before

  public IQueryable<Speaker> FindSpeakerByLastName(string name)
  {
    return AllSpeakers().
      Where(c => c.LastName.ToUpper().Contains(name.ToUpper()));
  }
}

对于 LINQ 可以执行的任何操作,你都可以对存储库表示为方法。

演讲者上下文

我需要注意的另外两个统计细节是:我需要设置 NakedObject 暂留引擎将使用的实体框架 DbContext,并需要将 Speaker­Repository 挂钩到 UI,这样就能有用于新建演讲者、查找现有演讲者等的菜单项了。

可以在 Template.DataBase 项目、ExampleDbContext.cs 文件(随时可以根据需要更改文件名)和 ExampleDbContext 类中实现第一个细节。作为相应类上的公共属性,需要将 DbSet 参数化为 Speaker 类型(名为“Speakers”),如下所示:

namespace Template.DataBase
{
  public class ExampleDbContext : DbContext
  {
    public ExampleDbContext(string dbName,
      IDatabaseInitializer<ExampleDbContext> initializer) : base(dbName)
    {
      Database.SetInitializer(initializer);
    }

    public DbSet<Speaker> Speakers { get; set; }
  }
}

随着更多顶级域对象类型添加到系统中,我需要添加更多返回 DbSet 的属性,但现在我只需要这么多。

若要将数据库播种为包含可入手的一些演讲者,需要打开 Template.SeedData 项目,查找 ExampleDbInitializer.cs 文件,并将(此文件中已定义的)Seed 方法编写为通过 Speakers DbSet 向 ExampleDbContext(刚在上一段落中重构的对象)添加一些 Speaker 对象,如图 2**** 所示。

图 2:向 DbContex 添加 Speaker 对象

public class ExampleDbInitializer : 
  DropCreateDatabaseIfModelChanges<ExampleDbContext>
{
  private ExampleDbContext Context;
  protected override void Seed(ExampleDbContext context)
    {
    this.Context = context;

    Context.Speakers.Add(new Speaker() {
      FirstName = "Ted", LastName = "Neward", Age = 47
    });
  }
}

请注意,这是直接的实体框架代码,因此任何有关如何更改数据库的删除/创建策略的进一步讨论都将归入实体框架文档集。

我需要置入代码的最后一个位置是,在 Template.Server 项目中的 NakedObjectsRunSettings.cs 文件内;此代码是对 NakedObjects 系统的注册“挂钩”或“提示”集合,指明了 NakedObjectsFramework 需要了解的内容。例如,如果你决定“Template.Model”不是此会议应用的模型对象的理想命名空间,可以随时更改它,只要此 NakedObjectsRunSettings 的 ModelNamespaces 也反映更改后的名称即可。

关于 SpeakerRepository,我需要将它添加到“Services”类型数组,从而让 Naked­Objects 知道它。如果我希望 SpeakerRepository 方法可以通过菜单选项进行访问,我还需要指示 NakedObjects 检查 SpeakerRepository 是否有用作菜单项的操作。因此,我需要将它添加为从 MainMenus 方法返回的菜单项数组的一部分,如图 3 所示。

图 3:NakedObjects 运行设置

public class NakedObjectsRunSettings
{
  // ...

  private static Type[] Services
  {
    get
    {
      return new Type[] {
        typeof(SpeakerRepository)
      };
    }
  }

  // ...

  public static IMenu[] MainMenus(IMenuFactory factory)
  {
    return new IMenu[] {
      factory.NewMenu<SpeakerRepository>(true, "Menu")
    };
  }
}
}

这些更改完成后,我便可以启动项目、调出客户端,并创建几个演讲者。请注意,显示的是通过 FirstName 和 LastName 生成的 FullName 属性,但不显示 Id。此外,因为 SpeakerRepository 上有 FindSpeakersByLastName 方法,所以我可以按姓氏搜索演讲者;如果返回的结果超过一名演讲者,客户端自动就会知道如何显示演讲者列表,以供单独选择。

总结

请注意,在本文中,我没有对模板进行任何操作。客户端项目才是重点!UI 是通过域对象和服务构造而成,并包含自定义属性(后面的专栏将详细介绍它们)提供的补充信息。同样,暂留是通过框架从类型中发现的结构和元数据生成。

今后需要进一步了解的是,NOF 如何处理更复杂的域方案(如包含演讲者可演讲的主题列表以及可展示的 Presentation 集合的 Speaker),以及如何为它们呈现 UI。在下一篇文章中,我将介绍其中部分主题。不过,在此期间,祝编码快乐!


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


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