逸言

GAE的设计

| Comments

说明:以下获得的设计思考,是阅读《GAE编程指南》时的一些简单体会。主要针对的是一些设计细节(非架构)的考量。

简单与灵活。在对API进行设计决策时,需充分考虑API的简单性和全面性。此二者都是站在调用者的角度来思考,即需要提供一种默认的简单调用形式,也可以根据实际情况提供可选的方式。这是简单性和灵活性的共同体现。在Google App Engine中,对数据存储区的实体存储,可以在创建实体对象时指定主键;如果不指定主键,就会生成唯一的ID。

如下Python代码:

obj = Book(title='The Grapes of Wrath',
           author='John Steinbeck',
           copyright_year=1939,
           author_birthdate=datetime.date(1902,2,27))

如果需要生成键名,在调用构造函数时,应加上key_name参数:

obj = Book(key_name='0143039431',
           title='The Grapes of Wrath',
           author='John Steinbeck',
           copyright_year=1939,
           author_birthdate=datetime.date(1902,2,27))

我想,之所以需要考虑这种简单性和灵活性的结合,在于API的特征,这种开放平台会面临不同消费者,在不同场景下做出不同的选择。二者的平衡保证了API的调用能够得到最大程度的需求满足。

封装。在设计时,我们应尽量避免使用基本类型。Martin Fowler在《重构》一书中将其称为“基本类型偏执”。例如在GAE中,就对key进行了合理的封装,包含了实体的类型和值。如果希望批量获取多个实体,可以将key放到列表中,作为get()方法的参数,返回实体对象列表。这种批量获取实体的性能比多次执行单次获取要好,或许是因为它能够减少对数据库IO操作的次数。根据key获得实体对象的python代码如下所示:

from google.appengine.ext import db
k = db.key('Entity', 'alphabeta')
e = db.get(k)

在上面的代码中,db.key()方法相当于key的工厂方法,负责创建key。如果需要根据多个key值获得多个实体对象,则代码为:

entities = db.get([k1,k2,k3])

比较python而言,Java代码略显复杂:

DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
Map<Key, Entity> entities = ds.get(new ArrayList(Arrays.asList(k1,k2,k3)));

对于Java的GAE API而言,似乎还可以提供一个类似KeyCollection的对象,用于封装key列表,并为DatastoreService的get()提供接收KeyCollection对象的重载方法。KeyCollection可以封装一些key列表的行为。但这要根据具体的API设计而定。

GAE为查询还提供了get_by_id()与get_by_key_name()方法。为何提供这两个方法呢?我想,首先因为它们满足最常见的查询要求;同时,这是一种高层次的抽象,无论实体属于什么类型,无论业务有何差异,数据结构有何区别,都需要这两种查询方式。此时,还能够充分体会到封装key的好处,因为这种封装能够在一定程度上消除不同实体(尤其是遗留代码实体)在key之间的差异。

一致性。在GAE中,创建实体的API与更新实体的API完全一致。数据存储区不会区分创建和更新操作。若保存实体时用到完整的键(例如带有类别kind和键名),而数据存储区恰好有这个实体,就会替换原有的实体。为避免更新有效的数据,可以事先测试该实体是否存在,但这种方式在GAE中,需要使用事务API。

Fluence Interface。GAE提供的查询API在Java和Python两种语言下,有一些细微的差别。比较而言,Python采用了Fluence Interface模式,使得它的调用显得更为流畅和简洁。以下是Python代码: Liquid error: invalid byte sequence in US-ASCII

Java的代码就略显冗长而不够流畅,可读性较低。这一方面与语言自身的特色有关,但倘若运用了fluence interface模式,相信能够比较好地提高代码的可读性:

DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
Query q = new Query("Book");
q.addFilter("copyright year", Query.FilterOperator.LESS_THAN_OR_EQUAL, 1950);
q.addSort("title");

PreparedQuery pq = ds.prepare(q);
for (Entity result : pq.asIterable()){
    String title = result.getProperty("title");
    //...
}

在使用PreparedQuery对象访问结果之前,查询其实是没有执行的。

根据我对上面这一段python代码的直观感觉,认为fetch()方法似乎是一种命令模式,之前的Query对象作为命令对象,由内部的Builder对象构建起内部的组成部分,这里是由filter()与order()方法来体现。因为没有阅读源代码,所以只能停留在猜测。我认为在Query类的内部,可能会定义一个Builder方法(或者定义一个内部的Builder类,如果要考虑扩展,甚至是一个Builder接口)。它可能会持有一个QueryStatement对象,作为Collection Parameter传递给Builder。这些Builder会修改QueryStatement对象,使其生成符合命令要求的SQL语句。如果Query是针对数据库的查询,则QueryStatement就可以是一个SqlStatement对象。若有时间,希望能阅读一下源代码,以此印证我的揣测是否正确。或许,有更好的解决方案?

Comments