领域特定语言DSL

简介

DSL(Domain Specified Language)领域特定语言是描述特定领域问题的语言,在软件开发中,其目的是解决领域专家与软件开发人员之间的沟通问题。领域专家通常不懂得编程,无法判断开发人员写的代码是否符合领域的要求,只能是等到软件编写完成,从软件运行表现出来的功能进行判断,而这时成本已经发生了,几个来回下来,进度超时,成本超支。DSL是针对特定领域的语言,使用的是领域相关的术语,领域专家可以理解,而语言本身基于某种宿主语言,比如C#,是可以编译运行的,所以开发人员也可以理解。所以恰当的DSL可以打通领域专家和开发人员之间的障碍,使软件的业务核心部分开发可靠并有效率。

与伪语言的不同

在需求描述的时候,经常使用各种图示或者伪语言对业务进行描述,伪语言一般是一种类似的结构化语言,这种貌似语言的东西往往是很有害的,因为只是大概描述了过程,很多实现细节被忽略或者隐藏了。由于不是严格的编程语言,无法生成可执行的代码,所以也就无法验证对错。

DSL是在某种宿主语言上的扩展,因此是严格的编程语言,可以通过编译执行进行验证,这是与伪语言最大的不同。

Flunt-API

使用Flunt Api可以使代码的可读性更好,更接近于领域专家。Flunt Api的另一个好处是易于扩展,可以通过扩展为核心业务类增加功能。这里举一个简单的例子进行说明,比如有下面的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
using System.Collections.Generic;

namespace ZL.Stock
{
public class StockAnalyzer
{
public List<StockFirstTrade> StockFirstTrades { get; private set; }
public StockAnalyzer(List<StockFirstTrade> stockFirstTrades)
{
StockFirstTrades = stockFirstTrades;
}
}
}

我们为这个类增加一些方法,可以定义一个类,声明为static:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ZL.Stock
{
public static class StockAnalyzerExt
{
public static StockFirstTrade Detail(this StockAnalyzer analyzer, string stockcode)
{
return analyzer.StockFirstTrades.FirstOrDefault(o => o.TSCode == stockcode);
}
}
}

这个类中声明一个静态方法,这个方法的第一个参数带有this关键字,那么这个方法就会扩展到this参数修饰的类上。可以这样调用这个扩展方法:

1
2
3
var lst = JsonConvert.DeserializeObject<List<StockFirstTrade>>(json);
var analyzer = new StockAnalyzer(lst);
var detail=analyzer.Detail("688001.SH");

使用中文

通常我们编写程序时不主张使用中文作为变量或者方法名称,尽管现代编程语言的编译器很多已经不限于只支持ASCII码,但我们仍然无法确保在某些情况下不出现问题(比如如果将中文命名的方法映射为Web Api接口,不支持中文的客户端可能无法调用这个Api)。然而作为领域特定语言的DSL就不用有这个限制,DSL的主要目的就是沟通,如果必须用英文或者汉语拼音进行编写,效果就会大打折扣。看一下下面的代码,是不是很好理解?

1
2
3
4
5
Console.WriteLine(analyzer.最高涨幅());
Console.WriteLine(analyzer.开盘价与最高价最小差());
Console.WriteLine(analyzer.开盘价与最低价最大差());
Console.WriteLine(analyzer.开盘价就是最高价());
Console.WriteLine(analyzer.开盘价就是最低价());

想象一下如果不使用中文,编写这些代码的效果吧。

简单的例子

我们需要分析科创板股票首日上市时的涨跌幅情况,看一下中签的散户,上市首日以什么价格出售可以获得相对高的收益。我们以现有的科创板股票作为样本,首日上市的数据保存在json文件中,结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"TSCode": "688001.SH",
"TradeDate": "20190722",
"FirstOpen": 55.4,
"FirstHigh": 72.02,
"FirstLow": 39.59,
"FirstClose": 55.5,
"FirstChange": 31.24,
"FirstPreChange": 128.7716,
"SecondOpen": 46.92,
"SecondHigh": 52.65,
"SecondLow": 45.01,
"SecondClose": 48.73,
"SecondChange": -6.77,
"SecondPreChange": -12.1982
}

上面的数据结构对应的C#类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
namespace ZL.Stock
{
public class StockFirstTrade
{
public string TSCode { get; set; }

public string TradeDate { get; set; }

public decimal FirstOpen { get; set; }

public decimal FirstHigh { get; set; }

public decimal FirstLow { get; set; }

public decimal FirstClose { get; set; }

public decimal FirstChange { get; set; }

public decimal FirstPreChange { get; set; }

public decimal SecondOpen { get; set; }

public decimal SecondHigh { get; set; }

public decimal SecondLow { get; set; }

public decimal SecondClose { get; set; }

public decimal SecondChange { get; set; }

public decimal SecondPreChange { get; set; }
}
}

我们使用反序列化的方法读入这些数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;

namespace ZL.Stock
{
public class Utility
{
public static StockAnalyzer CreateAnalyzerFromJson(string json)
{
var lst = JsonConvert.DeserializeObject<List<StockFirstTrade>>(json);
return new StockAnalyzer(lst);
}
}
}

我们创建的分析类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
using System.Collections.Generic;

namespace ZL.Stock
{
public class StockAnalyzer
{
public List<StockFirstTrade> StockFirstTrades { get; private set; }
public StockAnalyzer(List<StockFirstTrade> stockFirstTrades)
{
StockFirstTrades = stockFirstTrades;
}
}
}

我们使用Flunt API创建分析使用的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ZL.Stock
{
public static class StockAnalyzerExt
{
public static StockFirstTrade 详细信息(this StockAnalyzer analyzer, string stockcode)
{
return analyzer.StockFirstTrades.FirstOrDefault(o => o.TSCode == stockcode);
}

public static decimal 最高涨幅(this StockAnalyzer analyzer)
{
return analyzer.StockFirstTrades.Max(o => o.FirstPreChange);
}

public static decimal 最低涨幅(this StockAnalyzer analyzer)
{
return analyzer.StockFirstTrades.Min(o => o.FirstPreChange);
}

public static decimal 开盘价与最高价最小差(this StockAnalyzer analyzer)
{
return analyzer.StockFirstTrades.Min(o => (o.FirstHigh -o.FirstOpen));
}

public static decimal 开盘价就是最高价(this StockAnalyzer analyzer)
{
return analyzer.StockFirstTrades.Count(o => (o.FirstHigh == o.FirstOpen));
}

public static List<StockFirstTrade> 开盘价就是最高价列表(this StockAnalyzer analyzer)
{
return analyzer.StockFirstTrades.Where(o => (o.FirstHigh == o.FirstOpen)).ToList();
}

public static decimal 开盘价就是最低价(this StockAnalyzer analyzer)
{
return analyzer.StockFirstTrades.Count(o => (o.FirstOpen == o.FirstLow ));
}

public static List<StockFirstTrade> 开盘价就是最低价列表(this StockAnalyzer analyzer)
{
return analyzer.StockFirstTrades.Where(o => (o.FirstOpen == o.FirstLow )).ToList();
}


public static decimal 开盘价与最低价最大差(this StockAnalyzer analyzer)
{
return analyzer.StockFirstTrades.Max(o => (o.FirstOpen-o.FirstLow));
}

public static List<StockFirstTrade> 最高价比开盘价高百分之10列表(this StockAnalyzer analyzer)
{
return analyzer.StockFirstTrades.Where(o => (((o.FirstHigh - o.FirstOpen))/o.FirstOpen)>0.1m).ToList();
}

public static List<StockFirstTrade> 最高价比开盘价高百分之几列表(this StockAnalyzer analyzer,decimal pre)
{
return analyzer.StockFirstTrades.Where(o => (((o.FirstHigh - o.FirstOpen)) / o.FirstOpen) > pre).ToList();
}

public static List<StockFirstTrade> 第二天开盘上涨列表(this StockAnalyzer analyzer)
{
return analyzer.StockFirstTrades.Where(o => (o.FirstClose<o.SecondOpen)).ToList();
}

public static List<StockFirstTrade> 第二天上涨列表(this StockAnalyzer analyzer)
{
return analyzer.StockFirstTrades.Where(o => (o.SecondPreChange>0)).ToList();
}
}
}