forecho

把生命浪费在美好的事物上

塑造成功框架的哲学

2015年12月27日

来源:Philosophies that Shaped Successful Frameworks

在过去的十年里我们看到了许多软件框架的出现,像 SpringRuby on Rails 已经是非常成功的框架了,掌握它们就意味着打开多扇就业机会的大门了。然而,对于每一个框架的成功,背后的大多数开发人员都不被人关注。2008年1月1日维基百科 列出了67个 Web 框架。然而今天,超过三分之二的消失在列表中或在三年内没有更新。作为 Yii 框架的创造者,我花了很多时间调查各种框架和理解为什么有些成功,有些失败了。我将描述我发现塑造成功框架的一些哲学。

为什么框架?

建立一个成功的框架,重要的是要了解什么是框架,开发人员为什么需要它们。

Douglas C. Schmidt 等人 认为框架作为一个集成的软件构件(如类、对象和组件)集合,为相关应用程序提供一个可重用的体系结构。根据这一定义, 框架应该是一个已完工的应用骨架组成可重用和可定制的组件。开发人员将扩展并定制一个框架通过提供他们的应用程序和领域特定逻辑来形成一个完整的应用程序。

一个框架典型的特征就是所谓的控制反转(inversion of control)。框架通常扮演着组织主程序的角色和调用应用程序代码。这里是反过来的控制流——它调用我而不是我调用框架。下图说明了框架之间的关系,函数库,和应用程序。注意框架通常提供现成的功能的库,以帮助开发人员构建应用程序更快。

开发人员使用框架最重要的原因是框架如何提高生产力和帮助提高代码质量。例如,现代的框架(例如,django),经常提供代码生成工具或样板帮助立即启动新项目。此外,精心设计的框架内嵌安全保护措施,帮助预防开发人员犯典型的安全漏洞。

企业使用框架,还有一个额外的好处是,它可以应用在整个企业,帮助执行标准。框架提供了记录模式,详细的设计和实现的工具用于在所有应用程序之间提供一个一致的结构。例如,在 Capital One (译者注:薛强所在的公司) 我们开发一个 「Chassis」的框架作为一个集成的基础,统一了许多厂商和顾客公司内部开发应用程序的 API。

当然,并不是所有的开发人员喜欢使用框架。一些一致的抱怨包括陡峭的学习曲线,框架耦合性比较高,性能较低,等等。今天,在这篇文章中我将为你解释现代框架如何的解决这些问题,让大多数的这些抱怨不再适用。

哲学

像任何一个产品一样,一个框架的成功取决于许多因素,包括其背后的思想,代码质量、文档,周围社区,营销,支持,等等。在我看来,特别重要的一项是考虑当一个框架被设计和开发的哲学。

好久以前 Python 开发者 Tim Peters 开发 Python 时发表了被称为 Python 之禅 的二十格言设计原则。『优美胜于丑陋,明了胜于晦涩,简洁胜于复杂……』他们鼓舞了许多类似的编程语言之禅(similar programming language zens),我发现这些格言是适用于框架设计的。根据我的框架开发经验,我特此冷凝和总结我认为任何成功的框架最重要的哲学。

  • 越简单越好
  • 整体设计是最糟糕的
  • 一致性
  • 明了胜于晦涩
  • 约定大于配置

越简单越好

让开发人员转换一个新的框架从来都不是一件容易的事。然而,当开发人员选用框架,会作为重点依靠它来投资当前和未来的项目。此外,不想使用类库 - 开发人员可以学习一个 API 实现它 - 学习框架要求开发人员在投入实际使用之前要充分理解框架规则。因此,重要的是要确保简单的设计一个框架,使它更容易、有趣,而且容易去学习,接受和利用。

为了实现简单,一个框架应该强制执行一定数量的限制规则;同时,这些规则应以统一的方式设计和有良好的文档记录。框架执行更多的规则,陡峭的学习曲线,让开发人员很难接受。当规则是一致的,开发人员可以更快学习它们。没有文档,一个框架是无用的。因为没有人会花时间反向工程其规则。

Express.js framework 框架路由语法规则的设计是一个很好的例子,一个非常受欢迎的 web 应用服务器框架。而路由在 web 应用程序中是一个重要的概念,是确定应用程序如何响应客户端请求一个特定的端点(一个HTTP方法和一个URI)。Express.js 介绍一个简单的规则来定义一个路线,app.METHOD(PATH, HANDLER)METHOD 是一个 HTTP 请求方法(例如 GET、POST),PATH 是服务器上的一个 URI 路径,HANDLER 是回调函数路线相匹配时要执行的。下面的代码片段显示了 Express.js 路由代码的样子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var express = require('express');
var app = express();

// accept homepage request 
app.get('/', function (req, res) {
  res.send('Hello World!');
});

// accept POST request at /user 
app.post('/user', function (req, res) {
  res.send('Got a PUT request at /user');
});

// accept DELETE request at /user 
app.delete('/user', function (req, res) {
  res.send('Got a DELETE request at /user');
});

上面的代码是不言自明的,因为它像是如何去看一个 HTTP 请求。因此,开发人员只需要很少的努力去学习就记住这个路由语法并且把它的实用性应用到自己的项目中。

整体设计是最糟糕的

这里的术语「整体」指的是以一个以紧密耦合的代码库为基础构建的框架。web 框架刚开始流行时,他们往往是一个整体,因为他们的主要目标是提供全方位的快速的 web 应用程序开发。渐渐地,人们意识到整体框架有很多问题。例如, 即使改变是框架完全无关的一小部分需要重新测试和释放整个,从而导致应用程序的框架要重建。实际上,整体框架的中代码耦合使得它非常难以保持不同版本的向后兼容性。比如专业缓存、日志、数据库,人们变得不那么愿意被绑定到一个单一的整体框架。

现代框架往往是松散耦合的体系结构。全栈框架(例如 Spring )已经演变成由松散耦合的组件可以单独使用或与第三方交换的框架。专门的框架是有明确的契约,以支持更好的互操作性,这使得应用程序不依赖于特定的框架。例如,一个非常受欢迎的 web 路由框架的特点是所谓『Sinatra-type 框架』,如 SinatraExpress.jsMartini。这些框架使用以下中间件管道架构支持请求路由和处理web应用程序。框架本身是非常小的,但开放式体系结构允许他们无限丰富的各种中间件组件。

一致性

一致性意味着一个框架,坚持使用统一的设计,命名约定,代码风格,代码组织等等。一个一致性的框架将降低门槛,因为用户可以学习框架一个方面,并且应用相同的模式,去快速学习其他的结构。一致还可以帮助用户减少框架特征错别字或误用的可能性。

例如,当设计 Yii 框架的 query builder ,我们把一致性作为一个指导标准。查询构建器(query builder)允许您以编程方式创建一个数据库无关的 SQL 语句,避免 SQL 注入攻击。为了帮助用户更容易地记住它的 API ,我们介绍了链式接口和命名后相应的 SQL 关键字的方法。下面的代码片段显示了如何使用 SQL 语句查询构建器设计。

1
2
3
4
5
6
(new Query())
    ->select('id, email')
    ->from('user')
    ->orderBy('last_name, first_name')
    ->limit(10)
    ->all();

上面的代码将生成和执行 MySQL 声明如下:

1
SELECT `id`, `email`FROM `user`ORDER BY `last_name`, `first_name`LIMIT 10

正如您可以看到的,代码读取非常类似于你编写 SQL 语句。查询构建器之间的一致性和 SQL 语法很容易学习查询生成器。

显示大于隐式

关于编写自己的代码显式大于隐式,避免过多的使用 “自动魔法”,有两个原因坚持这种哲学。首先,显示的代码更容易理解和维护。由于代码是自解释的,维护人员可能不是代码的原作者,不需要来回跳转找到实际上执行的代码。其次,显示的代码不容易出错。虽然显示的可能需要编写更多的代码行,它减少了看似简单含蓄确笼罩着重要的代码的情况。

看看下面的两个 ORM (对象关系映射) 在 PHP 的代码。他们都希望实现『订单』数据库记录和『客户』DB 记录之间建立外键引用约束的相同的目标。

1
$order->link('customer', $customer);

1
$order->customer = $customer;

第一个版本是正常的方法调用。第二个版本看起来更酷,因为复杂的数据库连接操作可以通过一个看似简单的任务来完成。然而,这是一种错觉,第二个版本的简单性是由其他地方的的复杂性隐藏掩盖。例如,用户不得不通过某种形式的文档来学习这种特殊的赋值语法,以便在实践中使用它。因为链接操作看起来像一个正常分配时,用户可能会忘记处理由它引起的潜在的异常,从而导致整个程序发生故障。

事实上,Yii 的发展过程中,关于两个版本我们讨论了很多,并最终选定了第一个版本,它已收到投诉很少。

约定大于配置

约定大于配置的概念已经存在好多年了。这个想法是一个框架应该采取坚持的公约,遵守约定同时仍然允许通过配置提高扩展性。决策的目标是减少开发人员需要做的数目,从而实现哲学# 1——简单性。

约定大于配置最早在 Ruby on Rails 框架中开始流行。Rails 提供一个 ActiveRecord 库,用类和数据库中的表之间的映射处理。按照惯例,表名是类名的多元化形式。因此,该类账户将有一个表称为帐户。如果该表不命名这种方式,用户将必须显式配置类名和表名称之间的映射关系。

许多 MVC 框架使用约定大于配置请求路由到特定的代码片断。如下图所示,Sails.js framework 框架使用的约定,其中的 /we/say/hi URL请求将被路由到controllers/we目录下 SayController 控制器类的hi 动作。按照本约定,开发人员不再需要对控制器的行为定义路由规则。但是,如果开发人员想要使用一个不同的路由规则,他们仍然可以通过显式绑定一个路由到一个控制器动作。

约定优于配置有助于减少需要编写的代码量。然而,它会给开发人员需要遵守规则引入了额外的成本。同时,也往往与前面讨论的『显示大于隐式』的哲学相冲突。事实上,虽然早期版本的 Spring 框架使用了似的 Sails.js 路由约定,Spring 现在要求开发者通过注释明确指定映射。因此,当决定是否引入新的规则以支持约定大于配置,应采取明智的判断。

总结

建设一个成功的框架是所有关于功能和简洁性之间的平衡。整个构建框架的过程中,取舍经常需要以坚守,并举例说明,上述哲学加以考虑。

有时候,你可能会遇到其中一个理念是与另一个直接冲突的情况。一致性是比简单更重要?在约定比显性更重要?在这种情况下,请记住,一个框架的最终目标是简化开发人员的工作,并简化代码的编写进程。因此,保持它的简单和直接。如果他们有明确性冲突,因为前者会带来隐藏的复杂性可以牺牲约定。同样,如果坚持一致性可以稍微违反严格会造成额外的并发症。

Posted Dec 15, 2015 by…

Qiang Xue

软件工程师LEAD、技术人员