1

4月份遗漏了一篇,这篇算是补充。

此文并非对设计模式的总结,而是要谈谈一般的编码风格,找设计模式的朋友可以移步了。


什么是模式?我搜到一个简短的解释:模式是指从生产经验和生活经验中经过抽象和升华提炼出来的核心知识体系。

模式是多个生活、生产行为的归纳总结,也就是解决某一类问题的方法论。从信息的角度来理解,模式是对信息减小信息熵的过程和结果。

比如一个 APP 需要组织一个表单,表单里的部分元素如选项列表由服务器提供,假设是一个行业列表,通常可能定义一个列表:

[
    {trade_id: 10, trade_name: "xdfsdf"},
    ...
]

看上去接口提供的这个数据没什么问题。如果此时表单里还有选项列表,选择城市,那你可能很自然想到 {city_id: 25, city_name: "wefel"} 。对于客户端,问题也不大,MVC 模式嘛,我在 Model 里分别指定不就行了。

嗯,你也许猜到我想说什么了,没错,我想用一个可更简单的、可复用的结构来进行描述。如:

[
    {id: 10, name: "xdfsdf"},
    ...
]

甚至:

[
    [10, "xdfsdf"],
    ...
]

这样对此类控件可以更容易的封装使用。按信息熵的观点这种模式对熵的降低最大。

但这种转换您可能嗤之以鼻:『切,这算什么模式?』

我觉得当开始思考模式时,就是在思考事物与事物间的关联,这才是一个程序员(Programmer)该做的事情;如果仅仅是为了写点代码实现某个功能,那该叫编码员(Coder)。

对照模式的解释,算法也是模式;我念书少,一直对算法掌握的比较弱,所以此文也就不班门弄斧了。在这里对仅对常规的 MIS(管理信息系统)的接口数据结构提出一些看法,主要针对数据的 CRUD(增删改查)。


数据模式

假设要为一家餐馆做一个小型 App,需求是构建菜谱,有单品,有套餐。单品的属性有:名称、单价、简介、照片(多个,文件、说明),套餐有:名称、折扣价、简介、餐品(多个)。绘制 ERM 如下:

图片描述

单品和套餐通常可以出现在一个搜索列表里,属性基本一致,故放在同一张表里(模式出现了)。照片能够复用,比如单品可以引用,某个套餐也可以用,甚至系统其他的地方也可以用(这就是模式),那就独立出来好了(没有 Product_id 而是用 Product_has_Picture 的多对多关系)。

下面我们开始设计 REST 接口:

GET    /xxx/products
Response Content: [
    {
        id: "商品 ID",
        name: "名称",
        note: "简介",
        price: "价格, 格式: RMB ###.##",
        is_package: "是否是套餐",
        main_picture: "主要展示照片 URL"
    }
]

GET    /xxx/products/ID
Response Content: {
    id: "Product ID",
    name: "名称",
    note: "简介",
    price: "价格, 格式: RMB ###.##",
    is_package: "是否是套餐",
    pictures: [
        {
            id: "照片 ID",
            name: "名称",
            note: "简介"
        }
    ],
    products: [
        {
            id: "商品 ID",
            name: "名称",
            note: "简介",
            price: "价格, 格式: RMB ###.##",
            main_picture: "主要展示照片 URL",
            is_free: "是否免费附送"
        }
    ]
}

POST   /www/products
PUT    /www/products/ID
Request Content: {
    name: "名称",
    note: "简介",
    price: "价格, 格式: RMB ###.##",
    is_package: "是否是套餐",
    pictures: [
        {
            Picture_id: "照片 ID",
            is_main: "0或1"
        }
    ]
}

DELETE /www/products/ID

看上去没什么特殊的呀!细心的话也许发现了,GET 拿到的 pictures 列表与 POST、PUT 发送的 pictures 列表结构不一致,这是根据我的框架能识别的模式所做的精简,事实上 pictures 的完整结构为:

Product_has_Picture: [
    {
        Product_id: "商品ID",
        Picture_id: "照片ID",
        is_main: "是否是主照片",
        Picture: {
            id: "照片ID",
            name: "照片名称",
            note: "照片说明"
        }
    }
]

在进行商品的创建和修改时,由于照片是从照片列表(接口)选择的,故 Picture 层是不需要的。如果用类似 Protobuf 的方式描述 Product 的数据结构,应该是:

message Picture {
    required int323 id = 1;
    required string name = 2;
    required string note = 3;
}

message Product {
    required int32 id = 1;
    required string name = 2;
    optional string note = 3;
    required float price = 4;
    optional bool is_package = 5;

    message Pictures {
        required Picture picture = 1;
        optional bool is_main = 2 [default = 0];
    }
    repeated Pictures pictures = 6;

    message Products {
        required Product product = 1;
        optional bool is_main = 2 [default = 0];
    }
    repeated Products products = 7;
}

可以看出 Pictures 内是单个的 Picture 对象,重复的是 Pictures 对象;如果仅仅按数据结构越简单越好的模式来评判,假设 Pictures 列表不需要 is_main 属性,按第一个图即为默认图的方式好了,也就是 Product 内直接定义 repeated Picture pictures。那么,PUT Product 时 pictures 可定义为一个由 ID 组成的列表甚至分隔符分隔的字符串。但是,这将增加处理程序的复杂性,程序并不能轻松的自动处理,或者要将多对多关联分解成纯粹的和有其他数据的两种模式。

这里涉及到另一个模式,我们的数据关联方式到底有多少种,多数的 ORM 框架会给你这样几种:BLONGS_TO HAS_ONE HAS_MANY MANY_TO_MANY,但当从 ERM 转为类图的时候,你会发现其实只有一种关系即可完整覆盖这 4 种关系,那就是 BELONGS_TO,在类图里叫 Dependency (个人觉得对 UML 的 Association Aggregation 等其他名词没必要太较劲)

为了显式的申明当前关联查询的表,确定关联方向,保留 HAS_ONE、HAS_MANY 便于理解(其实是我嫌麻烦,Django 就做到了只要申明了 ForeignKey 就可以正、反双向的关联查询),由此可得出 A MANY_TO_MANY C 可以分解为 A HAS_MANY B (by A_ID) BELONGS_TO C (by C_ID)

当理清楚了这个结构模式时,就可以编写程序『自动』处理请求了,比如你给了 products 数据 {product_id: 31, is_main: 1},就会去写入 Product_has_Picture 的关联数据, 外键 package_id 可以自动代入。查询商品列表时给了 products.product_id 就会提取含有某个(些)商品的套餐。

以此看来,数据本身就成了『查询语句』和『业务逻辑』。


动作模式

说完了结构再说说行为。对于常规的 CRUD,我认为从视觉操作上可以分解为两个组件:列表、表单;列表对应的接口有:获取列表、更新;表单对应的接口有:获取信息、保存(增加、修改)。

获取列表接口可涵盖:分页、排序、搜索(筛选)等;
列表的更新有:删除、更新状态(批量更新某个字段的值);

在多年以前 WEB 开发中页面与页面(组件)间的联动是靠显式的告知回跳地址或用 Referer 来回到来源处,比如连贯的行为:打开列表->添加商品->添加完成->跳回列表。

而独立的组件模式,组件与组件之间的联动是采用事件来驱动,A组件完成后广播自己完成的事件,哪个组件需要处理哪个组件监听该事件就好了,简单总结为:谁打开、谁负责。比如现有连贯操作:打开 A 列表->添加 A->进入 A 表单->选择 B->打开 B 列表->搜索 B->结果里没有->添加 B->打开 B 表单->保存 B->返回 A 表单->自动选中刚添加的 B->保存 A->刷新 A 列表。

这看上去好像很麻烦,用独立组件的方式就很好理解了:

  1. A 列表打开 B 选择列表,此时监听 B 的选择完成事件
  2. B 选择列表打开 B 表单,此时监听 B 的保存完成事件
  3. 当 B 列表监听到打开的 B 表单保存完成时,触发 B 选择完成事件
  4. 当 A 列表监听到打开的 B 列表选择完成时,将选中数据加入选项并选中

这种方式在 App 和 Web 的 Rich Client 开发中其实很常见。此处表单的选择是一个封装了打开、监听、选中这些行为的控件;每个组件、控件都仅仅关心自己打开的组件即可,并不需要关注完整的行为;其实平时工作中的人何尝不是如此,问题在于我们总是把问题搞得很复杂,而觉得花时间去思考它们的共性是在浪费时间

图片描述
创建一个订单,挑选几个商品。

图片描述
我要的商品不存在,那就建一个好了。纯举例,哪有餐馆没菜顾客自己动手的道理


利用模式,可以把一系列动作、判断用一个静态的描述来定义,比如一组表单验证,一个表关联关系描述。

而在行为模式上,将关注点从一系列动作缩小到两者之间关系的描述上。对程序复用和方便人操作识别都有价值。

模式是普遍存在的,从 HTTP 到 SQL。不是我们缺少模式,而是太多的精力和时间耗费在了模式的转换上,而忽略了模式与模式间转换的模式的思考。

模式就是要建立起一个足够『傻』的描述,如同《阿甘正传》里甘去见珍妮时坐在长凳上对路人说:

Mama always said "There's an awful lot you can tell about a person by their shoes." Where they're going. Where they've been.(妈妈常说看一个人的鞋子你就可以知道他的很多事情。他们要去哪里,去过哪里。)

参考资料

模式 http://www.baike.com/wiki/%E6%A8%A1%E5%BC%8F
信息熵 http://baike.baidu.com/view/401605.htm
《Google Protocol Buffers 入门》 http://shitouer.cn/2013/04/google-protocol-buffers-tutorial/
《UML类图符号 各种关系说明以及举例》 http://www.cnblogs.com/duanxz/archive/2012/06/13/2547801.html
HongsCORE for Javascript https://github.com/ihongs/HongsCORE/tree/develop/hongs-web/web/common


非墨
2.1k 声望44 粉丝

会出错的总会出错。知乎:[链接]


下一篇 »
妥协