2019 年 5 月
第 34 卷,第 5 期
[孜孜不倦的程序员]
裸编码:裸集合
作者 Ted Neward | 2019 年 5 月
欢迎回来,使用 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