项目开发回顾

重构目的

优化授权数据的存储方式,避免原先冗长的缓存计算

成果

无需再做定时的缓存刷新,据实施和客户反馈操作授权速度和流程度比原先有大幅提升

槽点和败笔

  1. 从目的出发业务基本无改动,完全搬运原先代码,也因此出现了一些纰漏
  2. 过度设计,ra-backend无意义的透传,controller service方法调用,首先侧重代码的清晰、可读性,再考虑其他规范,避免单文件代码量过大
  3. 缺少自测手段
  4. 前期在基础组件花费时间过长,缺乏进度管控,后期赶工,代码质量缺少保障
  5. // 沟通问题

开发体感

  1. 修改依赖授权数据的项目非常痛苦,要去了解原先的业务场景和数据格式以提供接口,在后期测试时,这块花费了很大时间,出了问题难以排除
  2. 本地开发启动很多服务联调
  3. 线上错误信息,因为bff,无法准确知道调用后端的接口和参数(telepresence,log插件优化)
  4. 前后端联通

工程化问题

解耦

  1. 服务拆分颗粒度 要公司业务发展趋势相契合,不能生搬硬套教程,做适当整合,基础信息类
  2. 避免ra-backend,只做透传
  3. 传入参数的校验,存在将前端传入参数直接透传查询,需要加以验证和鉴权

模块化

  1. 因为docker构建缓存,私有包不自动更新,增加Jenkins配置或者写明使用包版本
  2. 私有包版本管理,不兼容问题

质量保证

  1. 最小单元测试 确保不出现语法错误(await、拼写)
  2. 健康检查,考虑开放http来提供健康检查,通过fx-rpc插件实现再调用rpc来自检

代码风格

  1. 参数传入类型变化 bool contractType
  2. yield [] Promise.all
  3. 下划线转驼峰
  4. 分类id -1
  5. mongo查询条件中有undefined不生效
  6. 单文件代码行数,按controller映射service容易出现代码行数过大 需要做适当拆解

临时发布环境搭建

测试

测试准备

  1. 撤下老的项目,防止再被调用

自测能力

对复杂业务场景的自测能力

沟通

团队会议

  1. 回顾本周进度,指定下周计划,预防延期或及时做进度调整
  2. 对于有争议的问题由tl做决策避免拖延
  3. 确定任务边界,避免扯皮;回顾、调整目标,确保团队整体方向正确(避免无用功,有争议时从目标出发)
  4. 做好会议记录(记录决策)发给与会者

下阶段规划

rpc包更换

  1. 精简rpc包,剔除mq功能
  2. 使用egg agent,支持多worker启动

mq包更换

  1. 封装http版本的包
  2. 避免一个业务动作批量发送mq的场景(类似多段的分布式事务) 需要由业务侧做拆分

log

  1. 错误日志输出适配rpc

减少rpc传输字段

  1. 禁用select *

技巧分享

在node中实现高效缓存
http://wmtcore.com/2020/04/23/%E9%AB%98%E6%95%88%E7%BC%93%E5%AD%98%E7%9A%84%E4%BD%BF%E7%94%A8/

反向授权性能优化
http://wmtcore.com/2020/03/20/%E5%8F%8D%E5%90%91%E6%8E%88%E6%9D%83%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/

使用场景: 对于量级小,常用的基础数据做内存缓存,提高运算速度

getRegionCache方法不仅对外输出,同时内部计算也会使用,为了保证高效,需要隐性异步,不能直接声明async

初始化缓存

返回Promise 同步等待

1
2
3
4
5
6
7
8
9
function getRegionCache(key) {
if (!cache) {
return new Promise(resolve => {
reflashCache().then(() => {
resolve(cache.get(key))
});
});
}
}

调用者拿到的是Promise对象,直接await即可

1
2
3
async getRegionCacheAPI(key) {
return await getRegionCache(key)
}

缓存异步刷新

  • 对于这类基础数据的缓存,低实时性,通过自维护方式失效,类似LRU的实现

缓存到失效时间后通过process.nextTick放入微任务队列异步刷新, 同步计算先拿旧数据继续执行

1
2
3
4
5
6
7
function getRegionCache(key) {
if (overTime) {
process.nextTick(() => reflashCache());
updateCacheTime = Date.now();
}
return cache.get(key);
}

缓存击穿

  • 在缓存初始化和失效时,大量并发请求同时触发缓存刷新,严重影响数据库性能

通过设置once事件监听缓存刷新完成,待缓存刷新结束后,依次返回结果,避免并发刷新缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const event = new events.EventEmitter();
event.setMaxListeners(100); //注意设置监听者数量,超过数量会触发leak memory告警
await reflashCache() {
await new Promise(async resolve => {
event.once('finish', resolve);
if (eventStatus === 'ready') {
eventStatus = 'pending';
}
event.emit('finish');
eventStatus = 'ready';
}
});
}

总结

通过将数据库查询任务放入异步队列,避免了运算时同步等待,体现出node的性能优势

利用事件监听能力,很方便就能处理缓存击穿的问题

不变只是愿望,变化才是永恒

试验性工厂和增大规模

开发使用的环境由于规模、量级的限制,导致第一版开发的软件在真实环境运行时易出现各种问题,需要在测试中不断增大规模、模拟真实环境

把第一版开发的产品发布给顾客,可以获得时间,但是对于用户,使用极度痛苦;对于重新开发的人员,分散了精力;对于产品,影响了声誉,即使最好的再设计也难以挽回名声

唯一不变的就是变化本身

新的系统概念或新技术会不断出现,所以老版的系统必须被抛弃,但即使是最优秀的项目经理,也不能在最开始解决这些问题

  • 开发人员交付的是用户满意程度,而不仅仅是实际的产品
  • 用户的实际需要和用户感觉会随着程序使用而变化

目标上的变化不可避免,而且设计策略和技术上的变化也不可避免,随着学习的过程更改设计

前进两步,后退一步

对于一个广泛使用的程序,其维护总成本通常是开发成本的 40%或更多,该成本受用户数目的严重影响。用户越多,所发现的错误也越多

Resize icon

  • 缺陷修复总会以(20-50)%的机率引入新的bug

原因:

  1. 看似很轻微的错误,实际却是系统级别的问题,如果没有非常详细的文档,是很难被发现
  2. 维护人员常常不是编写代码的开发人员,而是一些初级程序员或者新手

成本: 在每次修复之后,必须重新运行先前所有的测试用例,从而确保系统不会以更隐蔽的方式被破坏

设计实现的人员越少、接口越少,产生的错误也就越少 微服务

前进一步,后退一步

  • 维护越多系统越混乱,所有修改都倾向于破坏系统的架构,增加了系统的混乱程度
  • 用在修复原有设计上瑕疵的工作量越来越少,而早期维护造成的漏洞所引起修复工作越来越多

系统软件开发是减少混乱度(减少熵)的过程,所以它本身是处于亚稳态的。软件维护是提高混乱度(增加熵)的过程,即使是最熟练的软件维护工作,也只是放缓了系统退化到非稳态的进程

机器在变化,配置在变化,用户的需求在变化,所以现实系统不可能永远可用。基于原有系统的重新设计是完全必要的

认识到微服务开发的复杂性

  1. 本地开发要有一个好的开发机器,糟糕的开发机器将会导致糟糕的开发实践
  2. 确保所有服务都使用构建工具,能在一台新机器上构建整个应用程序,而不需要进行太多配置,让开发人员能轻松地在本地运行应用程序的各个部分
  3. 使用Kubernetes,利用Telepresence以便轻松调试 Kubernetes 集群中的应用程序

如果对微服务开发的复杂性缺乏理解,那么团队速度将会随着时间的推移而下降

及时将库和工具更新到最新版

所有服务的依赖项版本保持同步,为依赖升级创建技术债务项,并应作为会议的一部分加以讨论,并定期予以解决,将所有依赖项更新到最新版本

不止包括升级依赖包的版本,还包括架构修正,如当引入一个新工具时,可能可以替换掉原先多个工具,有助于降低复杂性

几年前,许多团队开始将 Spring Cloud Netflix OSS 项目用于微服务。他们使用像 Kubernetes 这样的容器编排工具,但是因为是从 Netflix OSS 开始的,所以他们没有使用 Kubernetes 提供的所有功能。当 Kubernetes 内置了服务发现时,他们仍然使用 Eureka 作为服务发现

避免使用共享服务来做本地开发

举例共享数据库

  1. 一个开发人员可以删除其他开发人员为他们工作编写的数据
  2. 开发人员害怕实验,因为他们的工作会影响其他团队成员
  3. 很难单独测试更改。你的集成测试将变得不可靠,从而进一步降低开发速度
  4. 容易出现不一致和不可预测的状态,开发人员希望在表是空的时候测试边缘情况,但其他开发人员需要一个表来记录
  5. 只有共享数据库拥有系统工作所需的所有数据,团队成员失去了更改的可追溯性
  6. 如果未连接到网络,就很难开展工作

它也可以是消息队列、集中缓存(如 Redis)或任何其他可以发生改变的服务

代码托管平台的可见性

在托管平台按产品、服务对微服务进行分组,方便了解代码库结构

服务有明确定义

避免拆分过多服务,会导致管理成本直线上升,

应用单一责任原则(Single Responsibility Principle)来了解微服务是否变得过大,做的事情是否过多

任何服务都不应该直接与其他服务的数据库通信

服务通信是微服务系统性能低下的首要原因: 如果两条信息相互依赖,那么它们应该属于同一个服务,服务的自然边界应该是其数据的自然边界

明确代码重用策略

对处理相同问题的代码,使用包封装,发布在包管理平台,并通过构建工具来让依赖的微服务及时更新

没有适合的工具和自动化的情况下,使用微服务会导致灾难

多语言编程设计

如果你的开发人员还不够成熟的话,那么无论你使用什么编程语言,你开发的都将是糟糕的产品

一个组织可以指定2、3个语言列表,并列出语言的优势

在选择一门语言前,应该考虑以下一些问题:

  1. 找到成熟的开发人员有多容易
  2. 重新培训开发人员掌握新技术有多容易?我们发现 Java 开发人员可以相对容易地学习 Golang。
  3. 代码的可读、可维护性
  4. 就工具和库的方面而言,生态系统有多成熟?

不仅仅局限于编程语言,也适用于数据库领域, 要始终考虑使用多种技术的维护和操作方面

人员的依赖性

大多数团队专注于他们的特定服务,因此他们并不了解完整的生态系统

确保所有团队都有一个架构团队的代表,使每个团队与整个架构的路线图和目标保持一致(人月神话中有提过,外科手术团队,只需要协调各团队中的架构师)

维护文档

需要维护的文档:

  1. 设计文档
  2. C4 模型中的上下文和容器图
  3. 以架构决策记录的形式跟踪关键架构决策
  4. 开发人员入门指南

在Gitlab中维护所有的文档

功能不要超过平台成熟度

微服务要比传统的单体式应用更为复杂

需要考虑分布式跟踪、可观察性、混沌测试、函数调用与网络调用、服务间通信的安全服务、可调试性等等。这需要在构建正确的平台和工具团队方面付出认真的努力和投资

自动化测试

微服务架构为测试地点和测试方式提供了更多选择

微服务测试的金字塔

Resize icon

交流的缺乏导致了争辩、沮丧和群体猜忌。很快,部落开始分裂——大家选择了孤立,而不是互相争吵

缺少相互沟通会导致进度灾难、功能的不合理和系统缺陷

对程序功能的修改要从系统角度来考虑和衡量,及时做公开,告知

项目工作手册

是项目文档的集合,包括目的、外部规格说明、接口说明、 技术标准、内部说明和管理备忘录,帮助后来者了解产品设计

事先将项目工作手册设计好,能保证文档的结构本身是规范的

控制信息发布:确保信息能到达所有需要它的人的手中,对所有的备忘录编号或者树状的索引结构

记录修订日期记录和标记变更标识条

编程人员仅了 解自己负责的部分,而不是整个系统的开发细节时,工作效率最高,先决条件是 精确和完整地定义所有接口

大型编程项目的组织架构

团队组织的目的是减少不必要交流和合作的数量

减少交流的方法是人力划分和限定职责范围

树状组织架构:

  1. 任务(a mission)
  2. 产品负责人(a producer)
  3. 技术主管和结构师(a technical director or architect)
  4. 进度(a schedule)
  5. 人力的划分(a division of labor)
  6. 各部分之间的接口定义(interface definitions among the parts)

产品负责人:

  1. 主要的工作是与团队外部,向上和水平地沟通,建立团队内部 的沟通和报告方式
  2. 组建团队,划分工作及制订进度表,要求资源,确保进度目标的实现,根据环境的变化调整资源和团队的构架

技术主管:

  1. 提供整个设计的一致性和概念完整性,控制系统的复杂程度
  2. 对设计进行构思,确定内部结构。
  3. 提供技术问题的解决方案,或者根据需要调整系统设计。
  4. 他的沟通交流在团队中是首要的。他的工作几乎完全是技术性的

产品负责人和技术主管的组合方式

产品负责人和技术主管是同一个人:

  1. 只适用于几个人的小团队开发
  2. 同时具有管理技能和技术技能的人很难找到,大型项目中,每个角色都必须全职工作无法保证还能抽空做别的

产品负责人作为总指挥,技术主管充当其左右手:

  1. 实现难度在建立技术决策上的权威
  2. 必须对技术主管的技术才能表现出尊重,支持他的技术决定
  3. 在主要的技术问题出现之前,私下讨论它们,达到在基本的技术理论上具有相似观点
  4. 一些技巧 通过一些微妙状态特征暗示来(如,办公室的大小、地毯、装修、复印机等等)体现技术主管的威信
  5. 这种组合可以使工作很有效 项目经理可以使用并不很擅长管理的技术天才来完成工作

技术主管作为总指挥,产品负责人充当其左右手:

  1. 小型的团队是最好的选择
  2. 参考 外科手术队伍

对于真正大型项目中的一些开发队伍,产品负责人作为管理者是更合适的安排

之前的问题

在做反向计算时,主要涉及到区域数据的比较,通过查出所有子集再循环剔除,

数据量、大运算慢,频繁根据region_start_id查数据库,返回结果因为展开到商品,对反向涉及到的商品都会带上庞大的区域数据

插曲

深拷贝优化: 展开的授权数据,要从原始授权继承区域,需要深拷贝,直接深拷贝Object太慢,通过immutable做了优化,但涉及到的商品区域数据最后返回时要toJS,速度较慢

region缓存: region_start_id查数据库,将查询id数组长为1的做了lru,目的是缓存区域层级高、重复多的数据,在原始授权数据很多时有较大的速度提升

改造

根本问题

计算区域时,无法通过regionID直接知道层级,并做层级运算(会有子父级数据的剔除和补充)

实现方案

受树形数据加编号启发,通过一个层级key来表示regionID的层级信息,直接对key做运算

将原先展开循环的层级运算,变成字符串比较

缓存:

  • cacheRegionKeyObj key对应region记录
  • cacheRegionIdObj regionId对应key
  • cacheRegionKeysRank key对应子集keys,同层级数据groupby加列转行

在做层级运算时,通过字符串运算即可得到上下级key再到缓存里换出子集keys,再对展开结果做排除即可完成层级运算

优势:

  1. 深拷贝只有层级key数组
  2. 层级运算不涉及数据库
  3. 返回结果只有展开的层级key,前端通过缓存cacheRegionKeyObj换算即可

总结

  1. 深拷贝处理可以尝试immutable
  2. 对于层级数据的上下计算,考虑转成平铺的形式,同时保留层级信息,需要将数据缓存在本地计算,适合数据量较小的场景

文档化的规格说明——手册

  • 产品的外部规格说明,它描述和规定了用户所见的每一个细节,也是结构师主要的工作产物

随着系统的使用和反馈,规格说明中难以使用的地方也不断地被修改,对实现人员而言,修改需要阶段化,有进度时间表和版本

实现人员的设计和创造不应该被手册限制,手册要避免描述内部实现

体系结构设计人员必须为自己描述的特性准备一种实现方法,但是他不应该试图支配具体的实现过程。

规格说明的风格

  • 清晰、完整和准确,每条说明都必须重复所有的基本要素,所有文字都要相互一致
  • 由一两个人将结论转换成书面规格说明,保持文字和产品之间的一致性

形式化定义

  • 使用形式化标记方法达到所定义需要的精确程度

优缺点 精确完整,差异得更加明显,可以更快地完成。缺点是不易理解 需要记叙性文字的辅助,才能使内容易于领会和讲授

决不要携带两个时钟出海,带一个或三个 如果同时使用形式化和记叙性定义,则必须以一种作为标准,另一种作为 辅助描述,并照此明确地进行划分

形式化定义仅仅用于外部功能说明它们是什么 有时会通过一段实现该功能的程序来定义,但只是说明功能,不能限定体系结构

设计实现可以作为一种形式化定义的方法

优点所有问题可以通过试验清晰地得到答案,从来不需要争辩和商讨,回答是快捷迅速的
缺点 可能过度地规定了外部功能。有时会给出未在计划中的意外答案;特别容易引起混淆,当实现充当标准时,还必须防止对实现的任何修改

周会和大会

周会

每周半天的会议,所有的结构师,加上硬件和软件实现人员代表和市场计划人员参与,由首席系统结构师主持

在会议之前分发建议、会议内容,解决方案会被传递给结构师并做记录,当决策没有达成共识时由首席结构师来决定

周会优势: 相同小组每周交流,对相关内容比较了解,不需要安排额外时间培训;深刻理解所面对的问题,并且与产品密切相关;正式的书面建议集中了注意力,强制了决策的制订,避免了会议草稿纪要方式的不一致;清晰地授予首席结构师决策的权力,避免了妥协和拖延

大会

随着时间的推移,一些决定、一些小事情并没有被某个参与者真正地 接受。对于这些问题,有时周例会没有重新考虑,慢慢地, 很多小要求、公开问题或者不愉快会堆积起来。通过年度大会解决这些堆积起来的问题

大多数条目的规模很小,每个不同的声音都有机会得到表达。然后会制订出决策,每个人都在倾听、参与,每个人对复杂约束和决策之间的相互关系有了更透彻的理解,使决策更容易被接受

分布式任务调度

原因

  1. 存在一些耗时较长的运算,需要从web服务中移出,比如缓存计算、报表导出
  2. 管控力度:原先的运算集群采用mq通信方式,master对worker的管控力度不够,会出现一个任务重复失败拖垮集群的现象
  3. worker之前不能区分,原因还是没有直接由master派发任务,也无法做跨语言调度

目标

master

  1. 稳定、高可用,不执行具体运算
  2. 接收、记录待执行的任务,按任务优先级派发,记录任务执行结果,支持超时、重试、失败状态
  3. 能管控worker生命,知道它的运行状态和任务进度,根据worker状态派发任务
  4. 向业务方通知运行结果

worker

  1. 启动时向master注册自己的环境和算力
  2. 定时汇报自己的运行状态
  3. 支持多语言

实现

通信

基于grpc的stream方式通信,worker端目前采用重启方式重连,master端可通过end事件清除失效worker

1
2
3
4
5
6
7
service Greeter {
rpc Command (stream body) returns (stream body) {}
}
message body {
string body = 1;
}

master和worker的结构

  • 基于egg,风格结构与后端业务框架基本相同
  • 主要分为job和life_cycle,分管任务和生命周期

Resize icon

master
  • 通过mq记录任务,并定期读取任务到缓存,按优先级排序,
  • 处理worker连接,定期检查连接情况
  • 派发任务,只在新worker连接和任务完成时执行,定期检查任务执行状态
worker
  • 接受master派发的任务调用本地service/method执行,在表中记录执行结果
  • 定期自检健康发送心跳给master

使用

任务代码的编写

  • 在worker端实现
  • 代码位置在service文件夹
1
2
3
4
5
6
// app/service/authorization.js
const Service = require('@fgrid/egg').Service;
class AuthorizationService extends Service {
async deleteAuthorization(){}
}

代码结构与业务框架一致,业务数据通过rpc获取,原则上不直连业务数据库

业务服务调用任务

  • sdk直接写在fgrid-middleware

调用方式和rpc调用类似

1
2
// 通过mifStart来调用分布式任务,后面接serviceName.methodName
this.ctx.mifStart.authorization.deleteAuthorization()

计划实现功能

  1. master高可用
  2. worker多语言版和算力等属性
  3. 定时任务

结构师的交互准则和机制

成本

  • 结构师能在设计早期从开发那得到成本
  • 开发可以增高或降低估的计成本,来反映对设计的好恶

尽早交流和持续的与开发沟通能使结构师有较好的成本意识,以及使开发人员获得对设计的信心,并且不会混淆各自的责任分工

成本过高时的处理
  • 削减设计
  • 跟开发建议成本更低的实现方法(挑战估算的结果)

第二个是结构师固有的主观感性反应,是在向开发人员的做事方式提出挑战

  1. 开发承担创造性和发明性的实现责任,结构师只能建议,而不能支配
  2. 为所指定的说明提供实现方法,并对改方法保持低调和平静,并接受其他任何能达到目标的方法
  3. 准备放弃坚持所作的改进建议

一般开发人员会反对体系结构上的修改建议,通常他是对的——当正在实现产品时, 某些特性的修改会造成意料不到的成本开销

自律——开发第二个系统所带来的后果

  • 第二个系统是设计师们所设计的最危险的系统

开发第一个系统时,结构师倾向于精炼和简洁。知道自己对正在进行的任务不够了解,所以会谨慎仔细地工作,修饰功能和想法被小心谨慎地推迟,导致过分地设计第二个系统

着手第三、第四个系统时,通过之前的经验得到此类系统通用特性的判断,而且系统之间的差异会帮助他识别出经验中不够通用的部分

开发第二个系统的后果(second-system effect)与纯粹的功能修饰和增强明显不同,由于技术、设计的变化,可能会对已经落后技术进行细化、精炼

  1. 关注系统的特殊危险,避免功能上的修饰;根据系统基本理念及目的,舍弃一些功能
  2. 每个小功能分配一个值: 每次改进,有一个数据指标,比如功能 x 不超 过 m 字节的内存和 n 微秒。能作为决策的向导,在物理实现期间充当指南和对所有人的警示.例如facebook的Messenger,为每个功能设置了代码大小预算,要求工程师负责遵守预算约束,作为功能接受标准的一部分(构建一个系统来计算每个功能的二进制大小权重)

项目经理如何避免画蛇添足(second-system effect)拥有两个系统以上经验的结构师的决定。同时,保持对特殊诱惑的警觉,不断提出正确的问题,确保原则上的概念和目标在详细设计中被完整实现

概念一致性

  • 在系统设计中,概念完整性应该是最重要的考虑因素

大多数统体现出的概念差异和不一致性的原因: 并不是因为它由不同的设计师们开发,是由于设计被分成了由若干人完成的若干任务

概念的完整性要求: 系统只反映唯一的设计理念,用户所见的技术说明来自少数人的思想

为了连贯的设计思路,宁可省略一些不规则的特性和改进,也不提倡独立和无法整合的系统,哪怕包含着许多很好的设计

获得概念的完整性

  • 系统的实现目标:功能性,易用性,功能与理解上复杂程度的比值才是系统设计的最终测试标准
  • 简洁和直白来自概念的完整性
  • 易用性需要设计的一致性和概念上的完整性

每个部分必须反映相同的原理、原则和一致的折衷机制。在语法上, 每个部分应使用相同的技巧;在语义上,应具有同样的相似性

贵族专制统治和民主政治

系统的概念完整性

  • 要求设计必须由一个人,或者非常少数互有默契的人员来实现
  • 决定了使用的容易程度

不能与系统基本概念进行整合的想法和特色,最好放到一边不予考虑。如果出现了很多非常重要但不兼容的构想,就应该抛弃原来的设计,对不同基本概念进行合并,在合并后的系统上重新开始

系统的结构师(产品)

  • 用户的代理人,支持用户的真正利益,而不是维护销售人员所鼓吹的利益
  • 易用性很大程度上依赖结构师

结构师工作产物的生命周期比那些实现人员的产物要长,并且结构师一直处在解决用户问题,实现用户利益的核心地位。如果要得到系统概念上的完整性,那么必须控制这些概念

实现人员

  • 产品的成本性能比很大程度上依靠实现人员

在给定体系结构下的设计实现,同样需要创造性、同样新的思路和卓越的才华

纪律和规则

  • 最差的建筑往往是那些预算远远超过起始目标的项目
  • 外部的体系结构规定实际上是增强,而不是限制实现小组的创造性
  • 创造性活动会因为规范化而得到增强

一旦将注意力集中在没有人解决过的问题上,创意就开始奔涌而出。在毫无限制的实现小组中,在进行结构上的决策时,会出现大量的想法和争议,对具体实现的关注反而会比较少

在等待时,实现人员应该做什么

  • 不能让实现队伍来负责产品设计: 进度推迟,质量更加低劣: 概念完整性的缺乏导致系统开发和 修改上要付出更昂贵的代价

工作的垂直划分(工作被划分成体系结构、设计实现和物理实现)从根本上大大减少了劳动量,结果是使交流彻底地简化,概念完整性得到大幅提高