关于注解,之前看到了一篇文章,写得真的很不错。
我是没想到,有一天JavaScript也会有注解。尤其是当它“升级”到TypeScript之后,它就逐渐活成了Java的样子。“长大之后,我就成了你”?
至于为什么要有注解,我个人的直观感受是简单,比起那种复杂的配置文件要舒服很多。当然,也不仅仅是简单,这里面有一些深层次的设计哲学,这里面体现了约定优于配置(convention over configuration)的思想。以下摘自维基百科:
Convention over configuration (also known as coding by convention) is a software design paradigm used by software frameworks that attempts to decrease the number of decisions that a developer using the framework is required to make without necessarily losing flexibility. The concept was introduced by David Heinemeier Hansson to describe the philosophy of the Ruby on Rails web framework, but is related to earlier ideas like the concept of “sensible defaults” and the principle of least astonishment in user interface design.
The phrase essentially means a developer only needs to specify unconventional aspects of the application. For example, if there is a class Sales in the model, the corresponding table in the database is called “sales” by default. It is only if one deviates from this convention, such as the table “product sales”, that one needs to write code regarding these names.
When the convention implemented by the tool matches the desired behavior, it behaves as expected without having to write configuration files. Only when the desired behavior deviates from the implemented convention is explicit configuration required.
“约定优于配置(convention over configuration),也称作按约定编程,是一种软件设计范式,旨在减少软件开发人员需做决定的数量,获得简单的好处,而又不失灵活性。本质是说,开发人员仅需规定应用中不符约定的部分。例如,如果模型中有个名为Sale的类,那么数据库中对应的表就会默认命名为sales。只有在偏离这一约定时,例如将该表命名为"products_sold",才需写有关这个名字的配置。”
其实我觉得不如按字面含义翻译成“配置上的约定”,因为它从本质上来说,是为了简化配置,只要遵守一些配置上的约定就可以少写很多东西。当然,约定优于配置和所谓的契约式编程没什么联系。这个也是我一直以来的错误观念,觉得这两者之间总有某些神秘的联系,但其实没有。按照67的说法,契约式编程其实是“契约式设计”,设计过程中规定好前置条件、后置条件和一些不变量,用来保证或者说提高程序的正确性,是一个软件构造的话题。
比如,在早期的Hibernate配置中,是需要集中写XML配置文件的,但是实在是麻烦,尤其在小规模的开发中。当然配置和注解这两种写法各有优劣,不然配置文件也就没有存在的理由了。
从体系结构层面来说,利用配置将相互独立的部件和连接件联系起来,而不是直接指定它们的关系,在一次交互中,参与的部件可以变化;而对于部件来说,也不会固定参与某次交互。这也是Java动态性的一个体现吧,我总感觉反射就是为了这个出现的。
回到Hibernate上来,XML配置集中,相对方便接手的人理解整个系统的结构,而且改过配置之后不需要重新打包,只要重启一下就好了。但是问题很明显,就是复杂,而且代码风格实在是不优雅。至于注解呢,方便快捷,还能增强内聚,但是因为分散到各个地方,不太好维护,有时候需要翻半天,而且因为这些注解的原理是在运行时注入.class
文件,改过之后需要重新打包(当然重启是必须的)。
不过,这里倒是有一篇文章,从另一个角度给出了解释:
自由度这个概念在不同领域有不同的定义,我们借鉴数学中构成一个空间的维数来表达其自由度的做法,在此指的是:解决同一个问题彼此不相关的设计方法学数量。
例如,解决一个比如商品打折的问题,如何设计顺序、提取函数,具体的思路可能有很多,但是这可能都是从面向过程(OP)的角度,同样解决这个问题,如果另一门语言还支持面向对象(OO)的设计方法,那么我们认为后者的自由度要多一些,因为OO提供了几乎完全从另一个角度解决问题的能力。
既然自由度可以借鉴“维数”的定义,我们来尝试分析一下计算机语言的“维数”,在此之前,我们有必要简单分析一下语言是怎样一步一步变得复杂的。
本文关注的重点是命令式风格的计算机语言。
第一步,出现了结构体(数据结构)、常量、变量、算符、顺序、分支、循环等这些体现“命令”的基本方面;
第二步,例程的出现,包括函数、过程等;
第三步,宏的出现,包括宏、模板、泛型;
第四步,对客观世界在结构化上抽象能力出现,包括OO等;
第五步,元编程能力的出现,如注释、反射等等;
……
从计算机语言历史来看,以上步骤不一定按照时间顺序展开,我们更关注的是语言能力提升带来的意义。其中,第二步的完成,标志着结构化程序设计方法的出现,对大型软件工程提供了较好的支持,第三步是对第二步的进一步抽象,第四步所代表的意义更加重大,其中非常重要的一点,意味着终于可以支持实现“层次化”,可以实现将“内核”与“外围”做分离,将相对稳定与潜在变化的部分分开,也就是说,编码所表达的内容不再只能扁平化,终于进化出了“阶级”。
从本质上来讲,以上演进反映了语言自身抽象能力的不断提升。
这里非常有意思的一个现象是,抽象化的不断提升,会使得语言的维度提升至某个分数维——抽象的本质是在空间上提供了某种自相似的递归映射,从而体现出“分形”的结构形式,分形结构表现出在原有空间中增加了分数维,但是得到一个新的整数维是困难的,即比如1维可以提升至1.5维,但是无法到达2维。
所以,目前绝大部分计算机高级语言的维度是1.x。
但是第五步,意味着语言开始真正走向一个更高的维度。
事实上,实现元编程有多种方式,从语言本身来讲,可以分为两类:增强型API与新的语法实现,前者的代表是反射,后者的代表为Annotation。
我们来看一个例子:
public class TestCase{ @Before public void setUp() throws Exception{} @After public void tearDown() throws Exception{} @Test public void add() {} }
上面是Java语言中使用Annotation类型定义了一个单元测试的三个阶段,在这里:
@Before
、@After
、@Test
用“变量”定义了“变量”,同时定义了执行的顺序,这里是“对编码再进行编码”的过程,是元编程的一种典型的实现。我们当然也可以通过增强型API(反射或者用设计约束(比如摸版方法))来解决,但是无论哪一种,都不如Annotation的方式要简单直接明了。
根本的原因,在于增强型API的实现方式与原有代码这两个表达逻辑的维度存在过多的“相关性”,即1.x维的,但Annotation的方式在相关性上大大减少,两个维度更加解耦,所以后者的自由度更高。
这就从语言的“维度”层面解释了注解的必要性。在我的观念里,抽象的提升,意味着内聚的提升。正如引文所述,抽象是一种结构化的映射,抽象的提升是将更多的语义放到一个更小的空间里,毫无疑问提高了内聚。
与此同时,随着抽象的提升,语言所表达的内容的维度降低了。这不是说语言本身的维度降低,事实上这正是语言表达能力提高的一个表现。比如,假如我要在C语言里声明一个二维的字符串数组,我就得char***
,是一个三维的结构;而如果我抽象出了string类(不一定是string类,只是说一个抽象的数据结构),我就可以把问题降成二维;因为这个时候的string只是一个简单的概念罢了,我们的关注点就变成了这个二维的结构本身。当然,自然语言的能力是最强的,因为当我说二维字符数组的时候,对你我来说,这个本身就是一个没有实体的概念,近乎没有维度了。