使用 C# 4.0 的领域特定语言 - 第 3 部分





3.00/5 (7投票s)
解释如何使用 C# 语言构造定义内部 DSL
前几部分
在本文的上一部分中,我们提供了一个面向领域的 API 来创建一个新的考试,但留下了提供一种可读的方式来添加“n”个问题及其相应选项的部分。
级别 2:方法链和流畅 API
创建考试实例后,用户添加问题最可行的方法是什么?首先,让我们看看使用 ExamBuilder
中的 exam()
方法添加问题的当前 API
ExamBuilder.exam(".NET Fundamentals").AddQuestion(question);
我们能通过这种方式添加多少个问题?它对领域友好吗?第一个问题的答案是“只有一个”。第二个问题的答案是“是的,这绝对不是对领域更友好的”。上述问题的直接要求是,如果 AddQuestion()
返回当前考试对象,那么我们可以一次又一次地调用 AddQuestion()
,只要我们需要为这次考试添加问题。我没有修改 "AddQuestion()
",而是在 Exam
类中引入了一个方法 "question()
",如下所示
//Exam.cs
public Exam question(Question question)
{
AddQuestion(question);
return this;
}
我们刚刚为这个考试 DSL 创建了一个流畅的 API "question()
"。流畅意味着“可读性”,通过使用“方法链”。方法链是一种以接力赛的方式调用相同或不同对象中一个或多个方法的技术,从而产生一个面向领域的定义。在 question()
方法中,它只是调用 AddQuestion()
方法,然后返回 Exam
对象的当前实例。
这将允许添加多个问题,例如
//User Mindset: Space1 for defining questions
Question q1 = new Question("Expansion of CLR");
q1.AddOption("A", "Common Language Runtime");
q1.AddOption("B", "Common LINQ Runtime");
q1.AddOption("C", "C# Language Runtime");
q1.AddOption("D", "C Language Runtime");
q1.AddAnswers(new string[] { "A" });
Question q2 = new Question("Expansion of CTS");
q2.AddOption("A", "C# Type System");
q2.AddOption("B", "Common Type System");
q2.AddOption("C", "Compiler Test Symbols");
q2.AddOption("D", "CLR Tolerate Service");
q2.AddAnswers(new string[] { "A" });
//User Mindset: Space2 for adding above questions into Exam object
ExamBuilder.exam(title: ".NET Fundamentals")
.question(q1)
.question(q2);
然而,我们已经触及了蛋糕的一小部分。现在是时候重构 Question
对象的实例化及其 AddOption()
方法了。目前,实例化本身给用户带来了麻烦,而且他会感觉有两个不同的空间,一个用于定义问题,另一个用于将它们添加到 Exam
对象中。
让我们介绍另一种版本的 question()
。
级别 3:集合
//Exam.cs
private static string[] opts = new string[] { "A", "B", "C", "D" };
public Exam question(string description, IList<string> options)
{
Question q = new Question(description);
int i = 0;
foreach (string option in options)
{
q.AddOption(opts[i++], option);
}
return this;
}
</string>
这个方法没有将 Question
对象作为参数,而是将问题描述作为第一个参数,然后将 string
的集合作为选项。在内部,我使用 "opts
" 字段按顺序分配选项键 "A
"、"B
"、"C
"、"D
"。此版本的用法将是
ExamBuilder.exam(title: ".NET Fundamentals")
.question("Expansion of IL",
new List<string> {"Indian Language", "Intermediate Language"}
);
</string>
但是,您肯定不喜欢看到 "new List
params
" 修饰符。
public Exam question(string description, params string[] options)
//Usage
.question("Expansion of IL", "Indian Language", "Intermediate Language")
尽管如此,这仍然不是对领域友好的。用户怎么知道第一个参数之后的所有参数都是选项?在语法上,您无法在四个选项之后阻止用户。
我们应该尝试使用 Func<>
或 Action<>
委托吗?请参阅下面的代码及其相应的用法。
级别 4:委托
//Exam.cs
public Exam question(string desc, Action<question> options)
{
Question q = new Question(desc);
options(q);
AddQuestion(q);
return this;
}
//Usage
.question("Which one of the following is loaded during a new .NET process creation?",
q =>
{
q.AddOption("A", "ILAsm.exe");
q.AddOption("B", "ILDasm.exe");
q.AddOption("C", "MSCorEE.dll");
q.AddOption("D", "MSVCVM.dll");
}
)
</question>
上面的代码看起来对领域非常友好。让我们进一步重构 Question
的 AddOption()
方法,使其更具可读性,方法是引入一个流畅的版本。定义和用法将是
//Solution 1 - inclusive of Exam, Question ctor, methods used
in the usage section below
//Question.cs
public Question a(string option)
{
AddOption("A", option);
return this;
}
public Question b(string option)
{
AddOption("B", option);
return this;
}
public Question c(string option)
{
AddOption("C", option);
return this;
}
public Question d(string option)
{
AddOption("D", option);
return this;
}
//Usage
ExamBuilder.exam(title: ".NET Fundamentals")
.question("Expansion of CLR", q=>
{
q.a("Common Language Runtime")
.b("Common LINQ Runtime")
.c("C# Language Runtime")
.d("C Language Runtime");
})
.question("Expansion of CTS", q =>
{
q.a("C# Type System")
.b("Common Type System")
.c("Compiler Test Symbols")
.d("CLR Tolerate Service");
})
.question("Which one of the following is loaded during a
new .NET process creation?", q =>
{
q.a("ILAsm.exe")
.b("ILDasm.exe")
.c("MSCorEE.dll")
.d("MSVCVM.dll");
}
);
由于每个问题都应该有四个选项,我定义了一系列 "a(), b(), c(), d()
" 方法来添加选项。我在代码中添加了一个注释,说“解决方案 1”表示这将是考试 DSL 的可接受版本之一。
这个解决方案唯一的问题是 "q=>" 后面跟着 "q.a()....
",这会带来一点麻烦。我们可以调整这部分吗?或者任何其他完全替代方案会更具可读性?让我们看看。
待续...