jto*_*lle 3 api dsl frameworks
一些最有趣的每日WTF故事以私人语言为主.但是,特定于域的语言可能非常强大,并且似乎越来越受欢迎.当然,如果没有优秀的图书馆,我们就无法编程,但正如谚语所说,"图书馆设计就是语言设计".
没有人喜欢糟糕的API,但只是其中一个程度,或者WTF是一个完全不同的物种?显然这是主观的,所以我把它作为一个社区维基帖.(Stackoverflow联合创始人对一种特定的内部语言是否甚至是WTF有着不同的看法.)
我自己的预感是,这是一种普遍性的尝试,使WTF出现,但我想看看其他人的想法.
(这个问题是通过阅读JaredPar对这个问题的答案的评论引发的:https://stackoverflow.com/questions/901320/anti-joel-test/901361#901361)
(为了澄清一点,"私人语言"一词通常用于负面内涵,而"DSL"或"图书馆"则是中立的.如果有的话,"内部"工具在前往被嘲笑为一种可怕的"私人语言"除了可能使它成为一种坏工具的常见事物之外?这不一定是一种语言;它可能是一个图书馆或框架.)
最终编辑:我接受了罗杰佩特的回答:"实质上是什么?没有." 因为我觉得这个问题实际上是正确的.我想强调Aaronaught关于DSL的答案,因为我觉得它特别好.谢谢.
我不盖很多的DSL,但我有一点与他们的经验,我相信有这种一般的答案,但事实是,每一种情况是不同的.
当DSL不再具体时,它就不再有用了.我相信大多数DSL恐怖故事(AKA"私人语言")都围绕着DSL,它只是试图做太多事情.在某些情况下,他们甚至可能会尝试完全图灵,此时它们不仅仅是功能失调的编程语言.
我将一些现实生活中的例子包括在内; 跳到最后为tl; dr版本.
根据我自己的经验,一个例子是设备之间或PC与外部设备之间的消息系统.如果您想象一个面向对象的API,最终可能会得到如下代码:
public abstract class Message
{
public byte[] GetBytes()
{
using (MemoryStream ms = new MemoryStream())
{
byte[] result = new byte[ms.Length + 3];
result[0] = 0xFF;
result[1] = (byte)ms.Length;
WriteMessageData(result, 2);
result[result.Length - 1] = GetChecksum(result, 0,
result.Length - 2);
return result;
}
}
protected abstract void WriteMessageData(byte[] buffer, int offset);
}
Run Code Online (Sandbox Code Playgroud)
不要太在意这个细节,或者代码有多漂亮(不是).我们的想法是,我们有,我不知道,30种不同类型的消息发送都是完全不同的,但有一些共同的功能,比如内容长度标题和校验.现在我们必须开始构建消息:
public class AddMessage : Message
{
private const byte id = 0x9F;
protected override void WriteMessageData(byte[] buffer, int offset)
{
buffer[offset] = id;
MessageUtil.WriteInt32(buffer, offset + 1, Num1);
MessageUtil.WriteInt32(buffer, offset + 5, Num2);
}
public int Num1 { get; set; }
public int Num2 { get; set; }
}
Run Code Online (Sandbox Code Playgroud)
再一次,不要过于考虑消息的细节.它的作用并不重要.关键是,我们必须为它写一个类.我们必须覆盖一些功能.我们没有编写很多代码,但我们必须编写一些代码.我不了解你,但想到写下这些小小的一次性课程似乎让我心烦意乱.
但我们还没有完成.我们必须创建消息,发送消息并接收结果:
public int Add(int num1, int num2)
{
AddMessage msg = new AddMessage();
msg.Num1 = num1;
msg.Num2 = num2;
MessagingSystem.SendMessage(msg);
AddResultMessage result = MessageSystem.Receive<AddResultMessage>();
if (result == null)
{
throw new InvalidResultException("AddResultMessage");
}
return result.Sum;
}
Run Code Online (Sandbox Code Playgroud)
Blah等等,等等.这是一种最好的情况.我们公开了一个方便的小API,但我们必须继续编写这些类和方法来完成它.随着消息数量增加到10,20,50,100,1000 ......它开始变得有点荒谬.
如果不是编写所有这些样板文件,我们可以在某处写下一些"消息定义",这不是很好吗?
Message(Add)
Send: Num1 int, Num2 int
Receive: Sum int
Message(Multiply)
Send: Num1 int, Num2 int
Receive: Product int
Message(Divide)
Send: Divisor int, Dividend int
Receive: Quotient int, Remainder int
Run Code Online (Sandbox Code Playgroud)
好的,当然,您可以在数据文件中定义它并使用一些kludgy代码,其中大多数验证和实际逻辑在运行时发生.但我们真正想要的是将这些数据编译成我们可以实际编写代码,编译应用程序,获得编译时类型安全性和可测试性的东西.我们想直接从上面的代码转到下面的代码而不做任何额外的工作:
MyMessagingSystem ms = new MyMessagingSystem();
int sum = ms.Add(3, 4).Sum;
int product = ms.Multiply(5, 6).Product;
DivideResult = ms.Divide(10, 5); // Contains Quotient and Remainder properties
Run Code Online (Sandbox Code Playgroud)
现在,如果我们挥手一点,忘记DSL是如何编译的(并且它并不是那么困难,我已经完成了),我们已经消除了大约20行繁琐的容易出错的OO代码,而不是3易于理解的DSL代码行.
我参与过这样的项目.有很多消息.完善DSL和代码生成花了一些时间,但一旦完成,它节省了我几个小时 - 没有,几天努力,编写和调试繁琐无用的代码,一遍又一遍地做同样的事情.
那么为什么(在我看来)这是一个"好"的DSL?因为它具体.它完成了一件事:它定义了一系列类似但仍然独立的消息的格式,我希望能够生成强类型的类.
这个DSL的一个关键方面是没有用户定义的逻辑.它定义了整个应用程序的一个非常狭窄的方面,特别是消息中的内容,以及一些发送/接收配对.它没有说明如何编码消息或如何发送消息.它没有说明消息的语义或应该发送什么特定于订单的消息.它没有说明任何给定消息字段的有效值或应该如何处理错误.
当然,所有这些上述"附加特征"都可以在DSL中实现; 但是我们添加的越多,我们就越拿走. 语言变得越复杂,它实际上就越"特定于领域".一个糟糕的DSL(再次,IMO)看起来像这样:
Event: PaymentReceived(Payment)
Validation:
Condition: Amount > 0, "Invalid payment amount"
Condition: Date > Today - 7d, "Cannot backdate > 7 days"
Actions:
Update: Account(AccountID)
SetProperty: Account.Balance, Account.Balance - Payment.Amount
SetProperty: Account.LastPaymentDate, Payment.Date
Notify: Billing
Template: PaymentReceived.xlt
Field: CustName, CustomerName
Field: PaymentAmount, Amount
Field: PaymentDate, Date
Run Code Online (Sandbox Code Playgroud)
依此类推,我不打算这么做.这看起来很简单,也很诱人.嘿,看看改变验证是多么容易!
但这很容易吗?是真的吗?明天,一些经理确定有些客户从未在银行有钱; 他们的支票总是反弹,我们想要拒绝他们付款.容易,只需添加一个标志,对吧?但是我们如何添加这种类型的验证呢?我们必须查找有关客户的一些信息,并且就目前而言,Validation语法只能用于处理付款本身的验证.所以我们必须对DSL进行某种虚假的更新以适应它:
Event: PaymentReceived(Payment)
Validation:
Condition: All(
PaymentType = Cheque,
Account(Payment.AccountID).DelinquentFlag = False
), "Cheques no longer allowed for this customer"
Run Code Online (Sandbox Code Playgroud)
可爱,虽然有些人以前经历过这个可能已经开始变得那种"呃哦......"的感觉.第二天,经理说:嘿,验证工作得很好,但我们也希望收到通知.
好吧,我们并没有真正在DSL中构建条件通知,但我想我们可以添加它们:
Notify: Management
Condition: All(
PaymentType = Cheque,
Account(Payment.AccountID).DelinquentFlag = False
)
Template: DelinquentCheque.xlt
Field: CustName, CustomerName,
...
Run Code Online (Sandbox Code Playgroud)
这里发生了什么?这种"简单"的条件和行动开始变得非常难看.不仅如此,我们现在正在复制和粘贴.我们正试图在从未设计过处理它们的区域处理这些复杂的条件,并且DSL确实无法重复使用.
但这不是整个故事.这里真正的问题是什么?
真正的问题是这个DSL正在描述一个复杂的过程.它不像一个属性集合那样读取,它读起来就像一组指令,我们已经有了编写通用指令的工具,它被称为编程语言.我会离开这个细节作为练习给读者,但它应该在这一点上是非常明显的,经过几个"版本"我们上面的规格,它可能会更容易,只是在一个正常的,改写一般- 用途语言.
另一个现实的问题是,这DSL似乎是由非技术用户,而不是程序员被用于消费,但它最终会变得过于复杂,任何人,但程序员维护.流程并不简单.这就是为什么人们雇用我们来分析和编码它们并解决所有一点点不一致的问题.从我所看到和阅读的内容来看,非技术用户使用的DSL通常最终不仅没有被所述用户使用,而且对于程序员来说也很难维护,因为它们对于这些用户来说不够复杂.程序员需要做的事情.
当然,上面这个例子在技术上是一个"领域特定的语言",但它并没有增加任何价值,只有拥有一个记录良好的域模型和API.它将各种不同的概念混合在一起,大大违反了凝聚力的原则.每次我们需要添加新功能时,我们都需要开始使用DSL语法,而不是只添加几行代码.这真的让我们的生活更加艰难,而不是更容易."通用业务流程语言"似乎注定要发展壮大,直到它成为Turbo Pascal 1.0的模仿.
那么,什么使领域特定语言成为"WTF?" 根据我的经验,它是:
不是真正的特定领域.该设计似乎采用了非常自由的"域","特定"或两者的定义.
针对最终用户而非开发人员. 将DSL视为"前端"API非常诱人,许多DSL教程甚至似乎暗示这是一个合适的用例.也许是,但如果是这样,我个人没有目睹过它.
定义一个抽象的过程.当潜在条件和行为是严格定义的超集的一部分时,DSL仅适用于流程定义.大多数业务流程都不是这样的; 它们充满了高度复杂的条件和/或顺序逻辑.它们反映了人类的变化无常,冲动的思想和行为,而不是计算机系统的具体规范.
添加编程习语. 如果你发现自己甚至在思考诸如循环,子程序,继承之类的概念,那么现在是时候退后一步,询问DSL真正实现了什么.
哇,那是很多写作.恭喜所有到目前为止的人!
| 归档时间: |
|
| 查看次数: |
266 次 |
| 最近记录: |