├── LICENSE
├── Lark20210222-162009.png
├── README.md
├── all-in-one.md
├── autonomy
├── README.md
├── criteria-of-modularization
│ ├── avoid-crazy-busy-top-module
│ │ ├── README.md
│ │ └── dependency.drawio.svg
│ ├── common-module-should-be-stable
│ │ ├── README.md
│ │ └── dependency.drawio.svg
│ ├── extending-by-adding-new-module
│ │ ├── README.md
│ │ ├── dependency1.drawio.svg
│ │ ├── dependency2.drawio.svg
│ │ └── mocha.png
│ └── more-focus-on-violatility-than-functionality
│ │ ├── README.md
│ │ └── dependency.drawio.svg
├── loosely-coupled-interface
│ ├── no-return-value
│ │ ├── README.md
│ │ ├── dependency1.drawio.svg
│ │ ├── dependency2.drawio.svg
│ │ ├── dependency3.drawio.svg
│ │ └── dependency4.drawio.svg
│ ├── ui-composition
│ │ ├── README.md
│ │ ├── batch-ui.drawio.svg
│ │ ├── proceed-to-checkout.png
│ │ ├── step1.png
│ │ ├── step2.png
│ │ ├── step3.png
│ │ ├── step4.png
│ │ ├── step5.png
│ │ ├── ui-composition.png
│ │ └── workflow.drawio.svg
│ └── virtual-file-system
│ │ ├── README.md
│ │ ├── dependency1.drawio.svg
│ │ └── dependency2.drawio.svg
├── module-boundary-unchanged
│ ├── alibaba-middle-office-technology
│ │ ├── README.md
│ │ ├── nbf-1.jpg
│ │ ├── nbf-2.jpg
│ │ └── swak.png
│ ├── packaging-deployment
│ │ ├── README.md
│ │ ├── beijing.png
│ │ ├── beijing2.png
│ │ ├── luoyang.png
│ │ └── nanjing.png
│ └── replace-class-with-interface
│ │ ├── README.md
│ │ ├── dependency.drawio.svg
│ │ └── dip.drawio.svg
└── shape.drawio.svg
├── consistency
├── README.md
└── criteria-of-modularization
│ └── non-functional-should-be-consistent
│ └── README.md
├── docs
├── CNAME
├── Modules.md
├── Part1
│ ├── AmazonExample
│ │ ├── README.md
│ │ ├── batch-ui.drawio.svg
│ │ ├── proceed-to-checkout.png
│ │ ├── step1.png
│ │ ├── step2.png
│ │ ├── step3.png
│ │ ├── step4.png
│ │ ├── step5.png
│ │ ├── ui-composition.png
│ │ └── workflow.drawio.svg
│ ├── AutonomyMetrics.md
│ ├── Composition-1.drawio.svg
│ ├── Composition-2.drawio.svg
│ ├── Composition-3.drawio.svg
│ ├── Composition.md
│ ├── Consensus.md
│ ├── Consistency.md
│ ├── ConsistencyMetrics.md
│ ├── DependencyInversion
│ │ ├── DependencyInversion-1.drawio.svg
│ │ ├── DependencyInversion-2.drawio.svg
│ │ └── README.md
│ ├── Encapsulation.drawio.svg
│ ├── EssentialParameter.drawio.svg
│ ├── InformationHiding
│ │ ├── ClassDependency.drawio.svg
│ │ ├── ClassEncapsulation.drawio.svg
│ │ ├── GitDependency.drawio.svg
│ │ ├── Motherboard-2.drawio.svg
│ │ ├── Motherboard.drawio.svg
│ │ ├── Orchestration-2.drawio.svg
│ │ ├── Orchestration.drawio.svg
│ │ ├── README.md
│ │ └── YearsOfProfession.png
│ ├── Integration
│ │ ├── DiscreteProcess
│ │ │ ├── README.md
│ │ │ └── discrete-process.drawio.svg
│ │ ├── DiscreteUI
│ │ │ ├── README.md
│ │ │ ├── client-plugin.drawio.svg
│ │ │ ├── data-orchestration.drawio.svg
│ │ │ ├── order-details.png
│ │ │ ├── order-list.jpeg
│ │ │ ├── repeated-ui-dependency.drawio.svg
│ │ │ ├── repeated-ui-runtime.drawio.svg
│ │ │ ├── server-plugin.drawio.svg
│ │ │ └── ui-orchestration.drawio.svg
│ │ ├── Library
│ │ │ └── README.md
│ │ ├── MixedProcess
│ │ │ ├── README.md
│ │ │ └── mixed-process.drawio.svg
│ │ ├── MixedUI
│ │ │ ├── README.md
│ │ │ ├── cart.jpg
│ │ │ ├── chain.jpeg
│ │ │ ├── client-plugin.drawio.svg
│ │ │ ├── data-orchestration.drawio.svg
│ │ │ └── promotion-slot.png
│ │ ├── ProductFamily
│ │ │ └── README.md
│ │ └── README.md
│ ├── InterfaceStability.drawio.svg
│ ├── README.md
│ ├── Scenario
│ │ ├── AutonomyOptimization
│ │ │ └── README.md
│ │ ├── FeedbackOptimization
│ │ │ └── README.md
│ │ ├── README.md
│ │ └── UserInterface
│ │ │ └── README.md
│ └── VscodeExample
│ │ ├── README.md
│ │ ├── dependency1.drawio.svg
│ │ ├── dependency2.drawio.svg
│ │ └── mocha.png
├── Part2
│ ├── AutonomyFirst.md
│ ├── ControlBoundary.md
│ ├── ControlChange.md
│ ├── FeedbackMetrics.md
│ ├── FunctionBoundary
│ │ └── README.md
│ ├── Isolation.drawio.svg
│ ├── MultiProcess
│ │ └── README.md
│ ├── MultiTenancy
│ │ └── README.md
│ ├── MultiVariant
│ │ └── README.md
│ ├── PluginBoundary
│ │ └── README.md
│ ├── ProcessBoundary
│ │ └── README.md
│ └── README.md
├── Part3
│ ├── Prefab.md
│ ├── README.md
│ └── SignalNoise.md
├── README.md
├── _config.yml
├── _layouts
│ └── default.html
├── favicon.ico
├── favicon.png
└── vue-db
│ ├── CNAME
│ ├── _config.yml
│ ├── demo-counter
│ ├── assets
│ │ ├── index.30c35623.js
│ │ ├── index.63b6af3c.css
│ │ └── vendor.6d71b420.js
│ ├── favicon.ico
│ └── index.html
│ ├── demo-flat-form
│ ├── assets
│ │ ├── index.0e8d894e.js
│ │ ├── index.8aac1eb8.css
│ │ └── vendor.e6e42be5.js
│ ├── favicon.ico
│ └── index.html
│ ├── demo-nested-form
│ ├── assets
│ │ ├── index.d502520f.css
│ │ ├── index.e32eb44c.js
│ │ └── vendor.6ef24320.js
│ ├── favicon.ico
│ └── index.html
│ ├── demo-nested-resource
│ ├── assets
│ │ ├── index.8b16242f.js
│ │ ├── index.a79acb0e.css
│ │ └── vendor.9f5a4ccd.js
│ ├── favicon.ico
│ └── index.html
│ ├── demo-server-side-render
│ ├── assets
│ │ ├── index.06d14ce2.css
│ │ ├── index.bad88eab.js
│ │ └── vendor.dbecdd85.js
│ ├── favicon.ico
│ └── index.html
│ ├── demo-todo-client
│ ├── assets
│ │ ├── index.2dca2adb.css
│ │ ├── index.782b31d0.js
│ │ └── vendor.f0a0d7c7.js
│ ├── favicon.ico
│ └── index.html
│ ├── demo-todo-local
│ ├── assets
│ │ ├── index.2dca2adb.css
│ │ ├── index.a1d44690.js
│ │ └── vendor.f34b366c.js
│ ├── favicon.ico
│ └── index.html
│ └── index.md
├── feedback
└── README.md
├── modularization.md
├── old-README.md
├── patterns
├── README.md
├── event-driven-obstacles
│ └── README.md
├── how-to-add-field
│ └── README.md
├── how-to-add-new-order-type
│ └── README.md
├── how-to-invent-abstraction
│ ├── README.md
│ ├── composite-structured-design.djvu
│ ├── designing-software-for-ease-of-extension-and-contraction.pdf
│ ├── layers.png
│ └── working-with-objects-the-ooram-software-engineering-method.pdf
├── how-to-lower-plugin-tax-rate
│ └── README.md
├── how-to-maintain-readability
│ └── README.md
└── ui-composition-obstacles
│ └── README.md
├── shape.drawio.svg
└── 今日阅读.md
/LICENSE:
--------------------------------------------------------------------------------
1 | Creative Commons Legal Code
2 |
3 | CC0 1.0 Universal
4 |
5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
12 | HEREUNDER.
13 |
14 | Statement of Purpose
15 |
16 | The laws of most jurisdictions throughout the world automatically confer
17 | exclusive Copyright and Related Rights (defined below) upon the creator
18 | and subsequent owner(s) (each and all, an "owner") of an original work of
19 | authorship and/or a database (each, a "Work").
20 |
21 | Certain owners wish to permanently relinquish those rights to a Work for
22 | the purpose of contributing to a commons of creative, cultural and
23 | scientific works ("Commons") that the public can reliably and without fear
24 | of later claims of infringement build upon, modify, incorporate in other
25 | works, reuse and redistribute as freely as possible in any form whatsoever
26 | and for any purposes, including without limitation commercial purposes.
27 | These owners may contribute to the Commons to promote the ideal of a free
28 | culture and the further production of creative, cultural and scientific
29 | works, or to gain reputation or greater distribution for their Work in
30 | part through the use and efforts of others.
31 |
32 | For these and/or other purposes and motivations, and without any
33 | expectation of additional consideration or compensation, the person
34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she
35 | is an owner of Copyright and Related Rights in the Work, voluntarily
36 | elects to apply CC0 to the Work and publicly distribute the Work under its
37 | terms, with knowledge of his or her Copyright and Related Rights in the
38 | Work and the meaning and intended legal effect of CC0 on those rights.
39 |
40 | 1. Copyright and Related Rights. A Work made available under CC0 may be
41 | protected by copyright and related or neighboring rights ("Copyright and
42 | Related Rights"). Copyright and Related Rights include, but are not
43 | limited to, the following:
44 |
45 | i. the right to reproduce, adapt, distribute, perform, display,
46 | communicate, and translate a Work;
47 | ii. moral rights retained by the original author(s) and/or performer(s);
48 | iii. publicity and privacy rights pertaining to a person's image or
49 | likeness depicted in a Work;
50 | iv. rights protecting against unfair competition in regards to a Work,
51 | subject to the limitations in paragraph 4(a), below;
52 | v. rights protecting the extraction, dissemination, use and reuse of data
53 | in a Work;
54 | vi. database rights (such as those arising under Directive 96/9/EC of the
55 | European Parliament and of the Council of 11 March 1996 on the legal
56 | protection of databases, and under any national implementation
57 | thereof, including any amended or successor version of such
58 | directive); and
59 | vii. other similar, equivalent or corresponding rights throughout the
60 | world based on applicable law or treaty, and any national
61 | implementations thereof.
62 |
63 | 2. Waiver. To the greatest extent permitted by, but not in contravention
64 | of, applicable law, Affirmer hereby overtly, fully, permanently,
65 | irrevocably and unconditionally waives, abandons, and surrenders all of
66 | Affirmer's Copyright and Related Rights and associated claims and causes
67 | of action, whether now known or unknown (including existing as well as
68 | future claims and causes of action), in the Work (i) in all territories
69 | worldwide, (ii) for the maximum duration provided by applicable law or
70 | treaty (including future time extensions), (iii) in any current or future
71 | medium and for any number of copies, and (iv) for any purpose whatsoever,
72 | including without limitation commercial, advertising or promotional
73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
74 | member of the public at large and to the detriment of Affirmer's heirs and
75 | successors, fully intending that such Waiver shall not be subject to
76 | revocation, rescission, cancellation, termination, or any other legal or
77 | equitable action to disrupt the quiet enjoyment of the Work by the public
78 | as contemplated by Affirmer's express Statement of Purpose.
79 |
80 | 3. Public License Fallback. Should any part of the Waiver for any reason
81 | be judged legally invalid or ineffective under applicable law, then the
82 | Waiver shall be preserved to the maximum extent permitted taking into
83 | account Affirmer's express Statement of Purpose. In addition, to the
84 | extent the Waiver is so judged Affirmer hereby grants to each affected
85 | person a royalty-free, non transferable, non sublicensable, non exclusive,
86 | irrevocable and unconditional license to exercise Affirmer's Copyright and
87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the
88 | maximum duration provided by applicable law or treaty (including future
89 | time extensions), (iii) in any current or future medium and for any number
90 | of copies, and (iv) for any purpose whatsoever, including without
91 | limitation commercial, advertising or promotional purposes (the
92 | "License"). The License shall be deemed effective as of the date CC0 was
93 | applied by Affirmer to the Work. Should any part of the License for any
94 | reason be judged legally invalid or ineffective under applicable law, such
95 | partial invalidity or ineffectiveness shall not invalidate the remainder
96 | of the License, and in such case Affirmer hereby affirms that he or she
97 | will not (i) exercise any of his or her remaining Copyright and Related
98 | Rights in the Work or (ii) assert any associated claims and causes of
99 | action with respect to the Work, in either case contrary to Affirmer's
100 | express Statement of Purpose.
101 |
102 | 4. Limitations and Disclaimers.
103 |
104 | a. No trademark or patent rights held by Affirmer are waived, abandoned,
105 | surrendered, licensed or otherwise affected by this document.
106 | b. Affirmer offers the Work as-is and makes no representations or
107 | warranties of any kind concerning the Work, express, implied,
108 | statutory or otherwise, including without limitation warranties of
109 | title, merchantability, fitness for a particular purpose, non
110 | infringement, or the absence of latent or other defects, accuracy, or
111 | the present or absence of errors, whether or not discoverable, all to
112 | the greatest extent permissible under applicable law.
113 | c. Affirmer disclaims responsibility for clearing rights of other persons
114 | that may apply to the Work or any use thereof, including without
115 | limitation any person's Copyright and Related Rights in the Work.
116 | Further, Affirmer disclaims responsibility for obtaining any necessary
117 | consents, permissions or other rights required for any use of the
118 | Work.
119 | d. Affirmer understands and acknowledges that Creative Commons is not a
120 | party to this document and has no duty or obligation with respect to
121 | this CC0 or use of the Work.
122 |
--------------------------------------------------------------------------------
/Lark20210222-162009.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/Lark20210222-162009.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [https://autonomy.design](https://autonomy.design)
2 |
3 | # 症状
4 |
5 | 如果没有做好业务逻辑拆分,可能在项目晚期造成以下三种问题:
6 |
7 | * 拆了微服务之后做一个需求要拉很多人,代码写进来了就再也删不掉了
8 | * 要么放任自流,1个App里有4种日期选择方式。要么用力过猛,抽象出来的营销接口动辄几百个参数
9 | * 线上出了问题很难定位到谁引起的,本地做不了任何有意义的测试,反馈周期特别长
10 |
11 | 为何随机选择了这三种问题并归纳为业务逻辑拆分问题呢? 因为我认为以上三种问题都是由同一个不易变化的本质约束所造成。这个本质约束就是人类的感知与沟通速度是很慢的。
12 | 所谓业务架构,其实质就是想尽一切办法减少沟通。只有沟通少,效率才会高,质量才会好。就是这么简单的一件事情。
13 |
14 | # 代码腐化
15 |
16 | 没有哪份代码一开始是不想好好写的。大家在开始落笔之前都知道会出现上述三种症状,并且都自认为做好了设计去避免这些问题。然而往往事情的发展不是如自己所想的那样。我们可能会把代码腐化归咎于自己不够努力,或者需求太多了做得太匆忙。如果当初更用心一点就不会这样。是吗?
17 |
18 | 如果你带了一个新人,他可能会问你“我这个需求的代码应该写在哪个Git仓库里?”。然后你会根据你的直觉做出一个判断。这种高度依赖于某个人“我觉得”的决策模式是长久不了的。只要摆放代码严重依赖于某个人的判断,腐化就是无法阻挡的趋势。那有没有什么办法可以像“防腐剂”那样,在一开始的时候放进去,然后就可以保持很长时间的“新鲜”呢?
19 |
20 | 实现方案也不复杂,代码分成两部分。可以随便乱写的,和不能随便乱写的,这两部分。不知道“组合代替继承”的小朋友,只能在可以随便乱写的那部分里发挥。如果大部分代码量都在“可以随便乱写”的那部分里,那么只要很少的人就可以看住“不可随便乱写”的那部分。
21 |
22 | 说简单也不简单,怎么做到呢?
23 |
24 | # 你说的东西可以落地吗?
25 |
26 | 大部分人的日常工作都是维护一个已有的项目,没有几个人能够参与到 Greenfield 项目的初始设计阶段。这也是大部分读者所懊恼的地方,“我读你的东西有什么用,我这项目就已经烂成了这个样子了,我也改不了”。我希望能够出一些可度量的指标。这样对于现有的项目,我们可以拿这些指标去度量这些问题有多严重。
27 | 当下次别人问你微服务为什么这么拆,而不是那么拆的时候,你可以给出令人信服的理由,而不是“我觉得”。
28 |
29 | 我不是销售像 Angular 那样可以直接 git clone 一份的代码框架给你。我想分享的是一套可以适用于任何语言和框架的分解业务逻辑的套路。从《Clean Architecture》和《Domain Driven Design》你应该已经学到了很多了。如果仍然不会拆,那么可以再读读《业务逻辑拆分模式》试试。
30 |
31 | * [拆分成什么呢?](./docs/Modules.md)
32 | * [Part.1 代码防腐](./docs/Part1/README.md)
33 | * [Part.2 只对自己写的代码负责](./docs/Part2/README.md)
34 | * [Part.3 突出大逻辑,隐藏小细节](./docs/Part3/README.md)
35 |
36 | # RSS 订阅【今日阅读】
37 |
38 | https://github.com/taowen/modularization-examples/commits.atom
39 |
--------------------------------------------------------------------------------
/all-in-one.md:
--------------------------------------------------------------------------------
1 | # 如何不 Review 每一行代码,同时保持代码不被写乱
2 |
3 | 本文的读者
4 |
5 | * 你为代码总是被同事们写乱了而苦恼,但是又无法 Review 每一行代码
6 | * 你要开发一个 SaaS,实现各种复杂功能的组合,但是又不能像互联网公司一样堆很多人来开发微服务
7 | * 你模仿过主流的微服务,DDD 等做法,但并没有达到理想的效果,不介意尝试一些非主流的新办法
8 |
9 | 本文的目标是以尽可能浓缩的篇幅提供可模仿的步骤来达成“如何不 Review 每一行代码,同时保持代码不被写乱”的目标。总共三步
10 |
11 | * 第一步:不要拆分代码仓库,不要拆微服务。Monorepo is all you need,Feature Toggle is all you need。
12 | * 第二步:管控集成类需求的代码审查:主板加插件。
13 | * 第三步:管控规范型需求的代码审查:独家收口。
14 |
15 | # 第一步:不要拆分代码仓库,不要拆微服务
16 |
17 | 拆分微服务以及代码仓库的缺点
18 |
19 | * 利用组织边界来强化代码的分工边界会导致将来调整阻力很大。我们对于代码应该如何组织的认识是随着新需求不断调整的。不要轻易动用“组织架构”这样的核武器来达成小目标。
20 | * 拆分了代码仓库之后不利于在编译期做集成,做集成后的整体验证。即便运行时集成有万般好处,也没有必要丧失掉编译期集成的选项。
21 | * 跨代码仓库的代码阅读,开发时的辅助和检查都会变困难。
22 | * 微服务控制变更风险的灰度边界是固化的,也就是微服务的大小。切得越细,每次变更的东西就越少,风险就越小。这不够灵活。
23 | * 微服务的弹性边界是固化的,如果某种视频编辑需要特别多的内存,我们希望独立伸缩,就得把这部分代码切割出来变成一个独立的微服务。
24 |
25 | 拆分微服务和代码仓库相比单体架构,最重要的目标是减少分支冲突,控制发布变更的风险。但是拆分微服务和代码仓库并不是最佳的解决方案。Monorepo + Feature Toggle 是更好的解决方案。
26 |
27 | * Monorepo:所有的代码都在一个仓库里。这样就不存在不同模块的仓库有不同的版本问题。大家都是统一的一个版本。升级线上系统的过程拆分成:部署+发布,两个步骤。部署的时候,整个 Monorepo 的代码都部署到目标机器上了,但并不代表发布了。
28 | * Feature Toggle:特性开关来精确控制哪些逻辑分支被发布出去。这样部署就和发布解耦了。要灰度多少比例,可以精确控制。要一个特性开关的两个版本的逻辑分支共存,也可以实现。
29 |
30 | 使用 Monorepo + Feature Toggle 可以提供所有拆分微服务达成的目标,同时克服以上微服务拆分带来的缺点
31 |
32 | * 通过目录结构来控制代码所有权。你可以要求这个目录下的代码必须经过你的 Code Review。调整目录结构比调整代码仓库容易得多,比调整组织架构要容易得多。
33 | * 可以保持编译期集成这个选项。
34 | * 可以更容易实现开发时辅助和检查工具,可以很方便地阅读跨模块的代码
35 | * 变更风险更小,不仅仅开关回滚很快,而且开关可以灵活地定向灰度,而且一个开关的控制范围大小也可大可小,粒度非常灵活。
36 | * 弹性边界更灵活,不需要因为要独立扩缩容,就得把代码切分出去
37 |
38 | 经常听说的一个说法是最终是要拆分成微服务,多仓库的。单体应用单仓库只是一个过度形态。这会导致我们认为为啥步一步到位呢。但事实并非如此,微服务和多仓库并不一定适合所有人。你可以用 Monorepo + Feature Toggle 用一辈子。
39 |
40 | 具体如何实践 Monorepo + Feature Toggle 按照 https://www.branchbyabstraction.com/ 和 https://trunkbaseddevelopment.com/ 的指导去做就可以了。
41 |
42 | # 第二步:管控集成类需求的代码审查
43 |
44 | 当我们把代码都放一个代码仓库里之后,立即要面临的问题是代码不会写乱么?你怎么控制什么代码写在哪里?每一行代码写之前都来问你,每一行代码写完了都需要你来 Review 么?
45 |
46 | 所以,我们需要一种强制检查代码写在了正确的位置的自动化机制。这个机制就叫“依赖管理”。对应常见的编程语言
47 |
48 | * 如果是 TypeScript,这个叫 package.json
49 | * 如果是 Golang,这个叫 go.mod
50 | * 如果是 Java,这个叫 POM.xml
51 |
52 | 当我们把代码拆分成多个包(或者叫模块),并使得这些包(模块)形成特定的依赖关系,就可以通过编译器检查控制什么代码必须写在什么地方,从而不需要靠人去检查。这个依赖关系如下图所示
53 |
54 | 
55 |
56 | * 插件:尽可能完整的实现一个独立的功能,比如面向最终用户的完整的页面
57 | * 主板:当插件与插件之间有功能上的集成需要的时候,通过绕路主板来实现,而不能直接在插件和插件之间有引用关系
58 |
59 | 这样做的好处是可以减少 Review 的负担。不需要盯着每一行代码了,只需要重点盯着主板的修改就可以了。实现的步骤是
60 |
61 | * 先决定每个插件里封装什么的数据库表。如果是前端模块,则是封装什么后端的数据接口
62 | * 因为插件不能引用插件,所以对应的页面和功能就会自然选择有这些数据库表的插件里来写。因为写在其他插件里的话就访问不到了
63 | * 然后对于需要来自多个插件数据才能实现的功能,我们通过主板来实现
64 |
65 | 比如说我们决定有一个团购插件,有一张表 GroupPurchaseCampaign 记录了团购活动的参与商品和规则。那么要展示团购活动列表的时候,就会自然有限选择在团购插件里来写,因为这个插件里可以访问这张表。这里说的“访问”是指可以 import GroupPurchaseCampaign 这个类型的意思。插件不能 import 另外另外一个插件定义的类型,但是不意味着运行时不能访问别的插件的数据。运行时的数据都是通的。限制的是编译期,谁可以 import 谁。
66 |
67 | 当需要主板进来实现”集成类需求”的时候,应该如何做。分为以下三类
68 |
69 | * 一个界面需要同时展示来自两个插件的数据。例如商品详情页,需要常规商品数据,需要当前的券活动,需要当前的限时折扣活动等。在主板里把界面分成多个槽,然后不同的槽由不同的插件来实现。
70 | * 一个操作需要多个插件的数据进行综合决策判断。例如计算价格的函数,需要综合商品的原价,需要取得购物车选择券,需要判断是否满减等。在主板里把价格的计算流程里留出槽,然后不同的槽由不同的插件来实现。
71 | * 一个插件的界面里需要展示来自其他插件的数据。例如退款申请界面,需要展示商品图片等。这个不同之处在于整个页面绝大部分都是由一个插件自己实现的,只是在局部的地方需要其他插件的数据。所以就不值得把整个页面都下沉到主板里去写。实现方法是在主板里声明一个ProductCard组件,然后这个组件由常规商品插件实现,再由退货插件来使用用。
72 |
73 | 主板起到的作用和 C 编程里的“头文件”的作用是一样的,就是给模块之间相互调用提供声明。主板的代码要尽可能的少,绝对不要在主板里提供 CRUD 的裸数据接口,主板里定义的是界面的槽,流程的槽,而不是直接把数据库的原始数据暴露出去。
74 |
75 | 技术上如何实现:在一个包里提供声明,再另外一个包里写实现。这个有两类做法:
76 |
77 | * 通过运行时多态来实现。在主板里定义 interface,在插件里写实现 interface 的类的或者函数。然后在启动的时候,做一次 “AutoWire” 的绑定操作。这个绑定最简单的方式可以是对类型为函数指针的全局变量做一下赋值操作。也可以由 Spring 这样的依赖注入框架来做 AutoWire。
78 | * 通过编译期做源代码的复制粘贴。需要在编译之前先对源文件做一下处理,然后再喂给编译器。
79 |
80 | 无论是哪种具体实现技术,都不要实现成如下图所示这样
81 |
82 | 
83 |
84 | 在插件之上**不应该有**一个额外的包(模块)包含业务逻辑了。插件对主板的插入应该是一个 AutoWire,纯机械不含业务的过程。业务编排这样的概念一定不要出现在依赖关系的最顶层。我们已经在最底下的主板实现了所谓的“业务编排”了。
85 |
86 | SaaS 可以把自己的功能拆解到多个插件来实现。但是经常有“按需”组装,或者付费购买的需求。我们并不需要动态来组装代码来获得“按需”组装的产品效果。代码可以是一份,只是通过运行时的开关来控制某些插件是否启用。这些开关可以是配置文件,也可以是数据库表来控制。在没有启用的时候,界面上完全隐藏相关的组件(就是 if/else 判断),用户也察觉不到这个功能的存在。付费购买其实就是付费买这个开关,也不需要像 Apple Store 那样真的去做什么代码下载和安装。当然给外包公司做二次开发就是完全另外一个话题了,与本主题无关。是否打开某个插件可以是全局性的(给每个商家或者租户启用),也可以是“订单”级别。一个所谓的订单履约流程,需要组合多个插件的功能。对于每个订单来说,都有一堆 bit 开关来决定某个插件是否启用了,以及对应的业务数据是什么。比如 GroupPurchase + OrderSelfPickup + Order 可以组合成团购自提的订单。订单在此处只是一个例子,不同类型的业务有自己的领域概念。
87 |
88 | # 第三步:管控规范型需求的代码审查
89 |
90 | 有了主板加插件,Monorepo 已经切分出了多个子目录了。每个开发者也基本上能够知道什么需求写在什么目录下,哪些目录是自己经常修改的。接下来的问题是,如果每个开发者都各写各的,那他们之间有重复实现怎么办?谁来避免同一个东西,被不同产品经理提出多遍,再由不同的开发者用不同的姿势实现多变,导致浪费和返工?这个也是一个 Code Review 的问题。不能指望有一个人来 Review 每一行代码。
91 |
92 | 解决办法就是我们希望有一个人来“收口”,然后由这个人来保证收口之后的代码没有重复的实现,建立合理的抽象。如下图所示
93 |
94 | 
95 |
96 | 所谓“收口”,就是要阻止上图中这样的绕过“这层抽象”,去访问“底层API”的行为。比如说,所有的编程语言都提供了 Http 调用的能力。但是我们希望封装一个 Http Restful API 的调用 SDK。在这个 SDK 里我们统一实现重试,统一实现熔断摘除故障节点这样的一些功能。避免每个调用 Restful 接口的地方都重复地 try catch,重复地写不一致地重试逻辑。那就需要有一个人来封装这样的库,同时强制所有“应该使用这个库的地方”都使用了这个库。
97 |
98 | 实现方案要比管控集成类需求要稍微麻烦一些。集成类需求可以用包之间的依赖关系来约束什么代码写在哪里,规范型需求的问题是假设一个业务包,比如团购。它依赖了 Http Restful SDK,而 Http Restful SDK 又依赖了 Http 的库。那么就意味着团购这个包通过依赖的传递性,也依赖了 Http 的库。在现有的编程语言里,都无法禁止团购的包通过传递性依赖获得的调用 Http 库的权利。这个时候我们就需要通过自制 lint 工具的方式,在编译期额外做更严格的依赖关系检查。通过 lint 检查,强迫所有访问 Http 库的代码都“收口”到某个目录里。然后我们就可以通过 Review 这个目录的改动,确保重试逻辑只写了一份,而不是散落到各个地方。
99 |
100 | 这样的 lint 规则可以检查以下类型的访问
101 |
102 | * 对某个 API 是否可以调用:比如 Http 库的 API
103 | * 对某个自定义类型是否可以调用其指定的方法:比如 Datetime 类型,或者业务上自己封装一个 Money 类型
104 | * 对某个 API 的某几个参数是否可以传值:比如组件库中的 Button 组件提供了 style 属性,我们不希望把这么灵活的属性暴露出去
105 | * 对语言和框架某些特性是否可以使用:比如 vue 文件中可以写 style,但是我们不希望所有的目录都可以写 style
106 |
107 | 再举一个例子。通过 lint 检查,我们可以确保所有包含样式的前端组件都写在某个目录下,比如说 RegularUi 和 SpecialUi。其他目录中的组件,只能是通过组装 RegularUi 和 SpecialUi 目录中的组件来完成自己的设计稿还原。当然这样就是一种“收口”。我们可以通过 Review 对 RegularUi 和 SpecialUi 这两个目录下文件的修改,来发现是不是有两个开发者在尝试实现极度类似的页面组件,也可以促成两个产品经理互相交流一下,是不是把两个组件合并成一样的行为,避免不必要的实现成本。
108 |
109 | “收口”的代价是不可避免会出现很多一次性的需求,个性化的需求。比如优惠券的界面就是要和其他界面不一样。因为对样式做了收口,所以就不能直接写在优惠券这个包里面。于是就有了 SpecialUi 这个目录,用于写被收口了,但是并不可复用的东西。SpecialUi 里的组件数量的多寡,体现了 Ui 不一致性的严重程度。如果每个页面都不一样,都非常有艺术感。那说明这样的产品并不适合对样式进行收口,就应该各写各的,每个页面都纯手工打造。
110 |
111 | “收口”的 lint 检查的关键是要去掉对人的主观判断的依赖。我们不需要判断这里来是不是一定能复用 RegularUi。我们宁愿过度收口,导致 SpecialUi 的出现,也要避免人为主观判断的介入。这种“过度的”收口,是规范型需求能实现自动化检查的关键。一旦我们允许酌情出现一些例外的情况下,那么又变成了需要 Review 每一行代码了。
112 |
113 | “收口”之后的一个风险是强行抽象。明明不适合复用同一个组件的场合,仍然复用了同一个组件。导致组件变得更复杂,导致组件经常被修改。一个对策是控制组件或者函数的参数个数,参数应该尽可能地少。如果某个函数在 Monorepo 中有10处调用,但是其名为 IsVipUserPriviliged 参数仅仅在 1 处调用有传值。那么这个 IsVipUserPriviliged 参数大概率是不应该被添加进来的,是强行抽象的产物。对于 IsVipUserPriviliged 的处理,更适合直接写在调用的地方,而不是被写到可复用的目录里。
114 |
115 | # 收益
116 |
117 | 在这三步都完成之后,你获得了一个“机器人”。它帮你在每个开发者提交代码的时候检查代码是不是写到了正确的位置。再通过了这个机器人检查的基础上,你只需要关注重点的一些目录就可以了,对其他的修改仅仅需要抽查。这个机器人能够像拆分了微服务一样,确保代码不写乱。同时不像微服务那样,拆分之后就很难调整了。因为代码仍然一个仓库里,只是分了目录,随时都可以再调整。
118 |
--------------------------------------------------------------------------------
/autonomy/README.md:
--------------------------------------------------------------------------------
1 | 从 Autonomy 的角度来看
2 |
3 | * [模块切分的好坏标准是什么?](#criteria-of-modularization)
4 | * [这些经典的解决方案用了也就那样](#module-boundary-unchanged)
5 | * [松耦合的接口应该定义成什么样子?](#loosely-coupled-interface)
6 | * [为什么实际的业务代码都没有写成你说的那个样子?](#retrospective)
7 |
8 | # 模块切分的好坏标准是什么?
9 |
10 | 如何对系统进行模块分解。需要分成几个模块,模块之间的依赖关系是怎样的?
11 | 我们通过4个例子来非常具象化地讨论。
12 |
13 | ## 公共模块应该稳定
14 |
15 | [【阅读该例子】](./criteria-of-modularization/common-module-should-be-stable)
16 |
17 | 在 [Agile Software Development](https://www.amazon.com/Software-Development-Principles-Patterns-Practices/dp/1292025948) 书中,Robert Martin 讲过了很重要的两个原则
18 |
19 | * 越是被很多模块依赖的模块,越应该减少改动。道理很简单,底层模块一改,上层的模块必然受到影响。依赖关系的方向,就是“不稳定”依赖“稳定”的方向。
20 | * 要复用的模块不要把过多的东西捆绑,要复用就整体复用:Common Reuse Principle
21 |
22 | ## 避免超级繁忙的顶层模块
23 |
24 | [【阅读该例子】](./criteria-of-modularization/avoid-crazy-busy-top-module)
25 |
26 | 模块与模块之间的依赖关系,就是抽象与稳定的关系。但实践中,像“业务编排API”和“BFF”,你很难判断谁比谁更稳定,更抽象。当我们一个业务请求,需要经过一串模块的时候,往往是有问题的。因为当要做修改的时候,你会觉得在哪个环节拦一刀都有道理。David Parnas 在 [The Secret History of Information Hiding](https://www.researchgate.net/profile/David_Parnas/publication/200085877_On_the_Criteria_To_Be_Used_in_Decomposing_Systems_into_Modules/links/55956a7408ae99aa62c72622/On-the-Criteria-To-Be-Used-in-Decomposing-Systems-into-Modules.pdf?origin=publication_detail) 一文中也写道,他认为 Levels of Abstraction 是很难判断的。
27 |
28 | 这个例子应该怎样调整是合适的?分法有很多,可以按流程步骤分,可以按业务变化频率分,但从依赖关系的结构上来说,一定是这样的结构
29 |
30 | * 一定是**多个模块**直接面向**多个业务方向**,每个模块承担一些,而不是集中把修改工作都压到一个顶层模块上
31 | * 在这多个模块上面一定不能拿一个“业务收口”模块再往顶上套一层。所谓业务编排,其实就是业务编程。只要可以编程,就会抑制不住地往里面加东西。
32 |
33 | 不会因为把函数调用,改叫“业务编排”,就改变模块之间的依赖关系。依赖关系才是真正决定性因素。
34 |
35 | ## 通过新增模块来扩展功能
36 |
37 | [【阅读该例子】](./criteria-of-modularization/extending-by-adding-new-module)
38 |
39 | 从这个例子里我们可以看到如下的规律
40 |
41 | * 一个新功能要修改哪个模块取决于模块的依赖关系。typescript-language-features 在依赖关系里有 ts server,所以一些功能就得改它那里。
42 | * 新增模块来实现新功能可以避免把给现有模块添加新的依赖,比如 mocha 这个依赖就只需要加到 mocha-test-explorer 上。是否新增一个模块,还是修改已有的模块,是否引入依赖是一个关键决策因素。
43 |
44 | 稍微有点经验程序员都能体会到 vscode 做为 Eric Gamma 大神在 eclipse 之后的又一力作,架构上是很优秀的。但可能只是感觉优秀,又说不出来优秀在哪里。通过这个例子,我们就可以看到,判断一个模块拆分结构是否优秀的唯一标准,就是看它如何处理需求的变更和新增。当所有的需求都要往一个模块里改的时候,这个拆分就是糟糕的。当新的需求往往可以通过新增模块来实现的时候,这个拆分结构就是优秀的。
45 |
46 | ## 要更关注“易变性”而不是“功能切分”
47 |
48 | [【阅读该例子】](./criteria-of-modularization/more-focus-on-violatility-than-functionality)
49 |
50 | 这个例子说明了
51 |
52 | * 按流程步骤切分未必是最优方案,“易变性”是更重要的可度量指标,其实看看 git 提交记录就知道了
53 | * 前面三个例子都是说明不要把改动集中到一个模块里,这个其实相对好实现。极端情况下,每个函数,每一行代码都拆成一个模块就可以实现。
54 | * 这个例子要说明的是反过来,如果一个需求要改动太多的模块,那也是有问题的。这个其实非常难以实现,但是是追求的目标。
55 |
56 | ## 模块切分的好坏标准
57 |
58 | 模块切分的出发点其实非常简单直白
59 |
60 | * 如果所有的需求都要集中到一个模块去等排期,那必然会拖慢速度
61 | * 公共模块应该稳定
62 | * 避免超级繁忙的顶层模块
63 | * 通过新增模块来扩展功能
64 | * 如果一个需求动辄就要数个模块联合开发和调试,那必然会拖慢速度
65 | * 要更关注“易变性”而不是“功能切分”
66 |
67 | 综上,在这样的一个依赖关系下
68 |
69 | 
70 |
71 | 经常修改的应该是中间这一层。最好是一个需求只添加,或者修改其中的一个模块。
72 |
73 | 模块划分的静态结构无所谓好坏,只关注新需求如何修改或者新增的问题。
74 | 不用去争辩是应该大前台,还是大中台。代码量不是问题,圈复杂度也不是问题。
75 | 唯一度量的标准就是去看每个新需求都是怎么改出来的。
76 |
77 | # 这些经典的解决方案用了也就那样
78 |
79 | 上一章提出的好坏标准有任何一条是新鲜的么? 一条都没有。
80 | 以前的文章可能例子举得少了一点,但是总结的原则都是差不多的。
81 | 那接下来的追问就是,如果原则一直都在那里,那为什么我们过去看过的代码都没遵守这些原则呢?
82 |
83 | 我有三个猜测
84 |
85 | * 最容易想到的解决方案未必是最佳的方案
86 | * 真正松耦合的接口定义形式并没有被大众所熟知
87 | * 按照这些松耦合的接口去分解模块并不容易落地
88 |
89 | 我先来分析第一个猜想,列举几个最常见的解决方案。
90 |
91 | ## 用 interface 代替 class
92 |
93 | [【阅读该例子】](./module-boundary-unchanged/replace-class-with-interface)
94 |
95 | 这个例子里说明了两个现象:
96 |
97 | * 按流程步骤切分的模块,步骤之间必然有很强的数据依赖。这种业务逻辑上的依赖,用任何形式上的解耦合方式都是不起任何作用的
98 | * 名字具有欺骗性,收银台这样的生活中能够遇见的物理存在的概念,和业务上实际承担的角色可能是不对等的。你在商场里看见的收银台,和你这个系统里的收银台,只是名字相同。不能简单地认为 `pay(100, 'USD')` 就可以完全把服务给封装起来。
99 |
100 | 如果这种一个模块需要 `f(args)` 传递一个很大的结构体给另外一个模块的方式是不理想的。那么更理想的模块间接口形式是什么?[有些文章](https://www.ben-morris.com/why-is-loose-coupling-between-services-so-important/)会把运行时的RPC性能,系统不宕机,和不易于独立做需求变更放在一起讨论。但是我认为这样混到一起来讨论问题会误入歧途。比如是不是数一数两个模块之间的 RPC 调用的数量,甚至是从运维系统里导出一份运行时的 RPC metrics 就可以说明两个模块的耦合程度呢?
101 |
102 | 应该聚焦在“新需求怎么接”这一个问题上,不要躲闪,不要旁顾左右而言它。其实就是看一下,两个模块之间边界无论用什么来定义,是不是经常要被修改。用 interfac 关键字代替 class 关键字不会有实质性的作用。用 gRPC 代替 jar 包也不会有实质性的作用。
103 |
104 | ## 阿里中台到底是什么?
105 |
106 | 阿里巴巴公司有一个名字叫“中台”的技术。
107 |
108 | [【阅读该例子】](./module-boundary-unchanged/alibaba-middle-office-technology)
109 |
110 | 这个例子说明了两个现象:
111 |
112 | * 没有复用价值的复用是不值得去复用的:阿里内部至少三套“中台”框架。商品和交易链路对于淘系,盒马,闲鱼也是不同的。因为大家做出了判断,之前的方案,之前的模块,对当前的业务需求没有复用价值。这是一个理性的决定,不复用很多时候是正确的选择。
113 | * 没有消除 if/else 的银弹。不调整模块边界,只换一个if/else的表达方式,是换汤不换药的。
114 |
115 | ## 打包和部署方式的调整
116 |
117 | [【阅读该例子】](./module-boundary-unchanged/packaging-deployment)
118 |
119 | 这个例子说明了:
120 |
121 | * 代码生成,lowcode 等技术,实质上都是模块的打包和部署方案。
122 | * 从纯静态,到纯动态,我们有一个光谱的选项。选择更静态,还是更动态,主要的看这个修改是程序员来做,还是运营人员来做。只是不同角色的“编程人员”的称谓差异。
123 | * 打包和部署方案不会改变模块之间的边界,从管理依赖控制变更范围的角度来说,打包和部署的各种方案是等价的
124 |
125 | ## 不调整模块边界是没有效果的
126 |
127 | 上面的三个例子的共同特点就是用一个形式代替另外一个等价的形式。
128 | 调整打包和部署方式是容易的。
129 | 调整模块边界,重塑接口,这个是要触及灵魂的。痛彻心扉。
130 | 最容易想到也是最容易办到的方案,未必就是最好的方案。
131 |
132 | # 松耦合的接口应该定义成什么样子?
133 |
134 | 上一章我们看到了,只是改变依赖的“形式”,不会影响依赖的“实质”。
135 | 如果要让模块之间更好组合,最终仍然是要去调整模块之间的边界,也就是要把“模块接口”定义成“松耦合”的。
136 | “松耦合”已经是陈词滥调了。能不能用具体的例子来说明到底这样的接口是长什么样子的?
137 |
138 | ## 基于 UI 的组合
139 |
140 | [【阅读该例子】](./loosely-coupled-interface/ui-composition)
141 |
142 | 这个例子展示了两个最实用的技术:
143 |
144 | * 在 UI 上做组合,对于编排方来说,UI 组件内部就是一个完全的黑盒。
145 | * Opaque Business Pointer:透传业务 id,每个模块都可以根据这个 id 来解决出自己需要的含义来。对于透传方来说,这个 id 就是一个完全的黑盒。
146 |
147 | 无论是“UI组件透传”,还是“id透传”,从耦合级别上来说都是最黑盒的那种。
148 |
149 | ## 不要返回值
150 |
151 | [【阅读该例子】](./loosely-coupled-interface/no-return-value)
152 |
153 | 这个例子说明了两点:
154 |
155 | * Event 就是不要返回值。所以两个模块之间交换的信息更少,接口就更松耦合。
156 | * 单独由后台开发引入 Event 往往无法达成目的。需要配合前端团队引入“基于 UI 的组合”,以及配合产品团队引入“产品方案降级”才能调整得动边界。模块之间的边界是不能靠一个职能团队撬动的,必须集合前端,后端,产品多方的合力。
157 |
158 | ## 虚拟文件系统
159 |
160 | [【阅读该例子】](./loosely-coupled-interface/virtual-file-system)
161 |
162 | 这个例子说明了两个技术:
163 |
164 | * 一个模块可以把自己伪装成“虚拟文件系统”。让使用者感觉自己就是在读写一个存储而已。
165 | * pull v.s. push:在所有视图渲染,事件模式检测类的业务里。pull 都是更好的策略,它可以产生最精确的依赖关系,减少变更的影响范围。
166 |
167 | 当“UI 组合”这种纯黑的方案不行,“不要返回值”这种半黑的方案也不行,那么伪装成存储是需要模块间双向通信的前提下的比较优的解决方案。
168 |
169 | # 为什么实际的业务代码都没有写成你说的那个样子?
170 |
171 | 这是一个真正拷问灵魂的问题。
172 |
173 | * 有大量的实际业务中的项目,违反了所有的最佳实践。但是商业上仍然大获成功
174 | * 从1971年的《On the criteria to be used in decomposing systems into modules》开始,就不断鼓吹要做好模块分解。为什么这么多年过去了,不但没有看见进步,甚至感觉还在退步?
175 |
176 | 前两天在朋友圈刷到一句睿智的话
177 |
178 | * 当你听到别人的一个想法的时候,先想想为什么行得通
179 | * 当你要提出一个想法的时候,先想想为什么行不通
180 |
181 | 行得通的理由
182 |
183 | * 虽然不用最佳的模块切分,会导致更多的联合修改联合调试,但是仍然可以完成需求
184 | * 目标是及时响应市场需求,这并不完全依赖好的模块切分。通过996,通过找更多的人,仍然可以达成目标。
185 | * 编码只是响应市场需求要做的工作中的很少一部分,只要能实现,就不决定成败。有的时候系统宕机,可能更致命,更值得解决。
186 | * 设计简单,新人更容易上手
187 | * 弊端需要成年累月才会显现出来,对创业期没有影响
188 | * 技术革新速度很快,新公司倒闭速度很快,代码本来就应该隔几年重写一次
189 | * 数学家总是希望自己的定理尽可能泛化普适,而工程师为了效率等原因,更追求适用就好
190 |
191 | 行不通的理由
192 |
193 | * UI组合:前端技术最近几年变动特别剧烈,一直稳定不下来
194 | * UI组合:UI非常专业,需要独立的团队。没法实现端到端的业务切分
195 | * 不要返回值:我要返回值啊,没返回值实现不了界面,实现不了需求
196 | * 运行时报错信息是在一起的,这么分着写很难和运行时的现象对应起来
197 | * 编辑的时候往往需要在更多的模块/目录/文件之间跳转
198 | * 需求可能是跨模块的,创新性的需求往往会对模块化的假设产生剧烈的影响
199 | * 产品经理的分工调整,产品需求的粒度是经常变化的,导致一个产品经理需要改多个模块
200 |
201 | 我们要客观地看待复用和支持多样性。很多时候不复用就是最佳的解决方案。很多时候堆砌 if/else 就是最佳的解决方案。
--------------------------------------------------------------------------------
/autonomy/criteria-of-modularization/avoid-crazy-busy-top-module/README.md:
--------------------------------------------------------------------------------
1 | 假设在一个虚构的电商业务里,我们有如下的模块划分结构
2 |
3 | 
4 |
5 | 底下的各个微服务之间互相没有依赖关系。为了实现业务流程,安排了一个“业务编排API”模块来把这些微服务串联起来。同时为了让前端开发迭代效率更高,前端团队用serverless搭建了一个BFF模块(Backend for frontend)用来给前端页面收拢数据接口。我们不讨论现存代码量是多还是少的问题,也和粒度切分没有关系。我们只考虑新需求在哪里做的问题。
6 |
7 | 假设商品评论页需要允许没有购买过此商品的人也可以评论,但是购买过商品的人会在旁边加一个标签“验证购买过”,以表示这条评论更加真实可信。这个需求需要使用订单信息,所以商品微服务自己是无法完成这个业务逻辑的。那么我们有两个选择
8 |
9 | * 修改“业务编排API”
10 | * 修改“BFF”
11 |
12 | 如果是一个前端同学来写这个需求,他可能就加到 BFF 里了。如果是一个后端同学来写这个需求,他可能就加到业务编排API里了。
13 |
14 | 假设我们又有一个需求,需要售卖电子书。然后在退货退款页面里,对于电子书要把退货这个选项给去掉,因为电子书也没有发货和物流的概念。那么显然这个需求订单自己也搞不定,需要在产品目录里先把电子书这个品类上架上去。然后需要在退货退款页面里,加上可选项的接口调用。同样的,我们可以选择把这个“退货可选项”的接口加在业务编排API里,也可以加在BFF里。
15 |
16 | 然后我们又有一个需求,需要在订单结算页里添加蜜豆(一种代金券)可抵扣和不可抵扣的拆分。那么同样的,只有业务编排API和BFF同时拥有访问蜜豆配置(是否可抵扣)和商品目录(价格)的依赖关系,所以这个需求再一次地需要麻烦业务编排API的同学了。
17 |
18 | 我们可以总结出两个规律来:
19 |
20 | * 因为依赖关系,决定了流程性的业务只能在上层的模块里写,因为那里的信息最全。
21 | * 当“业务编排API”和“BFF”除了名字不同,依赖等级几乎完全相同的时候,我们很难判断谁比谁更抽象,谁比谁更稳定。
22 |
23 | 稍微有点经验的同学都能看出来,这种啥需求都往一个模块里改的模式是不可持续的。造成这样的原因在于最顶层的模块一定是最不稳定的,因为在那个地方写业务是最方便的,大家都想改那里。然后以“业务收口”的名义,只放一个模块在最顶层,就会导致所有的改动都集中到这一个模块里。
24 |
--------------------------------------------------------------------------------
/autonomy/criteria-of-modularization/common-module-should-be-stable/README.md:
--------------------------------------------------------------------------------
1 | 假设在一个虚构的出行业务里,我们有如下的模块划分结构
2 |
3 | 
4 |
5 | 计费模块提供了统计一个订单的里程,综合计算出费用的功能。专车,快车,企业等业务线都会复用这个计费模块来出自己的账单,收取费用。
6 | 我们不讨论是否是小前台大中台。这个问题和大小没关系,也和这些模块现存的代码量没有关系。
7 | 我们来考虑一个问题,就是当需求来了的时候,应该改哪个模块的问题。
8 |
9 | 假设快车需要做一个拼车的功能,需要给司机出一个账单,给乘客出另外一个账单。司机需要按实际行驶时长和里程计费,乘客只需要一个一口价就可以。那么我们改哪个模块呢?
10 | 如果改计费模块,那么大概是这样的
11 |
12 | ```
13 | function beginOrder(orderDetails, isPinChe) {
14 | }
15 | ```
16 |
17 | 根据这个 isPinChe 的标志位,计费模块会计算出两个账单出来。但是似乎里程计算并不受到影响,因为实际上只有司机需要计算里程。
18 |
19 | 然后我们又添加了一个需求,如果司机的账单和乘客的一口价之间差价过大,则可能是司机故意绕路,骗取平台的补贴。那么这个时候,需求就是要把乘客的账单从一口价,改成乘客所乘坐区段的实际账单。
20 | 注意这个实际乘坐区段并不等于司机的行程里程。因为如果有两个乘客前后上车,两个乘客的区段重叠之后合计里程,才是司机的行驶里程。
21 | 那这个时候计费模块就需要维护订单和行程,两个级别的账单。
22 |
23 | ```
24 | function beginTrip() {
25 | }
26 | function beginOrder(orderDetails, isPinChe) {
27 | }
28 | ```
29 |
30 | 这个会造成一个问题,就是这个功能其实是快车产品线独有的。企业出行的产品是不需要这部分代码的,但是他们有自己的独特需求。
31 | 企业出行的需求是他需要能够以快车的费用给企业报销,但是实际乘客是打的专车,然后用专车计费模式计算出来的账单是乘客+乘客所在企业实际需要支付给平台的。
32 | 快车费用和专车费用之间的差价,需要由乘客本人自己补齐。之前的拼车行程是一个司机账单,再给每个乘客一个账单。而企业的订单是企业一个账单,乘客一个账单。
33 | 然后,我们又接着修改计费模块
34 |
35 | ```
36 | function beginOrder(orderDetails, isPinChe, isQiYe) {
37 | }
38 | ```
39 |
40 | 我们可以看到,这个模式的显著缺点:
41 |
42 | * 计费模块和其用户之间的接口会越来越复杂,isXXX 的参数越加越多。这些复杂性是大部分模块的使用方用不到的,但是被迫接受。
43 | * 计费模块被多个业务线复用,其稳定性会影响很大。对计费模块的频繁修改,导致所有业务线的稳定性都受到影响。
44 |
45 | 如果仅仅根据上面这两个需求,与其把一个计费模块做大做强,把计费模块拆成多个是更合理的
46 |
47 | * 司机的行程计费
48 | * 乘客的专车计费
49 | * 乘客的快车计费
50 |
51 | 然后由上层的模块,由这些**依赖关系上处于更不稳定位置**的模块,去写不稳定的业务逻辑。
52 | 比如一个拼车行程,就包括了乘客和司机两份账单。比如一个企业订单,就需要给乘客做两份账单,然后再计算出哪些是需要乘客补齐的部分。
--------------------------------------------------------------------------------
/autonomy/criteria-of-modularization/common-module-should-be-stable/dependency.drawio.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/autonomy/criteria-of-modularization/extending-by-adding-new-module/README.md:
--------------------------------------------------------------------------------
1 | 在 vscode 中,我们有如下的模块划分结构
2 |
3 | 
4 |
5 | 其中 vscode 提供了字符输入,换行,响应鼠标等基础的编辑器。typescript-basics 模块依赖了 vscode,提供了代码区块的折叠能力(识别typescript的class/function语法),提供了基于词法分析的着色。而 typescript-language-features 则提供了代码补全等需要 typescript 类型检查信息的 IDE 编辑功能。
6 |
7 | 假设有一个需求是需要在文件上添加一个右键菜单,展示所有调用了这个文件的列表。我们应该修改哪个模块? 显然应该是修改 typescript-language-features,因为只有它依赖了 ts server,其他模块拿不到函数调用关系的信息。
8 |
9 | 假设需要提供一个快捷键,把整个段落的代码选中。那么我们是应该修改 typescript-basics 还是 typescript-language-features? 这个情况下,修改 typescript-basics 就可以实现。因为这个功能仅仅需要对代码做词法分析,就可以识别出“段落”来。
10 |
11 | 但是上面两个修改都不需要修改 vscode 本身。如果把对 typescript 的功能修改,实现在 vscode 这个模块里则会显得非常奇怪。就像我们在前面计费模块的案例里看到的,像 vscode 这样的位于依赖关系底层的模块,应该尽量少的改动。如果改动了之后,会导致引入很多其他上层客户不需要的功能,则违背了 common reuse principle。
12 |
13 | 如果我们要给 *.spec.ts 的文件,添加如下的业务功能
14 |
15 | 
16 |
17 | test 上面的 “Run | Debug” 就是需要新增的功能。那么我们是应该修改 typescript-basics 还是 typescript-language-features 呢? 都不应该。因为这个功能需要依赖 mocha 的 test runner,要在点击按钮之后调用 mocha 执行测试。所以实现的方式是
18 |
19 | 
20 |
21 | 通过新增一个模块的方式,把新功能给实现了。这样 vscode / typescript-basics / typescript-language-features 都不需要修改。虽然最终呈现的效果是在用户打开了 *.spec.ts 文件之后,给这个编辑器增加了一个新的功能。vscode 是如何做到这一点的呢? 是因为 vscode 把编辑器上的功能做了“标准化的定义”。vscode 把自己的职责从实现需求,变成了制定标准。这些 editor 区域的标准化扩展接口就包括了
22 |
23 | * CodeLens
24 | * CodeAction
25 | * Formatting
26 | * SignatureHelp
27 | * ...
28 |
29 | 这些扩展接口可以是 typescript-language-features 提供一部分,然后 mocha-test-explorer 实现另外一部分,由 vscode 最终集成起来。但是这个集成和前面的“业务编排API”的例子不同。不同之处体现在了依赖关系上,vscode 是被 typescript-language-features 依赖,而不是 vscode 同时依赖了 typescript-language-features 和 mocha-test-explorer 去把这两个模块整合起来。
30 |
31 |
--------------------------------------------------------------------------------
/autonomy/criteria-of-modularization/extending-by-adding-new-module/dependency1.drawio.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/autonomy/criteria-of-modularization/extending-by-adding-new-module/mocha.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/autonomy/criteria-of-modularization/extending-by-adding-new-module/mocha.png
--------------------------------------------------------------------------------
/autonomy/criteria-of-modularization/more-focus-on-violatility-than-functionality/README.md:
--------------------------------------------------------------------------------
1 | 假设有如下虚构的外卖业务,其现有的模块划分如下图所示
2 |
3 | 
4 |
5 | 用户先在商品陈列模块里完成商品的选购,然后去结算页选择优惠。下单之后,骑手配送到你家,点收货。如果产生了异常,例如取消订单,由售后模块负责退款。
6 |
7 | 为了在业务低峰期促进高价值订单的成交。产品经理想要给外卖业务添加一个惊喜折扣的功能。大概的需求是先根据用户的浏览记录,选择命中的用户。在用户浏览商品陈列的时候,以天降红包的形式提示你被惊喜砸中了。在砸中之后,商品会出现一个额外的折扣价格。然后在结算页也需要提示这个折后价格帮你省了多少钱。但是这个折扣价,必须用一张会员红包进行抵扣。如果支付了之后,用户想要取消订单。订单使用的会员红包应该原路退回,实际退款金额应该等于当时支付金额。
8 |
9 | 为了实现这个需求,这些模块约定了在订单上添加一个字段表示这个是天降红包类型的订单。然后每个模块各自按照约定的字段,进行本模块所需的修改。
10 |
11 | 活动上线了几天之后,发现一些用户在被红包砸中之后没有立即下单。而是过了两个小时才下单。这个时候因为业务已经处于高峰期了,商家并不愿意亏本接这样的订单。产品提出需要给这个惊喜折扣加上时间限制,不是出现一次之后就一直可以享用。商品陈列模块和结算模块约定,必须传一个“天降红包权益”的id过来,在做下单的时候校验一下这个权益是否仍然处于有效的状态。
12 |
13 | 然后过了两天,发现这个功能很受欢迎。部分用户表示,能否一次多抵扣几张会员红包,享受更大的折扣?但是在第一次实现的时候,订单上添加的字段为“是否使用了红包”,没有考虑到需要改成数量。于是各个模块需要按照新的接口重新进行适配,又改一轮。
14 |
15 | 从这个例子我们可以看到两点
16 |
17 | * 在前面的案例里,我们看到每次做需求,都改同一个模块是有问题的。这个业务里,通过模块拆分,避免了大家都改同一个模块的问题。这是做的好的地方,把工作平摊了出去。
18 | * 但是在多轮的需求修改里,整个交易链路上的多个模块需要反复被修改,不断适配新的业务形态。
19 |
20 | 从“天降红包”这个项目组的角度来说,他们是非常希望能够有一个独立的模块来对他们这块业务负责的。从权益的发放,消费,到回滚,整个流程的业务都收敛到一起。识别这个模块的过程,就是我们识别“易变性”的过程。当我们发现这些地方经常同时被修改的时候,那么独立出一个模块来就比较合适。
--------------------------------------------------------------------------------
/autonomy/criteria-of-modularization/more-focus-on-violatility-than-functionality/dependency.drawio.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/autonomy/loosely-coupled-interface/no-return-value/README.md:
--------------------------------------------------------------------------------
1 | 假设有如下的论坛业务,其现有模块划分如下图所示
2 |
3 | 
4 |
5 | 用户每次发帖,都要给用户添加一些积分。
6 |
7 | 假设需要添加一个需求,用户一天发了三个帖子之后,完成了“每日活跃”任务,额外奖励更多的积分。如果是修改“帖子模块”就是
8 |
9 | 
10 |
11 | 在发放积分之前,先查询一下任务模块,再决定发多少。
12 |
13 | 假设又有一个需求,如果是新用户发的首个帖子,需要给邀请人发放积分。如果仍然是修改“帖子模块”,就需要变成这样
14 |
15 | 
16 |
17 | 从做需求的角度来说,帖子模块已经在发放积分了,我这个需求不过是给如何发积分添加了一些花样而已,改动很小的。但是每次改动,仍然是需要帖子模块和其他模块进行联调。
18 | 要减少模块之间的耦合就是要减少模块之间这样的 `f(args)` 的直接调用关系。
19 |
20 | 
21 |
22 | 帖子模块把新建帖子的事件发到消息队列之后就可以不管了。任务模块,拉新模块要做什么自己的逻辑,都可以自己去订阅这个消息队列。
23 | 帖子模块用对“消息队列”的依赖,替换了对积分模块,任务模块,拉新模块的依赖。
24 |
25 | > 这个技术不就是 Event Driven 吗?我跟你说,这个是行不通的。满足不了我的业务需求
26 |
27 | 以上这样的对话经历过几次之后,可以总结出以下的常见反例
28 |
29 | * 业务的下一个步骤是不可或缺的。比如你打了个车,这个订单变成一个 event 有什么意义。用户要的是有车来接我,主流程上玩花活没有收益。
30 | * 业务必须及时完成,我跳转的下一个页面就需要这个数据那,你异步的 event handler 消费慢了一点怎么办?
31 | * 我这个RPC接口的返回值上有这些字段是界面必须要的,如果改成发消息异步处理了,我RPC接口就得改了,前端那边不干啊
32 | * 什么!?你要改产品方案,改界面?是不是没有被PM毒打过?
33 |
34 | 对于所有得反例,你只需要记住一个问句就可以:
35 |
36 | > 如果这个依赖宕机了,我这个RPC就不能降级吗? 降级之后的界面怎么显示缺失的“返回值”?
37 |
38 | 在一个虚构的出行系统里,司机在结束计费的时候能够看到本单能收到多少费用。理论上来说,可以在结束计费的时候通过 Event 来出账单。但是因为结束计费和展示费用在产品设计上是一体的,所以这个基于 Event 的解耦一直无法实现。直到因为同步出账产生了太多次 P0 事故之后,终于在展示费用这个产品功能上添加了一个可降级的版本。也就是当费用还没有计算出来的时候,展示“费用计算中”。通过产品需求的调整,实现了模块之间的松耦合。
39 |
40 | 只要是可被降级的依赖,就是可以被异步化的依赖。
--------------------------------------------------------------------------------
/autonomy/loosely-coupled-interface/no-return-value/dependency1.drawio.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/autonomy/loosely-coupled-interface/no-return-value/dependency2.drawio.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/autonomy/loosely-coupled-interface/no-return-value/dependency3.drawio.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/autonomy/loosely-coupled-interface/ui-composition/README.md:
--------------------------------------------------------------------------------
1 | 下面这个例子来自于 Finding your service boundaries - a practical guide - Adam Ralph (https://www.youtube.com/watch?v=tVnIUZbsxWI&t=2482s)。
2 | 以下为中文译本
3 |
4 | 我相信大家都在亚马逊之类的网站上买过东西。当你在亚马逊上买东西的时候就开始了一个工作流。第一步,下单
5 |
6 | 
7 |
8 | 然后选择你的送货地址
9 |
10 | 
11 |
12 | 选择使用的信用卡
13 |
14 | 
15 |
16 | 再选择送货方式
17 |
18 | 
19 |
20 | 再确认
21 |
22 | 
23 |
24 | 直到你最终完成整个下单过程。
25 | 我们都知道亚马逊不是一个 Monolith。
26 | 它也许是世界上最 Service-oriented 的公司了。
27 | 亚马逊发布的时候不会打包成一个T的应用,然后一起发布。
28 | 所以你可以假设在这个工作流中是有很多个Service参与的,它们被某种形式编排,从而使得流程变成这个样子。
29 | 那么在你下单的时候,我们看一下后端的会有哪些 Event。
30 | 它们可能是这个样子的:
31 |
32 | 
33 |
34 | 有 sales,finance,shipping 三个 service。
35 | 当我们点击确认按钮的时候,command 被发往 sales,它可以做它想做的任何逻辑。比如检查所购商品是不是还有货,如果还在售但是库存不足了,我们可以往供应商再追加一笔采购单等。最终,sales 说,我搞定了,到下一步吧。
36 |
37 | 订单被创建出来,然后发布一个 event 给 finance 和 shipping。
38 | finance 收到这个 event 的时候,它可以为这个订单收取客户的费用。
39 | 它接着可能会发一个 order billed 的事件。
40 | 然后 shipping 知道订单创建了并且被 billed 了,它就可以发货了。
41 | 发货完成之后,再发一个 shipped 事件出来。
42 |
43 | 问题是:这些 event 都包含什么字段呢? 是不是 order placed 事件需要包含 shipping 所需的所有字段呢? 要不然 shipping 咋知道发往哪里。
44 | 是不是事件需要包含 finance 像客户收费所需的所有信息呢? 比如客户的信用卡详情之类的东西。
45 |
46 | 这么搞是可以行得通的。但是这种方式带来的问题是“耦合”。
47 | 如果事件需要包含送货方式,信用卡详情等所有信息,
48 | 我们又回到了高度耦合的被动场面里了。
49 | 我们又不能以独立的方式来修改系统行为了。
50 |
51 | 假设我们要开始支持用比特币付费了,我们需要给 sales 增加关于如何用比特币收费的信息。所以 sales 可以把这些信息再传给 finance。
52 |
53 | 如果我们要开始售卖电子书了,我们不希望买电子书也提供一个送货的门牌号。
54 | 所以我们又要修改 sales,从而它可以把电子书的信息传给 shipping。
55 |
56 | 上面这样的 event 就是所谓的 fat event。
57 | 可以看到,如果我们要开始卖电子书,或者收取比特币,这些 event 会变得越来越 fat,字段越来越多。
58 | 再某些时候,一些字段允许为 null,在其他场景下,这些字段又是必填的。
59 | 当它们是 null 的时候,事情就变麻烦了。
60 | 那我们如何把 fat event 重新又变回 thin event 呢? 如何让系统变得更松耦合呢?
61 |
62 | 如果我们在这些 event 上只包含 id 会怎么样?
63 | 如果 sales 在 order placed 的事件里仅仅说 order123 下单了。
64 | finance 收到 order123 之后,它已经知道如何对这个订单收费了。
65 | 它要么是用信用卡收费,要么是开始一个比特币的交易。
66 | 接下来 finance 告诉 shipping,order123 已经被 billed 了。
67 | shipping 收到 order123 之后,它知道要把这个订单配送到一个物理的门牌地址,
68 |
69 | finance 和 shipping 是怎么知道如何处理 order123 的呢?
70 |
71 | 
72 |
73 | 如上图所示,
74 | 这种 thin event 可以行得通的前提就在于每个 service 都有自己的 ui,它们可以直接从用户那里得到自己关心的数据。但是这种方法有两个问题
75 |
76 | 第一个问题是:如果订单要 sales 处理完之后才创建,那么 finance 和 shipping 的界面把自己的数据存到哪里呢? finance 和 shipping 怎么能在订单创建之前就知道订单在哪里呢? 这不就是先有鸡,还是先有蛋的问题了吗?
77 |
78 | 解决办法就是我们要在这个工作流的开始就把 order id 给生成出来,也就是这里
79 |
80 | 
81 |
82 | 甚至我们可以直接在网页上用 uuid 生成这个 order id。这使得在工作流的后面的步骤里,shipping 你要发到这个地址,订单id 是 123.
83 | 在finance那一步,你要从这个选择信用卡收费,订单id 是123。
84 | 直到工作流的最后异步,你才把 command 发给 sales,确认订单。
85 |
86 | 这样如果你想要支持比特币付款,我们仅仅只需要修改 finance。
87 | 因为 finance 收到 order123 的时候,它已经知道了该怎么收费了。
88 | 类似的,如果我要支持电子书的线上发货,我们也不需要修改 sales 了,我们可以把改动局限在 shipping 内部。
89 | 这么搞唯一诡异的地方就是要在流程的开端就把 order id 生成出来。
90 |
91 | 第二个问题:如果你把这么几步都填完了,再最后快下单的时候,你老婆从房里出来让你别剁手了,你不得不取消订单。你关闭了浏览器。这个时候,你填的收货地址,你选择的信用卡信息都卡在 finance 和 shipping 的手里,没法往下走了。
92 | 这些未完成的订单不算是浪费存储资源,不算是垃圾么?
93 | 垃圾只是放错了位置的资源。商家也许非常希望知道是哪些商品被选择了,但是没有完成下单的。
94 | 他们可以拿这些数据去优化购物下单的转化流程。这些数据都是非常有分析价值的。
95 | 而且你也知道,这些数据不过是数据库里的一行而已,也没有多少人会关心这些数据会带来多少开销。
96 | 而且也未必是要落盘到数据库,比如信用卡详情之类的东西,因为其敏感性,也许放在 session 里是更恰当的。这样当订单被取消的时候,这些 session 里的信息也自然就没有了。
97 |
98 | 在这个例子里,我们可以看到。模块与模块之间有两个集成方式
99 |
100 | * 在同一个界面里,左边渲染 sales,右边渲染 billing。两个模块通过共享同一块屏幕的方式实现了基于 UI 的集成
101 | * sales => finance => shipping 的事件里只包含了 id
102 |
103 | 在这个 UI 集成的例子里,我们可以再展开一下。如果,产品的需求是这样的:
104 |
105 | 
106 |
107 | 这种情况下,配送方式和收费方式就不再是独立保存了。它们要在点确认的时候统一保存,而且要在同一个弹框里反馈保存成功与否。在这种情况下配送方式和收费方式就不能仅仅提供
108 |
109 | ```
110 | render()
111 | ```
112 |
113 | 这么一个集成接口。还需要提供
114 |
115 | ```
116 | render()
117 | save(): err
118 | renderError(err)
119 | ```
120 |
121 | 注入这样的更多的接口出来,才能在 UI 上拼装出所需要的产品体验。但即便是这样,像支持比特币付款这样的需求改动,我们仍然是可以封闭在 finance 模块内部来完成。
122 |
--------------------------------------------------------------------------------
/autonomy/loosely-coupled-interface/ui-composition/proceed-to-checkout.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/autonomy/loosely-coupled-interface/ui-composition/proceed-to-checkout.png
--------------------------------------------------------------------------------
/autonomy/loosely-coupled-interface/ui-composition/step1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/autonomy/loosely-coupled-interface/ui-composition/step1.png
--------------------------------------------------------------------------------
/autonomy/loosely-coupled-interface/ui-composition/step2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/autonomy/loosely-coupled-interface/ui-composition/step2.png
--------------------------------------------------------------------------------
/autonomy/loosely-coupled-interface/ui-composition/step3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/autonomy/loosely-coupled-interface/ui-composition/step3.png
--------------------------------------------------------------------------------
/autonomy/loosely-coupled-interface/ui-composition/step4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/autonomy/loosely-coupled-interface/ui-composition/step4.png
--------------------------------------------------------------------------------
/autonomy/loosely-coupled-interface/ui-composition/step5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/autonomy/loosely-coupled-interface/ui-composition/step5.png
--------------------------------------------------------------------------------
/autonomy/loosely-coupled-interface/ui-composition/ui-composition.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/autonomy/loosely-coupled-interface/ui-composition/ui-composition.png
--------------------------------------------------------------------------------
/autonomy/loosely-coupled-interface/virtual-file-system/README.md:
--------------------------------------------------------------------------------
1 | 假设有这么一个虚构的故障检测与分析的业务, 其模块构成如下
2 |
3 | 
4 |
5 | 从网络探针,主机Agent等多个渠道,都会有探测器不断上报到消息队列里。掉底watchdog订阅消息队列,监控是否有数据中断的情况。盗刷风控则是反作弊团队维护的规则引擎,有复杂事件的检测能力。数据汇总给故障分析模块,进行统一的根因分析。为了协助故障分析,前置了一个通用异常检测模块,去筛选出正常事件流里的异常特征,例如同环比的突变等。
6 |
7 | 假设有一个需求是要支持指标的一定程度的延迟和乱序。比如SSO平台事件可能会稍微晚30s才上报过来。为了支持事件流到达顺序的不一致,掉底watchdog需要添加一个缓冲区,晚一点才做掉底的识别。盗刷风控也需要加一个缓冲区,通用异常检测也需要加缓冲区。所有消费事件的处理模块都需要做一遍修改。各个处理的时间也可能不同,所以最终到达故障分析模块的时间也可能不同,所以故障分析模块也需要加一个缓冲区来汇总。
8 |
9 | 假设又有一个需求,网络探针模块进行了升级,从 protobuf 的序列化格式,修改成了 thrift 格式的。虽然掉底 watchdog 和盗刷风控都是订阅的消息队列,但是对消息的格式的变化仍然需要修改这些下游模块。即便这个消息格式的变化引入的新数据对掉底 watchdog 来说毫无价值,仍然需要修改掉底 watchdog。其实从掉底与否的角度来说,你报了就是报了,没报就是没报,对报文内容是完全不关心的。
10 |
11 | 
12 |
13 | 如果我们把依赖关系翻转过来,从 push 改成 pull 则会方便很多。所有的事件上报都在上报之前就统一成同样的格式。引入一个 window 模块把最近一段时间内的事件存入多维分析的数据库里。
14 |
15 | 对于掉底watchdog来说,基本上永远不用修改了。就是定时查询一下 window 模块,看看是不是有数据中断就可以了。
16 |
17 | 对于盗刷风控来说,可以把自己的内部 buffer 给去掉了。由 window 模块统一屏蔽了事件上报的乱序和延迟问题。查询的维度和指标都是按照业务需求来确定的,不相关的上报格式的修改,只要不牵涉到关心的维度,就可以不修改盗刷风控模块。
18 |
19 | 对于故障分析来说,之前是被动接收事件
20 |
21 | ```typescript
22 | onNetworkAnomaly(site, floor, segment) {
23 | }
24 | onClusterDown(cluster) {
25 | }
26 | ```
27 |
28 | 所有的事件都是已经分析出来的结论,计算已经做完了。提供过来的信息就是所有能提供的信息。
29 | 而把依赖关系反转过来之后,就是故障分析根据自己所需要的信息去查询:
30 |
31 | ```typescript
32 | queryAnomaly(target, anomalyType)
33 | ```
34 |
35 | 这样做的好处是可以最小化依赖。如果故障分析不需要的信息,就可以不会被使用到。
36 | 假设上游提供了一个目录的数据供查询。下游根据实际需要决定的查询的路径,比如 `readFile('site1/cluster1/anomaly')` 。也就是基于 pull 的策略,来按需决定依赖。那么没有使用到的路径,比如 `site2/cluster3/anomaly` 改动了也不会有影响。
37 | 而且还有可能,这个计算是按需触发的,也就是你去调用 `queryAnomaly` 的时候现算的。就算是你使用的是文件系统的 api,比如 `readFile`,linux 也有 FUSE 这样的文件系统,可以把 readFile 转换成例如查询 gmail 邮件这样的网络调用。从数据提供方选择“实现方式”的角度来说,提供 query 性质的接口也给予了实现更大的灵活度。
--------------------------------------------------------------------------------
/autonomy/module-boundary-unchanged/alibaba-middle-office-technology/README.md:
--------------------------------------------------------------------------------
1 | # 参考资料
2 |
3 | 下面是关于阿里中台的一些公开资料
4 |
5 | ## TMF & 星环
6 |
7 | * 阿里玄难:面向不确定性的软件设计几点思考 https://mp.weixin.qq.com/s/Uc_wJSVQr7sSz2js2pb3OA
8 | * 如何实现32.5万笔/秒的交易峰值?阿里交易系统TMF2.0技术揭秘 https://mp.weixin.qq.com/s/Pm9L57iSWhjTE2WqJWrQeg
9 | * 大麦交易融入阿里电商平台之路 https://www.infoq.cn/article/W3Ztwqs9Q4aStbksD0mJ
10 | * 阿里发布业务平台解决方案 预计3年内将帮助业务提升10倍效率 http://www.cww.net.cn/article?id=432702
11 | * 2017双11交易系统TMF2.0技术揭秘,实现全链路管理 https://segmentfault.com/a/1190000012541958
12 |
13 | ## SWAK
14 |
15 | * 闲鱼从零到千万 DAU 的应用架构演进 https://www.infoq.cn/video/aPw3FJXaLwt75zWL8Myz
16 | * 在闲鱼,我们如何用Dart做高效后端开发? https://mp.weixin.qq.com/s/jAD3hacFMVcOv9GnAfCFOw
17 | * 业务代码解构利器--SWAK https://mp.weixin.qq.com/s/iS7QjI-LSOET4cmRGo_Jow
18 | * 老代码多=过度耦合=if else?阿里巴巴工程师这样捋直老代码 https://mp.weixin.qq.com/s/_BvHA2gzSd6CFytcE1ZU0w
19 |
20 | 
21 |
22 | ## NBF
23 |
24 | * NBF:新零售服务开放的 Serverless架构与深度实践 冯微峰 https://myslide.cn/slides/21861# https://time.geekbang.org/dailylesson/detail/100040849
25 | * 为什么它有典型FaaS能力,却是非典型FaaS架构? https://www.jianshu.com/p/716ae8f4d219
26 | * 盒马核心系统架构演进 http://ddd-china.com/look-back-2018.html
27 | * 为了30分钟配送,盒马工程师都有哪些“神操作”? https://juejin.im/entry/6844903800470257678
28 | * 盒马生鲜搜索服务化实践与思考 https://m.yubaibai.com.cn/article/5553042.html
29 |
30 | 
31 | 
32 |
33 | # 祛魅
34 |
35 | 这个例子就是前面的三张图。图没看到的同学,请打开科学上网。
36 | 阿里中台在外边的名头很响。但是一直都挺神秘的,不知道中台到底长什么样子。
37 | 希望你看完这三张图之后可以大概明白阿里中台是一个什么样的解题思路。
38 |
39 | 这么多链接的文章也不会有人去读的,我帮你们高度总结一下阿里中台的思路:
40 |
41 | * 出发点是业务的能力(Capability)的复用,而不是代码的复用。代码之上的运营流程,人员,组织这些都是 Capability 的一部分。
42 | * 落地方案由后台开发人员主导,以 Java 为主要实现语言。
43 | * 最终在代码里体现为中台部门写了一个 Java 框架给业务开发用,主要解决 Java 后台代码里 if/else 杂乱无章的问题。
--------------------------------------------------------------------------------
/autonomy/module-boundary-unchanged/alibaba-middle-office-technology/nbf-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/autonomy/module-boundary-unchanged/alibaba-middle-office-technology/nbf-1.jpg
--------------------------------------------------------------------------------
/autonomy/module-boundary-unchanged/alibaba-middle-office-technology/nbf-2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/autonomy/module-boundary-unchanged/alibaba-middle-office-technology/nbf-2.jpg
--------------------------------------------------------------------------------
/autonomy/module-boundary-unchanged/alibaba-middle-office-technology/swak.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/autonomy/module-boundary-unchanged/alibaba-middle-office-technology/swak.png
--------------------------------------------------------------------------------
/autonomy/module-boundary-unchanged/packaging-deployment/README.md:
--------------------------------------------------------------------------------
1 | 以下是从[链家的网站](https://sz.lianjia.com/tool/esfsf/)上截的几个图。这个工具是给中介帮二手房买卖双方计算税费使用的。
2 |
3 | 
4 | 
5 | 
6 | 
7 |
8 | 其核心逻辑是收集房子,卖家,买家三个业务实体的属性。然后根据当地的法规制度,计算出各种税费的税率,然后算出总共应该交的税费的总表。
9 |
10 | # 单个 React 组件
11 |
12 | 我们可以把所有这些城市的业务逻辑,都实现在一个 React 组件里。
13 |
14 | * 渲染空的表单
15 | * 响应表单的用户输入
16 | * 计算出表单的联动状态
17 | * 响应开始计算按钮
18 | * 实现计算逻辑
19 | * 渲染税费总表和饼图
20 |
21 | React 16 的函数式组件就是一个函数。我们可以认为,整个业务也就是一个超级大的函数。这个函数通过 useState,包含了在页面上的状态。如果我们把 useState 通过 local storage 持久化,可以认为这是一个包含了数据库的,前后端一体的完整应用程序。如果我们再把这个界面给买家,卖家分别使用,各自填写自己的信息。那么这个函数就实现了一个多角色协作的业务流程。
22 |
23 | # 调整模块边界
24 |
25 | 一个函数包含了
26 |
27 | * 展示界面,业务计算,持久化
28 | * 买家,卖家
29 | * 北京,南京,洛阳……
30 | * 增值税,契税……
31 |
32 | 显然,对这一个 React 组件,我们需要进行模块化拆分。我们可以有多种模块之间边界的选择
33 |
34 | * 一个组件只负责展示,一个函数负责逻辑计算,一个函数负责数据库读写
35 | * 一个组件面向买家,一个组件面向卖家
36 | * 一个组件负责抽象的计算器,每个城市一个实例化的组件装配自己的参数
37 | * 每个税费种类一个模块,然后把税费各自依赖的输入合并成一个大的表单
38 |
39 | 不同的模块边界选择,是基于对于“什么是易变”的不同判断。这样可以尽量把易变的部分隔离到独立的模块里去修改。假设说,我们判断,城市是易变的。希望做到新增一个城市的时候,可以只是新增一个模块。那么,模块拆分之后大概是这个样子:
40 |
41 | ```tsx
42 | function TaxCalculator(props: {
43 | fields: { shouldShowIsOnlyApartment: boolean, ... },
44 | calcTaxRates: () => { qisuiRate: number, zengzhisuiRate: number, ... }
45 | }) {
46 | return
...
47 | }
48 |
49 | function BeijingTaxCalculator() {
50 | function calcBeijingTaxRate(props) {
51 | // ...
52 | }
53 | return
54 | }
55 | ```
56 |
57 | # 调整模块的打包和部署模式
58 |
59 | 调整有两个方向
60 |
61 | * 保持打包部署模式不变,调整模块之间的边界,重新定义接口
62 | * 保持模块边界不变,保持接口不变,但是调整打包和部署模式
63 |
64 | 那么在保持上面定义的模块边界的前提下,我们有哪些打包和部署的选择呢?
65 |
66 | ## 配置化
67 |
68 | 在上面的例子里,fields 和 calcBeijingTaxRates 这两个参数是程序员在源代码编辑的阶段给定的。这种打包和部署模式是以源代码git管理参数的模式。
69 | 我们可以选择把 fields 和 calcBeijingTaxRates,从源代码里提取出来,放到配置系统里。运行时就可以不用改源代码,不用重新走源代码发布系统,而是通过配置发布系统来发布了。
70 |
71 | ## 自助化
72 |
73 | 配置系统往往仍然是一个高风险的操作。可以进一步把这个配置做成图形化界面,由运营人员在运营系统里直接修改。甚至可以提供拖拽的方式,让运营人员去做一些字段顺序调整的事情。再高级一点,可以搞出 lowcode 的概念来。
74 |
75 | ## 静态求值
76 |
77 | 我们知道,如果我们在代码里写了 3 * 5 的话,编译器会在编译的时候直接优化成15,而不是运行时再去计算。我们这里,静态传递了参数
78 |
79 | ```tsx
80 |
81 | ```
82 |
83 | 那编译器也可以进行“静态求值”,就是把 fields 和 calcBeijingTaxRates 代入到函数里,静态生成一个北京版的计算器。这个做法也就是所谓的代码生成。
84 |
85 | ## Feature Branch
86 |
87 | 比静态代码生成更静态的办法就是给每一个城市一个 git 分支,把这个城市的特殊逻辑维护到这个 git 分支里。
88 |
89 | ## 静态链接 / 动态链接 / 微服务
90 |
91 | 常见的 .a 文件和 .so 文件也是打包部署技术,微服务独立一个 RPC 进程也是打包部署技术。这个就不赘述了。
--------------------------------------------------------------------------------
/autonomy/module-boundary-unchanged/packaging-deployment/beijing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/autonomy/module-boundary-unchanged/packaging-deployment/beijing.png
--------------------------------------------------------------------------------
/autonomy/module-boundary-unchanged/packaging-deployment/beijing2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/autonomy/module-boundary-unchanged/packaging-deployment/beijing2.png
--------------------------------------------------------------------------------
/autonomy/module-boundary-unchanged/packaging-deployment/luoyang.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/autonomy/module-boundary-unchanged/packaging-deployment/luoyang.png
--------------------------------------------------------------------------------
/autonomy/module-boundary-unchanged/packaging-deployment/nanjing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/autonomy/module-boundary-unchanged/packaging-deployment/nanjing.png
--------------------------------------------------------------------------------
/autonomy/module-boundary-unchanged/replace-class-with-interface/README.md:
--------------------------------------------------------------------------------
1 | 在一个虚构的电商系统中,我们有如下的模块划分
2 |
3 | 
4 |
5 | 其中购物车实现了价格的计算,包括整个 invoice 的 total,以及每个 line item 的原价和折后价格。而收银台则实现了通过不同的渠道来支付。包括支付宝和微信这些现付渠道。一些企业给员工开通了企业预付的福利,可以在一定的额度内由企业预付,避免员工买完之后再去报销。
6 |
7 | 单单从现有需求来说,购物车和收银台之间是一个非常简单的接口。就像你在星巴克买一杯咖啡一样,结账的时候也就看个总价。所以从收银台的角度来说,购物车你给我这些信息就够了:
8 |
9 | ```typescript
10 | pay(args: {
11 | totalAmount: number;
12 | currency: 'CNY' | 'USD';
13 | })
14 | ```
15 |
16 | 这个接口和是否使用依赖倒置原则,是否使用 interface 隔离是没有关系。比如我们把购物车和收银台之间的直接依赖,改成共同依赖一个“插件化系统”。
17 |
18 | 
19 |
20 | 这个接口可以表达为等价的 interface,只是换了个写法:
21 |
22 | ```typescript
23 | interface Invoice {
24 | totalAmount: number;
25 | currency: 'CNY' | 'USD';
26 | }
27 | ```
28 |
29 | 从依赖关系图的表面上来看,购物车和收银台是没有直接的依赖了的。这个也说明了“依赖关系图”是可以被欺骗的。实际上,因为在业务流程上,购物车和收银台之间的上下游关系,导致两者之间是有牢不可破的数据依赖关系的。
30 |
31 | 比如企业发现员工经常在平台上买个人用的香烟,而不是书籍。所以企业希望,预付款仅限于给员工购买文具和书籍这两个品类下的商品。那么这个信息就必须从购物车传给收银台。
32 |
33 |
34 | ```typescript
35 | interface Invoice {
36 | totalAmountInWenJuAndShuJi: number;
37 | totalAmount: number;
38 | currency: 'CNY' | 'USD';
39 | }
40 | ```
41 |
42 | 然后收银台拿着传过来的额外字段,把账单一份为二,包括可以用预付的部分,和员工自付的部分。但是对于没有开通企业预付的普通消费者,展示的就只有 totalAmount。
43 |
44 | 过了两天,微信突然提升了虚拟物品(唱片之类的)的手续费。平台不想补贴这部分的交易手续费。所以希望在用户选择了用微信支付,且购买了虚拟物品的时候,由用户支付额外的微信渠道手续费。但是不能直接给虚拟物品提价,因为如果选择支付宝支付的话,支付宝的交易手续费还是比较低的。这个时候,购物车就又要改这个接口
45 |
46 |
47 | ```typescript
48 | interface Invoice {
49 | totalAmountInXuNiWuPin: number;
50 | totalAmountInWenJuAndShuJi: number;
51 | totalAmount: number;
52 | currency: 'CNY' | 'USD';
53 | }
54 | ```
55 |
56 | 我们可以看到,虽然我们使用了面向接口设计,使用了依赖倒置原则。但是这些东西都没有办法阻止你一而再,再而三的修改购物车和收银台。依赖倒置确实让购物车避免了依赖微信支付的接口,支付宝支付的接口这些实现细节。从这个意义上来说,依赖倒置是成功了的。但是仍然无法避免购物车依赖 Cashier.protobuf 这个“接口定义”本身。当业务变更导致不仅仅是实现需要修改,接口本身也不得不修改的时候,面向接口编程并无帮助。
57 |
58 | 每次修改都需要双方进行接口商定,以及联合调试。这个还是在收银台仅仅对接了支付渠道的情况下,如果收银台同时还要负责选择优惠券等业务,可以预想,这样的接口修改会更加频繁。
--------------------------------------------------------------------------------
/autonomy/module-boundary-unchanged/replace-class-with-interface/dip.drawio.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/consistency/README.md:
--------------------------------------------------------------------------------
1 | 从 Consistency 的角度来看
2 |
3 | * [模块切分的好坏标准是什么?](#criteria-of-modularization)
4 |
5 | # 模块切分的好坏标准是什么?
6 |
7 | 业务逻辑就是“没有逻辑”。做业务不是搞数学,没有内在的美和对称性。盲目地追求一致性,会抑制业务的创新,是不可取的。
8 | 但是任意的引入不一致性,又会导致无法复用,从而提高成本。关键就在于平衡,所有的不一致性都要有明确的业务收益。
9 |
10 | ## 非功能性需求必须一致
11 |
12 | [【阅读该例子】](./criteria-of-modularization/non-functional-should-be-consistent)
13 |
14 | 这个例子妇孺皆知,无需多言。非功能性,顾名思义就是业务无关的。所以在这里追求一致性,业务也不会有啥好bibi的。
--------------------------------------------------------------------------------
/consistency/criteria-of-modularization/non-functional-should-be-consistent/README.md:
--------------------------------------------------------------------------------
1 | 假设你有一个微服务架构,其中包含了前台业务模块,以及订单模块,策略模块,商家店铺模块。分别采用了 HTTP REST 接口,GRpc 接口,Thrift 接口。所有的 RPC 库都是用编程语言自带的网络库来完成的。其中 HTTP REST 接口更过分,是每一个不同的 URL 都有一份独立的调用代码。
2 |
3 | 这个时候需要来了一个需求,要统计上报接口错误率和延迟。所有协议的所有实现,都要修改一遍,添加上统计上报的代码。这里包括遍历所有的业务方法,找到 HTTP 调用的地方。
4 |
5 | 然后又来了一个需求,要对接口进行容错,对故障节点进行自动摘除。然后又要全文遍历一遍。
6 |
7 | 然后又来了一个需求,因为下游接入了弹性的云服务,不能使用域名进行服务发现,因为端口号会变化。于是又要改一遍所有的地方加上服务发现的代码。
8 |
9 | 然后又来了一个需求,需要对 RPC 调用进行染色,支持流量镜像等。需要在 RPC 协议中都加入一个 trace id 以及透传的字段,于是所有的 RPC 调用都要改一遍。
10 |
11 | 我们可以总结出以下规律:
12 |
13 | * 非功能性需求有很强的复用性,不仅是可以独立出来,而且是必须独立出来。
14 | * 破坏了 consistency,导致的就是重复实现,重复修改
15 | * 本质上仍然是分工问题,破坏了 autonomy。如果所有的 RPC 框架都由基础架构负责,稳定性也有基础架构负责。那么可以由基础架构团队一方自主完成所有的工作。
--------------------------------------------------------------------------------
/docs/CNAME:
--------------------------------------------------------------------------------
1 | autonomy.design
--------------------------------------------------------------------------------
/docs/Modules.md:
--------------------------------------------------------------------------------
1 | 拆分是任意的。随意给一个标准,我们都可以把任何东西按照这个标准给拆了。以下是我给的拆分角度:
2 |
3 | 业务逻辑可以被拆分成:
4 |
5 | * 编辑时:拆分成文件、文件夹、Git仓库
6 | * 运行时:拆分成进程
7 |
8 | 这个定义当然不是啥金科玉律。但是它在2020这个时间点的科技树而言,对于大部分开发者来说都是适用的。
9 |
10 | # 文件、文件夹、Git仓库
11 |
12 | 这种拆分标准屏蔽了不同编程语言和编程框架的影响,我们不用争论什么是Class,什么是Module,什么是Package。
13 |
14 | * 文件:大部分编程语言都使用了文本文件。只有非常少量的编程语言有自己的 Projectional Editor,这些语言的开发者面对的编辑界面和文件上存储的内容需要通过 IDE 进行映射。
15 | * 文件夹:绝大部分开发者使用的操作系统,对于每一个文件都有所存放的文件夹的概念。一个文件从属于唯一的一个文件夹。部分操作系统在从属关系之外,还有链接,把两个文件建立关联。文件夹与文件夹呈树状组织。
16 | * Git仓库:绝大部分公司会把代码拆分成多个Git仓库。Google等公司没有采用多Git仓库,而是全公司所有人在一个巨型的monorepo上工作。大部分公司切分Git仓库的主要目的是控制访问和版本。不同的开发者控制不同的Git仓库,从这个仓库下的代码产出整个仓库意义下的版本。
17 |
18 | 业务逻辑在编辑时需要拆分成文件、文件夹、以及Git仓库。那么从Autonomy,Consistency,Feedback这三个角度,如何评价拆分出来的结果,又有哪些拆分模式呢?
19 |
20 | # 进程
21 |
22 | 这种拆分标准屏蔽了不同运行环境的影响,无论是 iOS 设备上的 App,还是 apple.com 这个域名背后的服务器,软件总是运行在进程里的。
23 |
24 | * 手机桌面上的App图标:从用户的角度来说,两个图标就是两个App,是两个不同的东西。
25 | * 网站的域名:对于用户的角度来说,两个域名就是两个网站。
26 | * 网络游戏:对于用户的角度来说,我们一起刷的副本,那就是同一个游戏。
27 |
28 | 运行时的表面现象太多了。我们可以随意给一个标准,比如两个界面有什么本质不同呢? 我们可以说不同 URL 的界面是两个“页面”。但是也完全可以 URL 不变,使得界面处于完全不同的两个状态。这样的情况下,这算两个页面还是同一个页面呢? 所以避免拆分出来的东西见仁见智,我们只能选择不那么用户可见,但是又相对稳定的东西。想来想去,也就是进程是歧义比较少的东西。
29 |
30 | # 业务逻辑拆分模式是关于什么的
31 |
32 | * 如何拆分文件,文件夹,Git仓库:目标是[“代码防腐”](./Part1/README.md)
33 | * 如何拆分进程:目标是[“只负责自己写的代码”](./Part2/README.md)
34 | * 如何分层:这个其实也是分 Git 仓库,目标是[“突出大逻辑,隐藏小细节”](./Part3/README.md)
35 |
36 | 相比堆砌 MicroService,Domain Layer 这样的辞藻,更能穿透表面现象,看到事物的本质。到头来,总归是文件,文件夹,Git仓库和进程这些东西。
--------------------------------------------------------------------------------
/docs/Part1/AmazonExample/README.md:
--------------------------------------------------------------------------------
1 | 下面这个例子来自于 Finding your service boundaries - a practical guide - Adam Ralph (https://www.youtube.com/watch?v=tVnIUZbsxWI&t=2482s)。
2 | 以下为中文译本
3 |
4 | 我相信大家都在亚马逊之类的网站上买过东西。当你在亚马逊上买东西的时候就开始了一个工作流。第一步,下单
5 |
6 | 
7 |
8 | 然后选择你的送货地址
9 |
10 | 
11 |
12 | 选择使用的信用卡
13 |
14 | 
15 |
16 | 再选择送货方式
17 |
18 | 
19 |
20 | 再确认
21 |
22 | 
23 |
24 | 直到你最终完成整个下单过程。
25 | 我们都知道亚马逊不是一个 Monolith。
26 | 它也许是世界上最 Service-oriented 的公司了。
27 | 亚马逊发布的时候不会打包成一个T的应用,然后一起发布。
28 | 所以你可以假设在这个工作流中是有很多个Service参与的,它们被某种形式编排,从而使得流程变成这个样子。
29 | 那么在你下单的时候,我们看一下后端的会有哪些 Event。
30 | 它们可能是这个样子的:
31 |
32 | 
33 |
34 | 有 sales,finance,shipping 三个 service。
35 | 当我们点击确认按钮的时候,command 被发往 sales,它可以做它想做的任何逻辑。比如检查所购商品是不是还有货,如果还在售但是库存不足了,我们可以往供应商再追加一笔采购单等。最终,sales 说,我搞定了,到下一步吧。
36 |
37 | 订单被创建出来,然后发布一个 event 给 finance 和 shipping。
38 | finance 收到这个 event 的时候,它可以为这个订单收取客户的费用。
39 | 它接着可能会发一个 order billed 的事件。
40 | 然后 shipping 知道订单创建了并且被 billed 了,它就可以发货了。
41 | 发货完成之后,再发一个 shipped 事件出来。
42 |
43 | 问题是:这些 event 都包含什么字段呢? 是不是 order placed 事件需要包含 shipping 所需的所有字段呢? 要不然 shipping 咋知道发往哪里。
44 | 是不是事件需要包含 finance 像客户收费所需的所有信息呢? 比如客户的信用卡详情之类的东西。
45 |
46 | 这么搞是可以行得通的。但是这种方式带来的问题是“耦合”。
47 | 如果事件需要包含送货方式,信用卡详情等所有信息,
48 | 我们又回到了高度耦合的被动场面里了。
49 | 我们又不能以独立的方式来修改系统行为了。
50 |
51 | 假设我们要开始支持用比特币付费了,我们需要给 sales 增加关于如何用比特币收费的信息。所以 sales 可以把这些信息再传给 finance。
52 |
53 | 如果我们要开始售卖电子书了,我们不希望买电子书也提供一个送货的门牌号。
54 | 所以我们又要修改 sales,从而它可以把电子书的信息传给 shipping。
55 |
56 | 上面这样的 event 就是所谓的 fat event。
57 | 可以看到,如果我们要开始卖电子书,或者收取比特币,这些 event 会变得越来越 fat,字段越来越多。
58 | 在某些时候,一些字段允许为 null,在其他场景下,这些字段又是必填的。
59 | 当它们是 null 的时候,事情就变麻烦了。
60 | 那我们如何把 fat event 重新又变回 thin event 呢? 如何让系统变得更松耦合呢?
61 |
62 | 如果我们在这些 event 上只包含 id 会怎么样?
63 | 如果 sales 在 order placed 的事件里仅仅说 order123 下单了。
64 | finance 收到 order123 之后,它已经知道如何对这个订单收费了。
65 | 它要么是用信用卡收费,要么是开始一个比特币的交易。
66 | 接下来 finance 告诉 shipping,order123 已经被 billed 了。
67 | shipping 收到 order123 之后,它知道要把这个订单配送到一个物理的门牌地址,
68 |
69 | finance 和 shipping 是怎么知道如何处理 order123 的呢?
70 |
71 | 
72 |
73 | 如上图所示,
74 | 这种 thin event 可以行得通的前提就在于每个 service 都有自己的 ui,它们可以直接从用户那里得到自己关心的数据。但是这种方法有两个问题
75 |
76 | 第一个问题是:如果订单要 sales 处理完之后才创建,那么 finance 和 shipping 的界面把自己的数据存到哪里呢? finance 和 shipping 怎么能在订单创建之前就知道订单在哪里呢? 这不就是先有鸡,还是先有蛋的问题了吗?
77 |
78 | 解决办法就是我们要在这个工作流的开始就把 order id 给生成出来,也就是这里
79 |
80 | 
81 |
82 | 甚至我们可以直接在网页上用 uuid 生成这个 order id。这使得在工作流的后面的步骤里,shipping 你要发到这个地址,订单id 是 123.
83 | 在finance那一步,你要从这个选择信用卡收费,订单id 是123。
84 | 直到工作流的最后异步,你才把 command 发给 sales,确认订单。
85 |
86 | 这样如果你想要支持比特币付款,我们仅仅只需要修改 finance。
87 | 因为 finance 收到 order123 的时候,它已经知道了该怎么收费了。
88 | 类似的,如果我要支持电子书的线上发货,我们也不需要修改 sales 了,我们可以把改动局限在 shipping 内部。
89 | 这么搞唯一诡异的地方就是要在流程的开端就把 order id 生成出来。
90 |
91 | 第二个问题:如果你把这么几步都填完了,在最后快下单的时候,你老婆从房里出来让你别剁手了,你不得不取消订单。你关闭了浏览器。这个时候,你填的收货地址,你选择的信用卡信息都卡在 finance 和 shipping 的手里,没法往下走了。
92 | 这些未完成的订单不算是浪费存储资源,不算是垃圾么?
93 | 垃圾只是放错了位置的资源。商家也许非常希望知道是哪些商品被选择了,但是没有完成下单的。
94 | 他们可以拿这些数据去优化购物下单的转化流程。这些数据都是非常有分析价值的。
95 | 而且你也知道,这些数据不过是数据库里的一行而已,也没有多少人会关心这些数据会带来多少开销。
96 | 而且也未必是要落盘到数据库,比如信用卡详情之类的东西,因为其敏感性,也许放在 session 里是更恰当的。这样当订单被取消的时候,这些 session 里的信息也自然就没有了。
97 |
98 | 在这个例子里,我们可以看到。模块与模块之间有两个集成方式
99 |
100 | * 在同一个界面里,左边渲染 sales,右边渲染 billing。两个模块通过共享同一块屏幕的方式实现了基于 UI 的集成
101 | * sales => finance => shipping 的事件里只包含了 id
102 |
103 | 在这个 UI 集成的例子里,我们可以再展开一下。如果,产品的需求是这样的:
104 |
105 | 
106 |
107 | 这种情况下,配送方式和收费方式就不再是独立有两个保存按钮了。它们要在点确认按钮的时候统一保存,而且要在同一个弹框里反馈保存成功与否。在这种情况下配送方式和收费方式就不能仅仅提供
108 |
109 | ```
110 | render()
111 | ```
112 |
113 | 这么一个集成接口。还需要提供
114 |
115 | ```
116 | render()
117 | save(): err
118 | renderError(err)
119 | ```
120 |
121 | 注入这样的更多的接口出来,才能在 UI 上拼装出所需要的产品体验。但即便是这样,像支持比特币付款这样的需求改动,我们仍然是可以封闭在 finance 模块内部来完成。
122 |
--------------------------------------------------------------------------------
/docs/Part1/AmazonExample/proceed-to-checkout.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/docs/Part1/AmazonExample/proceed-to-checkout.png
--------------------------------------------------------------------------------
/docs/Part1/AmazonExample/step1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/docs/Part1/AmazonExample/step1.png
--------------------------------------------------------------------------------
/docs/Part1/AmazonExample/step2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/docs/Part1/AmazonExample/step2.png
--------------------------------------------------------------------------------
/docs/Part1/AmazonExample/step3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/docs/Part1/AmazonExample/step3.png
--------------------------------------------------------------------------------
/docs/Part1/AmazonExample/step4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/docs/Part1/AmazonExample/step4.png
--------------------------------------------------------------------------------
/docs/Part1/AmazonExample/step5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/docs/Part1/AmazonExample/step5.png
--------------------------------------------------------------------------------
/docs/Part1/AmazonExample/ui-composition.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/docs/Part1/AmazonExample/ui-composition.png
--------------------------------------------------------------------------------
/docs/Part1/AutonomyMetrics.md:
--------------------------------------------------------------------------------
1 | 我们担心这样的症状:
2 |
3 | * 沟通多:做新需求很难,因为需要牵涉到很多的团队,要和大量的人去沟通才能把需求落地。
4 | * 需求做了就删不掉:一旦需求做进去了之后,即便愿意把这个功能下线也非常困难。遗留代码日积月累。
5 |
6 | Autonomy 的愿景就是尽量减轻上述的症状,让拆分出来的代码更独立(更具有Autonomy)。从而新需求需要的沟通可以更少,不需要的功能也可以比较容易被干掉。
7 |
8 | 假定业务逻辑肯定要拆分,拆分成
9 |
10 | * 文件
11 | * 文件夹
12 | * Git仓库
13 |
14 | 以 Autonomy 为目标的话,我们可以很容易判断 Git 仓库是主要的着手点。为了减少沟通,每个块业务和产品经理,应该有个对口的 Git 仓库。不选择文件和文件夹的主要原因是 Git 仓库一般对应了编程语言的 Package 的概念,如果是 JavaScript 的话,对应的有 package.json 文件。使用 Git 仓库比较容易依赖编译器进行依赖检查。而选择了文件或者文件夹,则很容易变成表面上拆开了,但是仍然有调用关系,实际上仍然和写在一起没有区别。
15 |
16 | 度量 Autonomy 有如下指标
17 |
18 | # 会议时间
19 |
20 | 如果 Autonomy 的问题是高沟通成本,那么是否可以直接度量整个沟通成本。例如参与会议的时间,这是一个可能的指标。这样的指标会有什么问题呢? 有没有更好的指标?
21 |
22 | 度量会议时间会有如下的问题:
23 |
24 | * 会议没有包含全部的沟通成本,包括面对面沟通,IM沟通等
25 | * 不开会可能是因为没有新需求
26 | * 开会多可能就是因为需求多,说明业务蒸蒸日上
27 | * 开会多少只说明了成本的高低,只要最终的效能好不就 OK 了么? 或者换句话说,只要业务赚钱不就 OK 了么?
28 |
29 | 最有效的当然是总收入,总利润这样的结果指标。但是不能一竿子断定所有的过程指标都没有意义。不可能所有人都背最终的结果指标,也不可能所有的改进都要从结果指标入手。会议时间显然能说明成本构成,只是这个成本是否花得值很难判断。一个劳动者只有8个小时的符合劳动法的工作时间,如果有7个小时都在会议上,显然能够说明一些问题。所以我认为会议时间做为观察性的过程指标还是有一定意义,主要的作用是划个红线,超过了红线说明会议太多了。
30 |
31 | 会议时间这个指标的问题在于无法指导改进。因为开会多,可能仅仅是需求变多了。
32 |
33 | # “接口改动” / “实现改动” 比率
34 |
35 | 我们把Git仓库内的文件分为两种类型,负责接口的文件和负责实现的文件。如下图所示
36 |
37 | 
38 |
39 | 接口文件不一定只有 RPC,任何形式的跨 Git 仓库的依赖都算接口。这里 https://connascence.io/ 有一个非常详尽的 checklist。
40 | 理想的情况下,应该尽可能少的改接口,而是主要去改实现,这样才能减少跨Git仓库的人员沟通。如果一个新需求,需要同时改动N个Git仓库。但是只要不需要改动接口(包括用 `Map` 这样形式搞的隐式接口),仍然是理想的情况。虽然产品经理需要和多个团队沟通每个部分的需求是什么,但是开发团队之间的沟通仍然可以比较少。要每个新需求都只改动一个Git仓库由一个团队负责。或者说每个“产品”都仅由一个团队端到端负责。我认为这是不太现实的:
41 |
42 | * 新商业玩法往往是破坏性的。我们不要去做提前预测
43 | * 需求大小是任意的,产品经理分工也是有随机性的。总是有办法把一个需求弄大到全公司只做这么一个需求的地步
44 | * 啥叫一个产品,啥叫端到端,各种解读都行
45 |
46 | 接口完全不修改,开发人员之间完全不沟通也是不可能的。我们要关注的是目前的业务逻辑拆分是不是合理,多个 Git 仓库之间的接口如果需要频繁调整,那么说明 Git 仓库是不是分得过多了,或者边界不是最佳的。要根据新的输入,不断去审视过去做过的拆分决策。而 “接口改动” / “实现改动” 比率可以量化目前业务逻辑拆分是否让每个 Git 仓库有多少 Autonomy。这个值越小,说明仅改动实现情况占比越高,自主性做得就越好。
47 |
48 | 为了数据统计比较稳定:
49 |
50 | * 仅做到文件级别的区别。一个文件要么属于接口,要么属于实现。一般通过技术手段都可以做到这样的隔离。
51 | * 一天无论改了多少次,改了多少个文件都记为“1次”改动。这样避免了分多次提交,或者文件数量多寡引起的数据波动。
52 |
53 | 极端情况下,我们可以不分 Git 仓库,或者只有两个 Git 仓库,从而让 “接口改动” / “实现改动” 比率比较好看。这个也说明了分 Git 仓库的成本。把业务逻辑拆得越碎,必然会导致跨团队的沟通会上升。Git 仓库不是分得越多就越好,而是满足了团队的并发数就可以了。
54 |
55 | 这个指标的另外一个问题是日常性的文案修改会导致实现改动非常多。所以我们要以“Consistency”维度的指标去平衡。假设我们已经有了一种统一的文案配置机制。那么需要有一个“文案配置机制”接入率的指标。这样就可以避免日常性的例行修改破坏这个指标的真实性。
56 |
57 | [《A Philosophy of Software Design》](https://www.amazon.com/Philosophy-Software-Design-John-Ousterhout/dp/1732102201) 很重要的一个观点就是 "Modules should be deep",这样的隐喻让人们把注意力放在了静态的结构上。其实作者的本意是接口如果比实现要小很多的话,接口被修改相对于实现被修改的概率也就小了很多。这样我们大部分时候就可以只改实现,而不改接口。“业务逻辑拆分”其成本和收益都要在接下来做新需求的过程中体现,抽离了业务变更的时间轴,静态的代码结构无法度量其好坏。
58 |
59 | # 持久状态封装度
60 |
61 | * 把变量的静态类型分为两类,这个类型是持久状态,这个类型是非持久状态
62 | * 持久状态是相对而言的,如果一个地图软件,在导航过程的一个小时内都会在内存中持有的状态,我们仍然把其分类为持“久”状态。
63 | * 持久状态封装度,度量的目的是考察这些变量多大程度是真正的“全局变量”
64 | * 持久状态封装度 = 可以引用到该类型的代码行数 / 总代码行数,该比值越小越好,如果为 1 则代表 100% 全局变量
65 | * 切分静态编译单元,通过模块间依赖倒置,就是为了让持久状态能被隐藏起来无法被引用到(也就是封装)
66 |
67 |
68 | 和其他 autonomy metrics 的关系,持久状态封装度 => “接口改动” / “实现改动” 比率 => 会议时间
69 |
70 | 也就是我们观察到总是开会是表象,“接口改动” / “实现改动” 比率是原因,而持久状态封装度则是滋生问题的温床。道理很简单,就是一个状态越能被全局引用,就越可能被全局滥用。
71 |
72 | 在 local variable 和 global variable 之间应该有一个中间态。而不是一旦这个变量无法局部在一个函数内部了,就把其变成 100% 全局可以读写,甚至是可以跨线程读写。这里面可以控制 100% 这个比例,可以控制是可读还是可写,也可以控制是否是线程内可以读写。如无必要,勿扩范围。
73 |
--------------------------------------------------------------------------------
/docs/Part1/Composition-1.drawio.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/Part1/Composition.md:
--------------------------------------------------------------------------------
1 | # 相加的关系
2 |
3 | 显然我们可以把业务逻辑分解成如下的多个Git仓库
4 |
5 | 
6 |
7 | 这些一次性的Git仓库会相加到一起,就构成了完整的业务逻辑。
8 |
9 | # 相乘的关系
10 |
11 | 然后我们会意识到,有一些重复的模式。
12 | 比如说这后台的表单都长得差不多啊,我们把表单的校验和错误回显封装个UI组件吧。
13 | 然后所有的后台表单都必须复用这个UI组件。
14 | 这样为了保障实现的一致性,就引入了相乘的关系
15 |
16 | 
17 |
18 | 我们可以完全没有相乘的组合关系,而完全依赖相加来组合。但是相乘的组合关系通过只需要维护一份,使得一致性可以更好。从实现技术上来说,相乘的关系可能是运行时加载 Library 复用其函数,也可能是编译时的模板展开,代码生成。与"相加的关系"的主要区别是被复用多次,而不是用什么形式被调用。
19 |
20 | # 一致性
21 |
22 | 假设我们把业务拆分成了一堆 Git 仓库,它们之间构成如下的依赖关系
23 |
24 | 
25 |
26 | 在整个依赖关系树上面,我们可以画一根线。线之上的 Git 仓库定位就是一次性的,通过“相加”组合满足特定的业务需求。
27 | 通俗而言我们会称之为为“业务”。
28 | 对于业务我们仅仅要求 Autonomy 和 Feedback 的指标。
29 |
30 | 线之下的 Git 仓库定位是“保障整体的一致性”,是“相乘”的组合关系。
31 | 通俗而言我们会称之为“基础设施”。
32 | 除了要求 Autonomy 和 Feedback,还额外增加了 Consistency 的度量指标。
33 | 这些 Consistency 指标的主要目的就是防止过度抽象,什么都想“可复用”,导致 Autonomy 受损。
34 | 可以说这些 Consistency 指标实际上是度量了“可复用性的质量”。
35 |
36 | 实际上这些“基础设施”,与编程语言,操作系统,物理硬件能有多少区别呢?
37 | 区别仅仅是“开源社区共识”,还是“公司部门范围内的共识”罢了。
38 | 有野心一点的公司,或者业务模式内在一致性比较高的公司,大可以发明自己的领域特定语言(Domain Specific Language)嘛。
39 | 只要这个发明出来的 DSL 不是只适用于一两个地方,能够通过 Consistency 指标的检验,那就是有价值的。
40 |
--------------------------------------------------------------------------------
/docs/Part1/Consensus.md:
--------------------------------------------------------------------------------
1 | # 真的一点提前设计都不需要吗?
2 |
3 | 是的,需求是不可预测的,不要基于臆想的需求提前设计。
4 |
5 | 不是的,我们需要在开工之前先取得共识。共识应该包含两部分内容:
6 |
7 | * 我们总是要做出一个“如何分工”的决定,才能开始工作。没有完美的决定,Autonomy/Consistency/Feedback 三个方面总是有被取舍掉的方面。但是我们要对“取舍了什么”达成共识。
8 | * 敏捷开发需要我们持续地调整,持续地去响应变化。“如何保持敏捷”同样需要计划,需要达成共识。
9 |
10 | 在实践中,我们经常盲目地采取某种架构风格,比如说微服务架构,但是并不能明确地说出这个做法是“取舍了Autonomy/Consistency/Feedback中的哪些”。既不知道是为了获得什么收益,也不知道背后的代价是什么。
11 | 然后一条路走到黑,没有反思的节点,没有项目的复盘。如果我们不承认做 Big Upfront Design 是可能的,那至少要有日常反思的计划。什么症状应该触发大家去调整业务逻辑的拆分?这些症状能不能被量化?能不能排上议程?
12 |
13 | 我们可以有一些习惯做法,这些 bias 让我们可以快速开始
14 |
15 | * 大厂的商业那么成功,那复制他们的技术方案也会使我们商业成功
16 | * things executed in same time, write in same git repository
17 | * things executed in same os process, write in same git repository
18 | * 微服务,devops,每个人都有一个自己的进程
19 | * 共享数据库是邪恶的,每个 CRUD 得包个微服务
20 | * 招人那么难,相同技能的人放一起才能招募更多优秀的人
21 |
22 | 但这些 bias 不应该是盲从的终点,而是思考和试错的起点。如果选择某个切分的策略,需要想想
23 |
24 | * Autonomy 将来怎么保证
25 | * Feedback 将来怎么保证
26 | * Consistency 将来怎么保证
27 |
28 | 我们不需要提前设计,但是要基于共识进行分工。这就是《业务逻辑拆分模式》主旨所在。
29 |
--------------------------------------------------------------------------------
/docs/Part1/Consistency.md:
--------------------------------------------------------------------------------
1 | 我们担心这样的症状:
2 |
3 | * 难以改全:改一下RPC重试策略,需要把所有调用RPC的地方都改一遍
4 | * 用户体验不一致:一个APP有4种不同的 date picker 组件,做的都是选日期这个事情
5 | * 不知道该抄谁:要做一个新界面了,发现类似的界面布局有用 css flexbox 的,有用 grid 的,也有自己拿 margin 算的
6 | * 找不到:可复用的组件也不是我写的,我怎么能知道需要复用你呢?
7 | * 表面形式:咋一看全是复用组件,定睛一看每个组件都要传好多个参数
8 |
9 | Consistency 的愿景是尽量减轻上述的症状,尽量让一件事情只有一种实现。那 Consistency 就是说要做好 Reuse 吗?
10 |
11 | # 复用不复用那是产品经理的工作
12 |
13 | Consistency 和 Reuse 的出发点是不同的。
14 | 我对这两个词的感觉是,Reuse 是从所有代码中找重复,然后努力抽取出可复用的东西。
15 | Consistent 则是我先定义了一个标准,比如说UI规范,然后强制要应用到所有的页面上。如果没有应用上,那就得说明理由,引入的不一致是有意为之,还是偶然的设计失误。
16 | Consistent 隐含了先有共识(Consensus)的含义,就是产品和开发团队达成了什么是必须一致必须复用的共识。
17 | 而 Reuse 总有开发团队一厢情愿的意味在里面,依赖了每个人做新需求的时候去主动地消除重复。
18 |
19 | 以“省事”为理由,强行“复用”一套实现是长久不了的,这个出发点很容易被挑战。
20 | 只要不影响到用户可见的一致性,不影响到 Autonomy,不影响到 Feedback,实现写两遍又如何?
21 | 不要指望通过 reuse 一堆[相乘关系的 git 仓库](./Composition.md)来减少代码量,这不值得。产品经理设计出来的需求如果是没有规律性的,别强行复用。能用相加的关系组合好一堆 git 仓库就不错了。没有啥好 trade off 的,就是 autonomy 为先。打工人都希望做一些有leverage的事情,都希望多做一些可复用,可被渊源流传的事情,然而人要接受现实。我们业务开发和装修工人没啥区别,按面积收费的,涂一面墙,也就是影响了这一面墙。
22 |
23 | # 如果不是为了复用,那为什么还要 Consistency?
24 |
25 | 以下三个出发点
26 |
27 | * 对用户可见的一致性:显然色系,交互模式这些东西要保持一致。
28 | * 为了优化 Autonomy:依赖倒置是建立在一套共识的规范上的,可能会要求使用同样的编程语言,同样的工具链。
29 | * 为了优化 Feedback:响应故障的时候还要去看这个模块自研的监控系统,那就太耽误时间了。
30 |
31 | Consistency 就是业务的底座,业务的榫卯。无论是 Consistency 还是 Feedback,都是为了实现 Autonomy。
32 |
33 | # 一致性的推动力
34 |
35 | 通过把所有前端开发放入同一个部门,用组织架构来强化一致性也是长久不了的。
36 | 组织容易因为地域、时区而分开,因为人员扩张而容纳不下,因为各种人为因素而左右。
37 | 靠把所有人装入一个组织下来保障一致性的目标是不现实的。
38 |
39 | 靠一两个人跨越部门来做准入,也是不靠谱的。且不说这一两个人说话是不是算话,这两个人也很容易变成瓶颈。
40 |
41 | 推动一致性靠以下三方面的力量
42 |
43 | * 用户可见的一致性:靠 UI/UE 和产品设计者。他们对于视觉上不一致,交互方式上的不一致都会有相对比较高的标准。然后靠前端组件团队来落地。
44 | * 优化 Autonomy:靠上层业务的 stakeholder 提供影响力。他们是最有动力各搞各的。
45 | * 优化 Feedback:靠QA、研发效能、基础架构和SRE部门。这些部门直接负责了监控和变更的工作,Feedback 的指标本来就是部门的 KPI。
46 |
47 | 日常的一致性检查应该尽可能使用自动化检测手段。就像 git 提交的时候检查 lint 规则一样。
48 |
49 | # 场景和指标
50 |
51 | 那么根据过往经验,常见的需要强制 Consistency 的[场景](./Scenario/README.md)有哪些呢?
52 |
53 | 我们如何能度量在 Consistency 这个维度,现在的问题有多严重。或者换句话说,当我们做出了一些改进,如何[度量](./ConsistencyMetrics.md)有改善呢?
--------------------------------------------------------------------------------
/docs/Part1/ConsistencyMetrics.md:
--------------------------------------------------------------------------------
1 | 我们可以把 Git 仓库分为[“相加的关系”和“相乘的关系”](./Composition.md)。
2 |
3 | 一致性指标是对“相乘组合关系”的Git仓库的额外要求,是为了防御常见的设计错误:
4 |
5 | * 过度抽象:强行把一堆不相关的东西拧巴到一起。所以引入了“必要参数占比”和“咨询量”这两个指标。
6 | * 过早抽象:没想清楚的情况下,根据局部的一两处重复就抽出一个可复用的东西。所以引入了“使用次数”,“使用率”和“阻断率”这三个指标。
7 |
8 | # 必要参数占比
9 |
10 | 我们把可复用Git仓库对外提供的函数参数分为两类,必要参数和非必要参数。非必要参数的计算口径是只有 10% 的调用方传递了的参数。
11 |
12 | 
13 |
14 | 指标为必要参数的数量占总参数数量的占比。为了抽取出可复用的模块是不是做得过度了?是不是把一些小众场景的具体业务也以额外参数的方式 pull down 到可复用Git仓库里了。
15 | 每一个额外参数都增加了调用者的负担,是额外的学习和维护成本。
16 |
17 | # 咨询量
18 |
19 | 当一个定位为“可复用模块”的团队,是孤单寂寞的。如果被拉去参与了某重点项目的 Feature Team,那是莫大的荣耀。
20 | 但是为了保证 Consistency,可复用的 Git 仓库,应该努力降低使用者的成本。
21 | 使用者的最大成本来自于沟通问询。如果文档不清楚,接入开通方式是手工的,必然会体现在咨询量上。
22 |
23 | 咨询量这个指标怎么计算就很难说得清楚了。在不同的团队里,咨询量的体现方式各有不同。总之这个指标就是越接近零越好。
24 |
25 | # 接入次数
26 |
27 | 使用次数只有1次或者2次,就不应该被抽取成可复用的Git仓库。至少要被使用3次。
28 |
29 | # 接入率
30 |
31 | 不认为去识别“代码重复率”是有意义的指标。代码重复并不一定是问题,很难说今天是一模一样的代码,明天还会保持一模一样。不管三七二十一的“复用”反而可能造成耦合,降低团队自主性(Autonomy)。
32 | 一个典型的反面案例就是 utils 包,utils 类。没有人说得清楚啥时候要用你抽取出来的这个 utils 类,也说不清楚啥时候不应该用。
33 | 如果要抽出可复用的代码,出发点应该是 consistency,是在团队关键成员达成了一致之后的有意识行为。
34 |
35 | 每个可复用的Git仓库,要定义清楚自己的适用范围。在适用范围内需要度量接入率。如果说不清楚啥情况该复用,啥情况不该复用的东西,就应该当成一次性的业务逻辑,不要让其他Git仓库对其产生依赖关系。
36 | 接入率基于代码扫描自动计算,可接入的通过 pattern match 算得,已接入的直接看代码符号的引用关系。
37 |
38 | # 阻断率
39 |
40 | 当我们使用了 Java 这样的编程语言的时候,Java 会阻断你在代码中使用汇编语言直接操纵 CPU。这是比较典型的“阻断”。
41 | 阻断是最强有力地保障一致性的措施。
42 |
43 | 
44 |
45 | 如果使用了 C++ 这样的编程语言,很有可能 Git 仓库之间对什么是一个 string 都没有共识。
46 | 这样就必须要在一定范围内(比如某个项目,某个部门),强制要求所有的 Git 仓库都接入同样的 string 库,从而保证互操作的低摩擦。
47 |
48 | 类似的,一个彼此互相RPC调用的分布式应用,各个进程都用不同的 RPC 协议,用不同的 RPC 实现库来互相通信会导致很多问题。
49 | 例如,A调用B又调用C,一旦调用失败,层层加码地去重试,就可能导致最底层模块被反复重试,最终被击垮。
50 | 解决办法是需要分布式调用链上的所有进程都遵循同样的重试规则。
51 | 如果没有办法“阻断”手撸RPC的实现,看见一个http url,就直接随意找个 http 库去调用,那就很难保证重试规则的一致性。
52 |
53 | 阻断率指所有可接入的地方,有多少处上了强制检查,确保了违规行为会被阻断。
54 |
55 |
--------------------------------------------------------------------------------
/docs/Part1/DependencyInversion/DependencyInversion-1.drawio.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/Part1/Encapsulation.drawio.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/Part1/InformationHiding/README.md:
--------------------------------------------------------------------------------
1 | # 信息隐藏
2 |
3 | 通过信息隐藏,我们把代码分为“可以随便写的”和“不可以随便写的”两部分。从而控制代码腐化的蔓延。
4 |
5 | ## Class 的信息隐藏
6 |
7 | 我们都知道 Class 有一个叫封装的概念。把 Field 和 Method 分为 Public / Private / Protected 三种。通过只暴露 Public 成员,我们就把 Private 和 Protected 成员给隐藏起来了。
8 |
9 | 
10 |
11 | 如果有如下的代码
12 |
13 | ```java
14 | public double Area(object[] shapes) {
15 | double area = 0;
16 | foreach (var shape in shapes) {
17 | if (shape is Rectangle) {
18 | Rectangle rectangle = (Rectangle) shape;
19 | area += rectangle.Width * rectangle.Height;
20 | } else {
21 | Circle circle = (Circle)shape;
22 | area += circle.Radius * circle.Radius * Math.PI;
23 | }
24 | }
25 | return area;
26 | }
27 | ```
28 |
29 | 根据“开闭原则”(Open Closed Principle),好的代码应该尽量 Open for extension,Close for modification。也就是说,如果
30 |
31 | ```java
32 | if (shape is Rectangle) {
33 | // ...
34 | } else if (shape is Circle) {
35 | // ...
36 | } else if
37 | // ...
38 | }
39 | ```
40 |
41 | 这样一直修改这个 if/else 是不好的代码。好的做法是把 Rectangle 的 Width/Height 隐藏起来,把 Circle 的 Radius 也隐藏起来。
42 |
43 | ```java
44 | public double Area(Shape[] shapes) {
45 | double area = 0;
46 | foreach (var shape in shapes) {
47 | area += shape.Area();
48 | }
49 | return area;
50 | }
51 | ```
52 |
53 | 从依赖关系上来看,就是如下图所示
54 |
55 | 
56 |
57 | 这样我们就把代码分为上下两部分。对于 Rectangle,Circle 以及 AreaCalculator 来说,彼此都不知道对方的存在。
58 | 也就是大家都依赖 Shape,但是彼此没有依赖关系。
59 |
60 | ## Git 仓库的信息隐藏
61 |
62 | Class 有封装和依赖关系。Git 仓库也有封装和依赖关系。
63 |
64 | 
65 |
66 | 在上图中,B和C是互相隐藏的。B的实现细节对C隐藏了,C的实现细节对B也隐藏了。
67 | 我们都使用过 visual studio code,其插件化架构就类似上面的依赖关系。[通过新增插件来实现功能的扩展](../VscodeExample/README.md)。
68 |
69 | 我们可以把这种做法更一般的描述为“主板+插件”。
70 |
71 | 
72 |
73 | 容易写出幺蛾子的代码都是集中在一个(主板)Git仓库里的。
74 | 在做 Code Review 的时候,只需要重点观照倒置到底层的集成Git仓库是否合理。
75 | 所谓合理,就是能不改就不改。除非不开槽,不开扩展点,需求在插件中无法实现了。
76 |
77 | “主板+插件”不仅仅可以写 Visual Studio Code 这样的 IDE,对于各种类型的业务系统都是同样适用的。
78 | 只是 VsCode 可能一个插件点上可以有多个插件,而业务系统上一般不会有那么多彼此可替换的插件,更多是一个萝卜一个坑的搞法。
79 | 主板部分一定要尽可能的小,要不然就会变成所有的需求都要堆到主板里去实现了。
80 |
81 | 这种写法和上面的“Class信息隐藏”是不是一回事?从倒置的角度是一回事。但是区别是基于 Class 的依赖倒置缺乏编译器的保障,无法确保插件之间不互相引用。
82 |
83 | 
84 |
85 | 这两个禁止才是关键。
86 |
87 | * 禁止主板Git仓库反向依赖插件Git仓库
88 | * 禁止插件Git仓库之间互相依赖
89 |
90 | ## 如何实践,这玩意真能写业务?
91 |
92 | 理论上看起来很美好,然而有两个问题
93 |
94 | * [主板+插件的技术方案有哪些选择?](../DependencyInversion/README.md)
95 | * [对应具体的业务需求,怎么拆分出主板和插件来?](../Integration/README.md)
96 | * [离散型 UI](../Integration/DiscreteUI/README.md)
97 | * [混合型 UI](../Integration/MixedUI/README.md)
98 | * [离散型流程](../Integration/DiscreteProcess/README.md)
99 | * [混合型流程](../Integration/MixedProcess/README.md)
100 | * [产品族](../Integration/ProductFamily/README.md)
101 | * [领先技术](../Integration/Library/README.md)
102 |
103 | 最后我们来看一个[亚马逊商城的综合案例](../AmazonExample/README.md)。
104 |
105 | ## 不倒置可不可以?
106 |
107 | 是不是不依赖倒置就不能写插件呢?比如,这个样子是不是也是一样的
108 |
109 | 
110 |
111 | 这样不一样可以把代码都写在插件里吗?在最上层加一个“业务编排”,和所谓的“主板”不是同样的概念吗?
112 | 这样的问题是插件与插件之间没有互相的依赖关系怎么能实现业务需求呢?比如在成交了之后分佣,分佣插件怎么知道啥叫成交呢?这种写法的大概率后果是更频繁地需要改动“业务编排”Git仓库,从而使之成为瓶颈。然后又逐步发展成下面这个模式:
113 |
114 | 
115 |
116 | 如果不搞出一个主板,允许彼此插件之间互相调用那会更加混乱。相比上面有一个“业务编排”,下边有一个“主板”。那似乎把“业务编排”去掉更简单一些。
117 |
118 | 那我们只要一个主板吗?并不是这样的,主板的需求来自于 UI 界面的耦合,以及混合流程的耦合。
119 | 如果两个业务需求都有完全独立的界面,流程上可以拆分为事件驱动的,那完全就没有必要插到一个主板上,可以是完全独立的微服务,甚至是独立的产品(独立的SaaS)。
120 | 比如说我们有一个主板承载了商城订单的界面和流程,来了一个淘宝商品搬家的需求。
121 | 这个淘宝商品搬家有独立的UI,只需要商城开个写入商品的接口就可以完成需求。
122 | 类似这样的需求就没有必要商城提供什么插件和主板,然后通过主板来搞什么界面和流程集成。
123 | 简单搞个开放API,需要搬家的时候商品搬家调用一下商城的开放API就好了。
124 | 说到底是业务逻辑内在联系是否紧密,如果当成两个独立产品一样来开发和售卖,那自然是最理想的情况。
125 |
126 | ## 为何信息隐藏可以代码防腐?
127 |
128 | David Parnas 在 1971 年就写了 [On the Criteria To Be Used in Decomposing Systems into Modules](http://sunnyday.mit.edu/16.355/parnas-criteria.html),以上信息隐藏的做法基本上已经得到程序员的认可。但是我们看下图
129 |
130 | 
131 |
132 | 50年来,大量的新人不断地涌入这个行业。如果指望一个项目上所有的人都能遵守很高的标准,能够独立对“好”和“坏”的设计做正确的判断,这是不现实的。并不是说软件工程教育不重要,教育仍然是要做的。但是仅仅靠教育来提高软件工程的质量是不现实的。
133 |
134 | 
135 |
136 | 如上图所示的“信息隐藏”的做法,其实质是为了“代码防腐”。在这样的依赖关系下,插件的 Git 仓库是不会造成全局的影响的。这样我们就可以放心的把新人分配去写一个独立的插件,而不用担心其设计选择造成大面积的代码腐化。Code Review 仅需要集中关照主板。并且评价“高内聚低耦合”的标准也可以用主板的代码行数进行量化(在完成需求的前提下,主板的代码行数越少就是越好)。
137 |
138 | ## 编译期组装主板和插件的实现方案
139 |
140 | 不同的语言和编译工具链,实现方案会有所不同
141 |
142 | * TypeScript + Vite: https://github.com/taowen/vite-ioc-demo/
143 | * TypeScript + NPM: https://github.com/taowen/npm-ioc-demo/
144 | * TODO: 补充更多的语言和编译工具链的实现方案
145 |
--------------------------------------------------------------------------------
/docs/Part1/InformationHiding/YearsOfProfession.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/docs/Part1/InformationHiding/YearsOfProfession.png
--------------------------------------------------------------------------------
/docs/Part1/Integration/DiscreteProcess/README.md:
--------------------------------------------------------------------------------
1 | # 离散型流程
2 |
3 | 例如在订单成交之后,给中介分佣这一类的业务。它们有三个特点
4 |
5 | * 不需要返回值。也就是只需要在时间顺序上,安排在之后发生。
6 | * 不精确要求立即执行。稍微晚个几秒是可以忍受的。
7 | * 不影响事务结果。不因为分佣服务挂了,导致订单无法成交。
8 |
9 | 
10 |
11 | ## 事件驱动
12 |
13 | 订单的 Git 仓库提供 OrderCreated 事件。
14 | 分佣的 Git 仓库依赖订单 Git 仓库,订阅 OrderCreated 事件,在收到事件之后分佣金。
15 |
16 | 订单不能感知到分佣是否成功,也不能在下单成功的界面上立即展示出分佣的数据。
17 | 分佣也无法保证一定是在订单创建之后被立即触发执行的,消息队列仅保证最终会把消息投递出去。
18 |
19 | ## 可降级为事件驱动
20 |
21 | 很多流程需求夹杂了UI的需求,无法变成纯事件驱动的。
22 |
23 | 例如在结束订单的时候,要立即终止计费进程,并显示出账单。如果纯事件驱动的话,界面上的账单数据是拿不到的。
24 | 但是有可能计费进程这个时候异常,可以先在订单上标记好结束时间。
25 | 然后界面上显示“出账中,请耐心等待”。等计费进程恢复之后,再异步结账扣费。
26 |
27 | 这种类型的业务是订单的Git仓库依赖计费的Git仓库,而不是像上面的分佣业务那样,依赖是倒置的。
28 |
29 | 这种实现的伪代码是:
30 |
31 | ```ts
32 | try {
33 | const newUI = finishBilling();
34 | renderUI(newUI);
35 | } catch(e) {
36 | enqueueFinishBilling();
37 | renderUI('出账中,请耐心等待')
38 | }
39 | ```
40 |
41 | ## 界面重刷新
42 |
43 | 上一种实现方式里,finishBilling 同时完成了两件事情
44 |
45 | * 告诉订单结束,我这里搞完了,没问题
46 | * 同时返回了账单
47 |
48 | 渲染账单的逻辑可能是经常易变的,导致 finishBilling 的接口稳定不下来。
49 | 可以把这两个操作分为两步:
50 |
51 | ```ts
52 | try {
53 | finishBilling();
54 | refreshUI();
55 | } catch(e) {
56 | enqueueFinishBilling();
57 | renderUI('出账中,请耐心等待')
58 | }
59 | ```
60 |
61 | 这样,finishBilling 就可以仅仅专注于流程,而不用管 UI 需求。UI 逻辑可以使用离散型 UI 里的各种模式集成进来。
62 |
63 | ## 小结
64 |
65 | 在可能的情况下,纯事件驱动当然是 Autonomy 最佳的方案。奈何产品经理不喜欢异步。
66 | 最好不要在流程代码的接口里包含 UI,这样会使得接口更易变,也就更易于产生额外的沟通成本。
67 | 最好把 UI 需求按照离散型 UI 里描述的集成方式,用更 Autonomy 的方案集成进来。
--------------------------------------------------------------------------------
/docs/Part1/Integration/DiscreteUI/README.md:
--------------------------------------------------------------------------------
1 | # 离散型 UI
2 |
3 | 这一类的需求是展示一个界面。界面上可以非常很多个块。每一块的业务逻辑都可以分离到一个独立的Git仓库里。
4 |
5 | 称之为离散型是因为“离得比较开”,块与块之间非常独立。
6 |
7 | 例如下面这样的界面
8 |
9 | 
10 |
11 | ## 数据集成
12 |
13 | 我们可以使用服务端数据集成的方式来实现这个需求。
14 |
15 | 
16 |
17 | 这种集成方式
18 |
19 | * 从依赖方向上:业务API依赖买家信息,配送信息,付款信息
20 | * 从接口形态上:接口是数据,所有的字段都需要明确定义
21 |
22 | ## UI集成
23 |
24 | 我们可以使用客户端UI集成的方式来实现这个需求。
25 |
26 | 
27 |
28 | 配送信息,付款信息,买家信息的Git仓库同时提供服务端进程,以及客户端的组件。从进程部署上是两个,从Git仓库的角度是一起的。
29 | 这种做从一端到另一端贯通的做法,一般也称为“竖切”。
30 |
31 | 这种集成方式
32 |
33 | * 从依赖方向上:和上面的数据集成一样,业务API依赖买家信息,配送信息,付款信息
34 | * 从接口形态上:接口是UI,买家信息 Git 仓库对外提供的接口是一个 UI 组件。业务客户端 Git 仓库不需要知道这个 UI 组件会渲染什么字段。
35 |
36 | ## 数据当成UI来集成
37 |
38 | 虽然在服务端做集成,但是把接口定义成UI一样的黑盒。
39 |
40 | 例如,这样的伪代码
41 |
42 | ```ts
43 | function getOrderDetail(): Record {
44 | return {
45 | buyer: getBuyerDetail(),
46 | fulfilment: getFulfilmentDetail(),
47 | payment: getPaymentDetail()
48 | }
49 | }
50 | ```
51 |
52 | 数据在服务端完成聚合,但是聚合的过程是简单的粘合,就像 UI 组件组合成大界面一样。
53 |
54 | 这种集成方式
55 |
56 | * 从依赖方向上:和上面的数据集成一样,业务API依赖买家信息,配送信息,付款信息
57 | * 从接口形态上:接口是当成UI来用的黑盒数据
58 |
59 | ## 依赖倒置的服务端API
60 |
61 | 负责集成的服务端API可以在依赖关系上被倒置
62 |
63 | 
64 |
65 | 服务端API进程在调用关系上不改变。但是从依赖关系上,服务端API的Git仓库不再完成具体业务,而是下沉为底层的“通用数据网关”。
66 |
67 | * 从依赖方向上:通用数据网关变成底层模块,依赖关系倒置。具体的买家信息,配送信息,付款信息成为插件
68 | * 从接口形态上:接口是当成UI来用的黑盒数据
69 |
70 | ## 依赖倒置的客户端
71 |
72 | 负责集成的客户端Git仓库也在依赖关系上被倒置
73 |
74 | 
75 |
76 | 客户端的Git仓库不再完成所有的具体业务,而是留出界面插槽(Slot),由买家信息,配送信息,付款信息在界面上做填充。
77 | 那为何还需要客户端的Git仓库呢,因为仍然需要说明,买家信息,配送新信息,付款信息在同一个页面上的相对布局。
78 |
79 | * 从依赖方向上:买家信息,配送信息,付款信息在最顶层。客户端的Git仓库倒置到底层,只处理业务之间的布局,也就是描述这些业务如何集成到一起。
80 | * 从接口形态上:接口是黑盒的UI
81 |
82 | ## 规范型 UI
83 |
84 | 离散型 UI 如果其界面有规律,例如我们从某打车软件的截图来看:
85 |
86 | 
87 |
88 | 似乎可以抽象成规则性组件的重复。那我们可以把规范的条目数据定义成接口,然后使用依赖倒置。统一由订单列表来把规范的条目数据读取出来,渲染到界面上。
89 |
90 | 
91 |
92 | * 从依赖方向上:订单条目的 Git 仓库位于依赖关系的底层。快车,专车,顺风车依赖订单条目的 Git 仓库,实现其接口。订单列表依赖订单条目,负责渲染。
93 | * 从接口形态上:接口是规范型的数据,订单条目的 Git 仓库定义了这个数据结构的每个字段
94 |
95 | 读取订单列表的时候用 RPC 聚合,也可以是订单列表提供一个数据库,由每个业务线负责写入。这两种做法不改变 Git 仓库之间的依赖关系,从 Autonomy 的角度来说也是一样的。
96 |
97 | 
98 |
99 | ## 小结
100 |
101 | 从 Autonomy 的角度来说,使用 UI 做为接口,比使用数据做为接口更能自主变化。
102 | 因为展示细节变了,拿 UI 做为接口的情况,接口大概率是不需要修改的。
103 | 除非展示需求正好改的是界面布局这样的大尺度上的东西。
104 |
105 | 如果不使用依赖倒置,每个新需求写在哪个 Git 仓库里,会有一些争议性。比如用数据集成的方式来做这个界面,我们可以在业务API这个Git仓库里做所有的需求,因为它有所有的数据。
106 | 那配送信息要做一些日期格式化的调整是去改业务API的Git仓库呢,还是改配送信息的Git仓库呢。
107 | 在使用依赖倒置的情况下,接口需要显式地提炼出来,并有意去最小化接口部分。
108 | 从而可以让更多的需求改动封闭在更上层的依赖关系里完成,更多去改实现代码,而不是改接口代码。
109 |
110 | 拿UI做接口,依赖倒置,目的都是让接口改得少。从而使得跨团队的沟通量变小,提高 Autonomy。
111 |
--------------------------------------------------------------------------------
/docs/Part1/Integration/DiscreteUI/order-details.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/docs/Part1/Integration/DiscreteUI/order-details.png
--------------------------------------------------------------------------------
/docs/Part1/Integration/DiscreteUI/order-list.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/docs/Part1/Integration/DiscreteUI/order-list.jpeg
--------------------------------------------------------------------------------
/docs/Part1/Integration/Library/README.md:
--------------------------------------------------------------------------------
1 | # 领先技术
2 |
3 | 需求可能包含了对领先技术的需要。比如如果旗下有很多款长视频,短视频,直播类的 App,这家公司对领先的视频压缩算法的需求就会非常强烈。这些领先技术会有如下特点
4 |
5 | * 可能成为“Library”被多次复用:其 Git 仓库更多的是相乘的组合关系,而不是相加的组合关系
6 | * 无法从开源技术获取:也就是不能把工作“外包”出去
7 | * 人才招募难:需要有一个专业化团队来吸引人才,甚至组建“研究院”
8 |
9 | 显然“领先的视频压缩算法”这样的需求和之前谈过的所有需求都不同。如果我们把人才,Git仓库拆散来,势必是不利于在技术方面建立竞争壁垒的。其实这也说明了,前面可以按 UI 分,按合同分,而不是按“人才技能”进行拆分,其隐含的前提条件是“commodity technology”。也就是说使用的是“大白菜”技术,也就是开源的技术。
10 |
11 | 也就是“最小化沟通”未必一定是按业务领域来切分 Git 仓库。如果一项私有的尖端技术做得足够有深度,按技术来切分 Git 仓库也是必要的。
12 |
13 |
--------------------------------------------------------------------------------
/docs/Part1/Integration/MixedProcess/README.md:
--------------------------------------------------------------------------------
1 | # 混合型流程
2 |
3 | 在组合支付的流程里,需要先冻结券,然后再让你微信支付。如果微信支付成功,把券核销,然后订单支付成功。类似这样的流程和离散型流程是不同的
4 |
5 | * 返回值是必要的,影响流程走向
6 | * 必须立即执行,无法延迟
7 | * 步骤失败会导致整个事务回滚
8 |
9 | 
10 |
11 | 混合型流程与离散型流程的区别是源自于需求的,和怎么实现是无关的。离散型需求可以理解为 A => B 的两步流程。而混合型流程则至少是 A => B = > C|D 这样的三步流程。
12 | 很多业务需求是用自动化代码实现一个强制执行的虚拟合同,业务逻辑就是合同条款。
13 | 混合型流程才能承载大部分商业合同条款所需要的复杂度,离散型流程并不常见。
14 |
15 | 在本文里,我们并不关心幂等如何实现,并发吞吐怎么提高这些问题。我们只关注,从 Git 仓库拆分的角度来说,不同拆分方式对 Autonomy 的影响。
16 |
17 | ## 按步骤拆分
18 |
19 | 当我们有 A => B => C 的流程时,一个步骤拆分出一个 Git 仓库是最容易想到的实现办法。
20 |
21 | 合同一般都可以分为承诺和履行两个阶段。一般来说承诺的内容,就是对履行的要求。
22 | 比如上面的冻结券和核销券的过程,前序流程的修改,很容易影响到后续的流程。
23 | 简单地按照一个步骤一个Git仓库的做法,很容易造成Git仓库的接口经常变,导致沟通成本高。
24 |
25 | 按步骤拆分往往假定只有“单个合同”,会有一个全局的 status 字段。任何流程的修改,都需要修改 status 字段的定义。
26 | 这个几种定义的 status 字段很容易造成全局影响。比如新增了一个 status 的值,但是其他 Git 仓库不认识怎么办?
27 | 那往往就是 status 的定义一改,所有的 Git 仓库都要改一遍。
28 |
29 | ## 按合同拆分
30 |
31 | 按合同拆分和按步骤拆分的区别在于按合同拆分认为一个 status 字段是搞不定的。
32 |
33 | 比如上面的流程种,微信支付是一个合同,券是另外一个合同。那么我们需要一个 Git 仓库来定义微信支付这个合同的流程。
34 | 需要另一个 Git 仓库来定义券合同的流程。这两个合同都有自己的 status 字段,记录当前合同履行到哪里了。
35 |
36 | 特别注意的是“订单”有时不是一个合同,而是一组合同的集合。比如你下了一个外卖订单,商家在备货,骑手被派单前往商家取货。
37 | 这里面至少对于商家是一个合同,对于骑手是另外一个合同。最常见的影响 Autonomy 的错误是假定订单是单一的合同,把所有的行为都往订单上塞。
38 | 然后把订单的每个阶段都按步骤拆分成一个个的Git仓库。
39 |
40 | ## 集成代码写在哪里
41 |
42 | 当我们有 A,B,C 三个 Git 仓库。至少有这么三种流程集成策略
43 |
44 | * 写一个 D,把 ABC 给集成起来。
45 | * A 交棒给 B,B 又交棒给 C,击鼓传花
46 | * C 监听 B,B 监听 A
47 |
48 | 似乎每一种策略都是可行的。
49 |
50 | 我们换一个角度来看这个问题。所有流程代码都需要数据库里有个 status 字段。
51 |
52 | ```ts
53 | function updateStatus(newStatus: string) {
54 | // ...
55 | }
56 | ```
57 |
58 | 这样的裸接口暴露出去合适么?所谓“合同”,和一行裸的数据库记录有什么区别?
59 |
60 | 其区别就在于,合同是有合同条款的。
61 | 合同要求满足了某些条件之后,才能达成xxx的结果。
62 | 这些写操作的校验逻辑必须被封装到一个 Git 仓库内,而不是散落到各个 Git 仓库里。
63 | 我们不可能直接把 status 字段暴露出去。
64 | 也不能把所有写 status 之前的各种逻辑各种UI全部都纳入到同一个Git仓库里,那样推算的话,啥都能包括进来。
65 | 这么就变成了,把“一些逻辑”做为业务约束,纳入到一个Git仓库内。
66 | 这里的“一些”,就和厨师们所谓的盐少许一样。
67 |
68 | 有这么一种极端的建模方法,我们可以完全没有 status 字段。每个用户的操作,都是一条被记录的已经发生了的事实。
69 | 下一个操作如何响应,取决于所有之前所有用户做过的所有的操作(什么时候点过下单,什么时候点过了取消,诸如此类),综合判断而来。
70 | 换句话说,没有所谓的流程,只有 event,也就没有 status 字段了。
71 | 但是人们在谈论需求的时候,是不习惯到每个event粒度去谈的,更多的是拿个“合同”来跟踪一段流程上可能发生的多个事件。
72 | 当“合同”被创建出来的时候,就代表了将来一段行为会被履约。
73 | 这种习惯做法是来自于契约社会的现实生活的经验。
74 | 除了来自于生活习惯之外,status 字段的另外一个作用是参与到业务约束的判断里。
75 | 比如经常有 status 处于已发货的时候,不能点无理由退款按钮之类的需求。
76 | status 字段不取决哪里写了它,而取决于哪些需要读它。正是因为有很多需要读 status 字段做业务规则计算的地方,才是 status 字段被保存下来的意义。
77 | 即便我们把 status 字段从数据库里去掉,只记录每个用户的每个操作已经发生过的事实(event),在判断业务规则的时候仍然要在内存里把 status 给重建出来。
78 | 换句话说,status 字段是个冗余字段,我们完全可以只新增存储 event,而不更新 status。
79 | 但是这个冗余的 status 字段让写入的业务规则校验更好写,也让UI更容易渲染。
80 |
81 | 什么又是一个“合同”呢,粒度怎么控制?领域驱动开发告诉我们,去倾听业务人员,从他们的原话里提炼Domain概念。
82 | 这个原则当然是对的,但是实践中又会非常难以实操。
83 | 有三个启发性原则:
84 | 如果有多件事情在同时进行,进度无法归纳到同一个 status 字段,那应该拆分成多个合同。
85 | 如果有多个交易主体,那么两两之间很有可能需要有一个独立的合同。
86 | 尽可能往细里拆,直到业务规则里碰到需要判断 status 字段的时候,把一些前后序的合同合并起来。
87 |
88 | 回到“集成代码”的问题。集成代码,就是流程,也就是合同,也就是有 status 字段的东西,也就是应该有一个独立的 Git 仓库。
89 | 所以如果有 ABC 三个 Git 仓库,我们要加一个 D 仓库来把前三者在流程里串起来。否则就一定会出现 updateStatus 这样裸接口。
90 |
91 | ## 依赖倒置
92 |
93 | 当用户了点了一个按钮,但是有多个流程需要驱动的时候,就需要有一个Git仓库来做集成。
94 | 比如一方在买,一方在卖,这就有两个合同。当点了成交按钮的时候,买的合同要往前走,卖的合同也要往前走。
95 | 我们要不把这个集成代码写在最顶层,要么倒置在最底层。
96 |
97 | 比如在使用券和微信支付的例子里,我们除了券,还可能有积分,还可能有红包,还可能有打车金等各种私有凭证。
98 | 这样我们是有可能提供一个聚合支付流程,倒置在依赖的底层。然后把各种各样的私有凭证做为插件插入这个聚合支付流程里。
99 |
100 | ## 小结
101 |
102 | 按步骤拆分不利于 Autonomy。最好是一个合同一个 Git 仓库。
103 |
--------------------------------------------------------------------------------
/docs/Part1/Integration/MixedUI/README.md:
--------------------------------------------------------------------------------
1 | # 混合型 UI
2 |
3 | 这一类需求和离散型 UI 不同。界面上没有明显的大区块。很难说哪个 Git 仓库可以把完整的一部分拿走。例如
4 |
5 | 
6 |
7 | 购物车是典型的混合型 UI。没有哪个 Git 仓库可以声称对界面上的某个区块负责。会员价和限时特价都想给商品打上自己的标,但是实际算价格的时候得看用哪种优惠算得的价格更低。
8 | 在这种比较恶心的复杂需求下,如何最大化 autonomy 呢?
9 |
10 | ## 数据集成
11 |
12 | 我们可以使用服务端数据集成的方式来实现这个需求。数据集成可以实现任意复杂的需求,是对需求形态限制最小的集成方式。
13 |
14 | 
15 |
16 | 这种集成方式
17 |
18 | * 从依赖方向上:业务API依赖商品信息,会员优惠,限时折扣
19 | * 从接口形态上:接口是数据,所有的字段都需要明确定义
20 |
21 | 由业务API做统一的数据汇总。价格谁更低,标打谁的标,这些都由业务API来最终决定。
22 |
23 | ## 责任链模式
24 |
25 | 比如 Java Servlet Filter,长这个样子
26 |
27 | 
28 |
29 | 其接口是
30 |
31 | ```java
32 | public void init(FilterConfig config)
33 | public void doFilter(HttpServletRequest request,HttpServletResponse response, FilterChain chain)
34 | public void destroy()
35 | ```
36 |
37 | 所有的 filter 都能拿到完整的 request 和 response,然后对这个 request / response 做自己的修改。
38 | 最终返回到界面上的是所有 filter 处理完成的结果。
39 |
40 | 也就是说 HttpServletResponse 这个代表了界面,每个 filter 都在渲染这个界面。而 HttpServletResponse 规定了这个界面长什么样呢?
41 |
42 | ```
43 | header
44 | status
45 | body
46 | ```
47 |
48 | 数据结构非常简单。这样的接口当然稳定,但是也限制了用 HttpServletResponse 去表达更复杂的业务需求。
49 |
50 | ## 数据驱动界面
51 |
52 | 运营位是指给界面上留下固定的位置给运营来配置
53 |
54 | 
55 |
56 | 把一个界面做成数据驱动的,哪些地方留了多大槽,可以配置什么东西。
57 | 营销玩法是无穷的,策略是天天变的,但营销位是相对来说变化不那么频繁的。
58 |
59 | ## 责任链加工界面
60 |
61 | 结合上面两种模式。我们可以得到“责任链加工界面”的做法。
62 |
63 | 首先把购物车界面数据化,把每一块都可以配置什么定义成标准的数据结构。
64 |
65 | ```ts
66 | export class Cart {
67 | public order: Order;
68 | public cartProductItems: CartProductCard[] = [];
69 | }
70 |
71 | export class CartProductCard {
72 | public isEditStatus: boolean = false;
73 |
74 | public orderProductItem: OrderProductItem;
75 |
76 | public isChecked: boolean;
77 |
78 | public image: string = '';
79 |
80 | public variantInfo: string = '';
81 |
82 | public name: string = '';
83 |
84 | public count: number = 0;
85 |
86 | public price: number = 0;
87 |
88 | public inventory: number = 0;
89 |
90 | public actualPrice: number = 0;
91 |
92 | // '商品缺货' | '商品下架' | '规格失效' | '库存紧张
93 | public productStatus: string = '';
94 |
95 | public createdAt: Date;
96 |
97 | public tag?: string; // 商品名称前的tag标签值
98 |
99 | public readonly specExtra?: string; // 优惠选择
100 |
101 | public productPromotionType: string = '无优惠'; // 会员价\限时价\无优惠
102 | }
103 | ```
104 |
105 | 然后搞一个责任链来加工这个界面
106 |
107 | ```ts
108 | export class CalculateCart {
109 | public run(order: Order) {
110 | let cart = this.calcCartFromOrder(order);
111 | cart = this.calcProductPromotions(cart);
112 | cart = this.calcOrderPromotions(cart);
113 | return cart;
114 | }
115 |
116 | public calcCartFromOrder(order: Order): Cart {
117 | throw new Error('calcCartFromOrder not implement');
118 | }
119 |
120 | public calcProductPromotions(cart: Cart) {
121 | cart = this.calcMemberPromotion(cart);
122 | return this.calcXszkPromotion(cart);
123 | }
124 |
125 | public calcXszkPromotion(cart: Cart) {
126 | return cart;
127 | }
128 |
129 | public calcMemberPromotion(cart: Cart) {
130 | return cart;
131 | }
132 |
133 | public calcOrderPromotions(cart: Cart): Cart {
134 | return this.calcMjmzPromotion(cart);
135 | }
136 |
137 | public calcMjmzPromotion(cart: Cart) {
138 | return cart;
139 | }
140 | }
141 | ```
142 |
143 | 这样我们就把购物车界面的业务逻辑拆分成了两部分:
144 |
145 | * 接口部分:Cart 和 CalculateCart 是接口。变动相对来说不频繁。
146 | * 实现部分:具体的 calcXszkPromotion,calcMemberPromotion 这些界面渲染方法
147 |
148 | 利用依赖倒置,我们可以把 CalculateCart 定义在依赖关系的底层,由限时折扣,会员优惠这些 Git 仓库去做接口实现。
149 |
150 | 
151 |
152 | 相比 HttpServletResponse,Cart 这个接口显然更不稳定。只能在一定范围内的业务修改可以不去改 Cart 的定义。
153 | 但是 Cart 代表了 UI 一致性,在不同的业务逻辑里有一定的复用性,比如:
154 |
155 | * 商品下架了,购物车里已加购的商品显示到“失效商品”里(因为没货了)
156 | * 配送模式从快递改为自提,购物车里仅限快递的商品会显示到“失效商品”里(因为配送模式不支持)
157 |
158 | 虽然商品下架业务逻辑,自提的业务逻辑,完全没有关系,但是在购物车的展示失效商品的时候,可以以 UI 一致性的理由去做成一样的。
159 |
160 | ## 小结
161 |
162 | 对比“数据集成”和“责任链加工界面”,两个的区别在于是否易于判断一个新需求该改谁。
163 |
164 | 在数据集成的实现方式里,业务API是无所不能的,大部分需求都可以改业务API,也可以去改限时折扣这些Git仓库。
165 | 这就导致需要依赖Code Review,人肉确保没有把所有的业务逻辑都堆砌到最顶层的业务模块里。
166 |
167 | 责任链加工界面,其接口是显式定义的 Cart 和 calculateCart。
168 | 每个具体的业务都是直接返回的 Cart,而不依赖于业务 API 的二次翻译。
169 | 所以相对来说,calculateCart 里可以只放必要的规则互斥之类的逻辑,而不需要太关心什么数据放界面哪个位置这样的事情。
170 | 这样做更容易在改业务逻辑的时候判断应该改哪个 Git 仓库。
171 | Code Review 也可以把注意力主要放在 Cart 和 calculateCart 的修改是否是必要的上。
172 |
173 |
--------------------------------------------------------------------------------
/docs/Part1/Integration/MixedUI/cart.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/docs/Part1/Integration/MixedUI/cart.jpg
--------------------------------------------------------------------------------
/docs/Part1/Integration/MixedUI/chain.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/docs/Part1/Integration/MixedUI/chain.jpeg
--------------------------------------------------------------------------------
/docs/Part1/Integration/MixedUI/promotion-slot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/docs/Part1/Integration/MixedUI/promotion-slot.png
--------------------------------------------------------------------------------
/docs/Part1/Integration/ProductFamily/README.md:
--------------------------------------------------------------------------------
1 | # 产品族
2 |
3 | 由用户挑选自己所需要的产品,动态装配出符合用户需要的软件功能。用户的需求表单可能是单参数的,例如通过不同按钮下的订单,orderType 就不同。
4 | 用户的需求也可能是分阶段,多参数的,例如用户可能先选了专车,然后又表达了自己需要儿童座椅。有很多种装配多个 Git 仓库成为产品族的方式,但是 Autonomy 有很大差异。
5 |
6 | 产品族问题也可以理解为“最终用户编程”问题,它本质上是用户来动态装配组件,而不是每一种组合后的订单,每一种组合后的商品,完全由程序员预编程好。
7 |
8 | ## 单字段:预组合
9 |
10 | 最简单的实现办法是添加一个代表类型的字段,例如 orderType。这种做法就是“预组合”,把所有的可能性都提前叉乘出来,变成单个变量。
11 | 这种做法有两个节点:
12 |
13 | * 写入 orderType:如果和用户的按钮没有一一对应关系,在写入 orderType 的时候还需要有一个汇总多变量为单变量的逻辑
14 | * 读取 orderType:所有需要差异性的地方,都要对 orderType 进行解读
15 |
16 | 也就是 orderType 成为把多个 Git 仓库粘合起来的一个东西,大家都认这个字段的含义。这种做法会有如下问题
17 |
18 | * 难以支持分阶段表达的需求:比如用户下单的时候是一种类型,中途经过了机场,业务含义发生了变化怎么办?更新 orderType 吗?
19 | * 难以添加新的 orderType:加了新的值,所有读取 orderType 的地方都要更新自己的判断逻辑
20 | * 组合爆炸:如果变化的维度很多,叉乘出来的 orderType 可能是指数增长的
21 |
22 | ## 多字段:后组合
23 |
24 | 因为单字段的缺陷,所以实践中往往是一开始以单字段开局,然后很快地滑落为多字段的实现方案。然后又在两种方案之间反复横跳
25 |
26 | * 添加新的 orderType:可以加一个值同时代表很多含义,不用修改表结构
27 | * 添加新的字段:不关心这个字段的Git仓库可以不改。然而如果很多Git仓库都关心这个字段,不仅仅都要修改,而且要改接口定义
28 |
29 | 为了避免频繁加字段,就出现了用一个 bit 来代表某个 feature 是否激活的搞法。搞一个 64bit 的 int64 字段可以代表 64 个 feature flag。
30 |
31 | 公司内会有复杂的组织架构,他们都会对什么是他们负责的有自己的看法。类型字段怎么设置并不影响给不同部门出的报表,通过离线计算总是可以算出业务线所需要口径的数据的。
32 | 要避免按照公司的组织架构来设计字段,因为组织架构不是一成不变的,会拆分也会重组。
33 |
34 | ## 每个 Git 仓库定义自己的“合同”
35 |
36 | 除了多字段之外,还可以通过多表来实现组合。比如对于 Order,我们可以定义另外一个 OrderShipment 表,代表快递。定义 OrderSelfPickup 表,代表自提。
37 | 通过查找这个 order 是否在 OrderShipment 中,得知这个订单是否走快递配送,以及配送的目的地。
38 | 这种做法,相当于加 feature flag,每个新的变种,都是一个新的独立 bit。
39 | 也就是压根没有 xxxType 一说,只有一堆 isXXX 这样的 feature flag。
40 | 加字段,或者加表就是麻烦,单纯从 Autonomy 的角度来说,肯定是一堆独立的 Feature Flag 更独立自主。
41 | 实际工程实践中,占主导性的考虑往往是从IO效率和稳定性出发。或者说是为了迁就落后的基础架构。
42 |
43 | 多表可以理解为每个 Git 仓库定义了自己的“合同”,自己的业务记录在自己的合同内部,而不是都由“order”来夹带。
44 | 实践中,order经常变成 `Map` 这样的玩意,就是所有新的业务,都要往 order 上夹带新私货。
45 | 我们完全可以把具体类型的订单,拆分成一个独立的合同,比如常规购买,团购,周期购。
46 | 这样新加一类需求,可以用加表来实现。订单本身只提供销售额,销售时间这样的最大公约数,用来支持 GMV 统计等需求。
47 | 要不然实现统计 GMV 需求的时候又麻烦了,得一对一做网状集成,而不是都汇总到 order 上,然后做星型集成。
48 |
49 | 面向对象编程发展多年,凝结出来的一句智慧是“用组合代替继承”。用多个表来组合,就是用多个对象来组合。
50 | 本质上可以理解为一个内存中的组合对象,持久化到多个数据库表中。
51 |
52 | ## 虚函数 v.s. 冗余字段
53 |
54 | 我们要让一个对象,比如说,订单有如下接口
55 |
56 | ```ts
57 | class Order {
58 | get totalAmount(): number {
59 | // 多态实现
60 | }
61 |
62 | get merchandisedAt(): Date {
63 | // 多态实现
64 | }
65 | }
66 | ```
67 |
68 | 我们也可以把订单定义两个冗余字段
69 |
70 | ```ts
71 | class Order {
72 | // 销售额
73 | totalAmount: number;
74 | // 销售额计入哪天
75 | merchandisedAt: Date;
76 | }
77 | ```
78 |
79 | 如果我们有不同的合同来代表多样化的业务流程,统计销售 GMV 的工作就会很有挑战。在离散型 UI 里,我们看到了订单列表的例子,其选择就是虚方法的搞法。
80 | 如果下游是离线统计,BI 分析,冗余字段的搞法会更方便对接一些。这样就要求每种具体的合同,如果要把自己归纳为某种订单,都需要有一个对应的Order,同时冗余两个字段。
81 | 从会计的工作方式里我们也可以看到这样的做法。无论业务的原始凭证多么多样,会计都要誊写成会计账目。
82 | 某种程度上,会计的账目不也是冗余字段吗?
83 |
84 | ## 依赖倒置
85 |
86 | 当我们拆分出了多个合同之后,每个合同可能分布在不同的 Git 仓库里。
87 | 从依赖关系的角度来说,要尽可能避免网状的依赖关系。
88 | 比如说分佣,需要知道订单是团购单或者周期购吗?可能是无关的,只需要是一个订单就可以了。
89 | 比如说退款,需要知道团购的订单和直接购买的订单退货流程是不同的吗?可能是相同,退货申请可能和订单关联就可以了。
90 | 这就是订单做为“业务泛型指针”的作用,把订单放在依赖的最底层,所有的上层具体合同通过订单进行彼此互相引用。
91 | 类似商品等泛化的概念也是如此。
92 | 与离散型UI的拆分不太一样的地方在于,这种倒置的依赖关系是怎么工作的。
93 | 离散型UI中,底层的集成组件是利用虚函数的方式来引用上层的具体实现。
94 | 而在产品族中,底层的订单有两个和上层具体实现的互动方式:
95 |
96 | * 强制类型转换:订单做为一个 `void*` 或者 `interface{}` 一样的无类型指针,让具体的业务代码去 downcast(强制类型转换)成自己含义下的订单。
97 | * 数据同步:也就是每个具体的业务把数据冗余一份给订单。
98 |
99 | ## 小结
100 |
101 | 如果判断变化不会很多,作用域控制在单个 Git 仓库内部,可以选择单字段的模式。如果预判将来会有各种幺蛾子,需要用来集成多个Git仓库,还是选择多字段,或者多表更不容易埋坑。
--------------------------------------------------------------------------------
/docs/Part1/Integration/README.md:
--------------------------------------------------------------------------------
1 | # 常见的需求模式
2 |
3 | * 抽象的A、B、C这样的描述是没有代入感的。遇到实际的业务逻辑,仍然不知道怎么拆解。
4 | * 在实际的业务实现过程中,经常发现 Autonomy 的反模式。这些反面例子比正面例子更有教育意义。
5 | * 书本上理想化的说教经常没法套入产品经理乱七八糟的需求里。我不仅仅需要看上去美的小例子,也要有一些实现起来其实挺难受的恶心例子。
6 |
7 | 产品经理很少会从 Autonomy 的角度去思考问题,更多是从整体效果的角度来看。
8 | 拆分是因为"没法把实现都写一起",这个现实约束引起的副作用,是产品经理无法感知的实现细节。
9 | 产品经理和开发者的常见矛盾就在于一方从整体效果出发,而另外一方则从拆分出的细节出发。
10 | 熟知需求模式,是为了把纷繁复杂的需求,往有限的模式里套。
11 | 当套不进去的时候,思考一下这个地方做得这么特殊有什么特别的收益吗?
12 |
13 | 开发者经常陷入的一个误区是从名词出发。比如在应用 Domain Driven Design 的时候,会去想什么叫“商品”呢?
14 | 一个名词什么都不是,又可以什么都是。
15 | 纠结一个词是什么意思,毫无意义。
16 | 我们的目标是实现业务逻辑,把业务逻辑做好拆分。这些业务逻辑的外在表现才真正定义了什么叫“商品”,什么叫“线索”,什么叫“工单”。
17 | 常见的外在表现不外乎 UI 长什么,流程是什么。所以我们来具体看看常见的 UI 和流程需求怎么能从 Autonomy 的角度拆好:
18 |
19 | * [离散型 UI](./DiscreteUI/README.md)
20 | * [混合型 UI](./MixedUI/README.md)
21 | * [离散型流程](./DiscreteProcess/README.md)
22 | * [混合型流程](./MixedProcess/README.md)
23 | * [产品族](./ProductFamily/README.md)
24 | * [领先技术](./Library/README.md)
25 |
26 | # 中台?集成!
27 |
28 | 中国企业喜欢包办一个客户的所有需求,这和国外崇尚专业化的做法是非常不同的。这就导致了中国式的 App 从需求上就包括
29 |
30 | * 所有的功能都要挤到同一个App的同一个界面的同一个流程里去实现。特别是要往主业务里挤,这样才能分到流量。
31 | * 业务与业务之间有网状的互联互通需求。比如 Uber 就不会分专车,快车,优享。但是国内的业务就会分得很细,彼此之间又要倒流升舱一键呼叫。
32 |
33 | 中台的出现,不是为了复用,减少新业务的软件开发成本。软件开发能有多少成本,或者说能省多少成本。
34 | 不是因为我们认为需求都差不多,可以归纳出可复用的预制件,然后沉淀到中台去。
35 | 中台的本质是为了让上面这样的深度整合的需求因为集中到一个部门能做得更快一些。这种类型的需求也许是有中国特色的。
36 | 与传统企业财务集中那样的离线整合不同,这里的集成需求需要是在线,深入参与客户交互体验过程之中的。
37 | 如果让主界面被任意一个业务所全部独占,其他业务与其合作就阻力会更大一些。
38 | “收口”到中台可以方便业务之间实现稀缺资源的共享(分配,撕逼),也方便业务之间的互联互通,减少适配成本。
39 |
40 | 中台的本质是“中间的台”,是因为其位置在中间,把各种业务各种功能整合集成到了一起。
41 | 过去某种特定的实现中台的技术方案可能会过时,会消失。
42 | 但是只要中国式的 App 风格不变化,对中台的需求是不会消失的。
43 |
44 | 最大化客户 LTV 在获客成本高企的今天有其合理性。
45 | 技术是为业务成功服务的。如果业务需要高复杂度的逻辑整合,那么技术写得出来得写,写不出来也得写。
46 |
47 | 希望前面提过的几种业务逻辑拆分模式可以让我们对于中台该做什么,不该做什么,有一个不同的视角的认识。
48 | 以这个[虚构故事](https://zhuanlan.zhihu.com/p/82586450)自省:
49 |
50 | ```
51 | 中台部门自称自己是 Software Product Line,提供了一堆预制件以及装配用的 DSL,能加速新业务的快速上线与试错。
52 | CTO 希望中台是 Central Platform 削平内部的山头,打通数据和权限的利器。
53 | 实际不小心落地成了 Middle Office(Middle 衙门),只要从此过,留下买路财的官僚机构。
54 | ```
55 |
56 | 当我们拆分出了一堆Git仓库之后,
57 | 对于某些集成的需求(并不是所有的集成需求都是如此)使用星型的集成比点对点的集成更经济。
58 | 这个负责集成的Git仓库需不需要一个专职的团队,是不是一定要是独立的进程,是不是要创建一个名为中台的组织?
59 | 与财务,法务,市场,营销,人力资源等职能不同,这些职能是有自己的专职工作内容的,对技能的要求是有专业性的。
60 | 有 HRBP,那需要中台 BP 吗?
61 | 业务逻辑拆分模式只探讨到Git仓库的拆分,关于Git仓库如何与组织架构对应起来,留给读者自行寻找答案。
62 |
--------------------------------------------------------------------------------
/docs/Part1/README.md:
--------------------------------------------------------------------------------
1 | # Part.1 代码防腐
2 |
3 | 这个 Part 我们主要讨论如何拆分 Git 仓库的问题。我们首先来明确什么样的拆分结果是“好”的,如果拆分不当其症状是什么:
4 |
5 | * [度量 Autonomy 的指标](./AutonomyMetrics.md)
6 | * [度量 Consistency 的指标](./ConsistencyMetrics.md)
7 |
8 | 但是要保持代码是“好”的状况很难。代码腐化似乎注定的
9 |
10 | * 最初:没有谁是不想好好写的。都有一个宏伟的规划,这次一定
11 | * 途中:Code Review 如同“堂吉诃德”一般,根本架不住大批量大批量的修改
12 | * 放弃:躺平了,下次一定
13 |
14 | 如此循环往复。然而腐化了之后,是无法起死回生的。
15 |
16 | * 食品防腐是 low tech 的事情,但是中毒身亡之后起死回生是天顶星技术
17 | * 新冠疫苗已经被人类掌握,但是免疫风暴造成的多脏器衰竭仍然是天顶星技术
18 |
19 | 虽然很多人醉心于遗留代码改造之道。笔者也从事铲屎业务很多年,仍未掌握此项技术。
20 | 还是让代码一直保持在未腐化的状态更简单一些。那么代码如何防腐呢?不靠 Code Review 又靠什么呢?
21 |
22 | * [对策1:信息隐藏](./InformationHiding/README.md)
23 | * [对策2:持续改进](./Consensus.md)
--------------------------------------------------------------------------------
/docs/Part1/Scenario/AutonomyOptimization/README.md:
--------------------------------------------------------------------------------
1 | # 优化 Autonomy
2 |
3 | 为了达成 “各写各的” 的目的,在一口锅里吃饭总是要先有一些规矩的。
4 |
5 | 这部分的一致性主要靠上层业务的 stakeholder 去推动,因为独立自主是他们受益,他们是最有动力保持互相没有关系的状态。
6 | 如果各个 stakeholder 的诉求都可以独立写一个自己的页面,独立搞一个自己的流程,那皆大欢喜。
7 | 然而事情往往是大家都希望往同一个页面里加东西,都希望往同一个业务流程里夹带私货。
8 | 正是因为这些集成需求,才导致了 Autonomy 受到了挑战。
9 | 要优化 Autonomy,就要避免专职的 xxx-api 团队来集成所有人的代码,这个团队很快就会变成瓶颈。
10 |
11 | 常见的“一致性”是某种插件化架构,包括不限于微前端,OSGi,代码加载器,编译期钩子等等。
12 | 重点不在于搞出来什么,这些代码打包技术能力上都是等价的。
13 | 重点是大家都用同一套。
14 |
15 | # 指标计算与自动检查
16 |
17 | 指标方面要特别关注“必要参数占比”。达到优化 Autonomy 的目的了就可以了,过度了就不好了。不要以一致性的名义,把不要写在一起的东西强行写一起了。如果插件小,motherboard大,motherboard天天改,那就需要反思一下了。
--------------------------------------------------------------------------------
/docs/Part1/Scenario/FeedbackOptimization/README.md:
--------------------------------------------------------------------------------
1 | # 优化 Feedback
2 |
3 | 在 Feedback 这部分,我们讨论了“控制边界”和“控制变更”这两部分内容。这分别对应了监控和发布变更这两项运维的传统职能。
4 | 在监控系统,发布部署系统的选择方面,要保持一致性并不难,运维强推就可以。
5 |
6 | 如果要更精细一些的Feedback,那可能要牵涉到 RPC 框架的统一,进程内的边界监控。这些就要靠 QA,基础架构,研发效能等职能部门的共同努力了。
7 | Feedback 的最大敌人是多编程语言,如果编程语言都无法统一,这些一致性的保持代价会非常高。
8 | 基础架构团队很难去给每种编程语言提供足够优良的 SDK,新的特性也很难保持各种语言的实现会同步跟进。
9 | 盲目地拆微服务尚有挽救的必要,盲目地拆成多语言的微服务,建议直接放弃治疗。
10 |
11 | # 指标计算与自动检查
12 |
13 | 以 RPC 框架的一致性为例:
14 |
15 | 通过代码扫描可以很容易发现哪些地方是直接 http 调用,而没有走 RPC 框架的。
16 | 甚至用正则匹配就可以把“接入率”给计算出来。
17 | 另外一个途径是在运行时采集流量数据,比如哪些请求是 RPC 框架发的,可以打上特殊的标记。
--------------------------------------------------------------------------------
/docs/Part1/Scenario/README.md:
--------------------------------------------------------------------------------
1 | 这里根据有限的经验,列举几个常见的场景。
2 |
3 | * [用户可见的一致性](./UserInterface/README.md)
4 | * [优化 Autonomy](./AutonomyOptimization/README.md)
5 | * [优化 Feedback](./FeedbackOptimization/README.md)
--------------------------------------------------------------------------------
/docs/Part1/Scenario/UserInterface/README.md:
--------------------------------------------------------------------------------
1 | # 用户可见的一致性
2 |
3 | 用户可见的一致性包括以下三类:
4 |
5 | * 颜色、字体
6 | * 布局组件
7 | * 以表单为代表的预制件
8 |
9 | 颜色、字体应该是全局设置,而不是在每个地方用 css 指定颜色的 RGB 值。就算要覆盖全局的设置,也应该是用 prominent 之类的变量名字来引用颜色,而不是用 RGB 值。
10 |
11 | css 有无数种实现布局效果的做法。不应该让每个页面都用 css 盒模型这样的高灵活度的方式来做布局。否则很容易一种布局,写出好多种写法来。而是把 UI 稿中的布局模式进行归纳,做好一系列的布局组件。每个页面都应该用归纳出来的布局组件来做布局,而不是直接使用 css 的 flexbox 这些。
12 |
13 | 更进一步的,我们会发现很多视觉效果,交互模式,乃至局部的产品功能都是重复的。有追求的开发者会有抑制不住的热情去归纳“预制件”(与之相对的是现浇混凝土),预期能通过对业务的归纳达到减少代码量的目的。但是如果仅仅由开发团队发起,这是一个很危险的道路。如果要抽取出“一键生成表单”这样的预制件,一定要是和产品与设计团队达成前提下来做。如果产品设计团队通力配合,B 端表单类的需求是完全有可能通过复用预制件大幅减少开发和维护时间。
14 |
15 | # 指标计算与自动检查
16 |
17 | 比如,规定了不能写颜色的 RGB 值。那可以自动化去检查代码中是否有 RGB 值。
18 | 如果团队达成了一致,禁用 css 写布局,必须用布局组件。那么自动检查中可以看提交的代码里是不是有 css,如果有 css 则直接禁止提交。
19 |
20 | 可复用的组件如果仅仅是做了放在那里,很容易就找不到,而不复用你。如果是出于一致性目的做的组件,应该强制推广。不是使用者主动选择使用哪些组件,而是在应该用组件的地方没有用,需要提示乃至禁止提交。比如表单组件如果有一层封装,那么系统原生的表单组件就应该被禁用。最好是能有量化的办法来统计出接入率,阻断率。
21 |
--------------------------------------------------------------------------------
/docs/Part1/VscodeExample/README.md:
--------------------------------------------------------------------------------
1 | 在 vscode 中,我们有如下的模块划分结构
2 |
3 | 
4 |
5 | 其中 vscode 提供了字符输入,换行,响应鼠标等基础的编辑器。typescript-basics 模块依赖了 vscode,提供了代码区块的折叠能力(识别typescript的class/function语法),提供了基于词法分析的着色。而 typescript-language-features 则提供了代码补全等需要 typescript 类型检查信息的 IDE 编辑功能。
6 |
7 | 假设有一个需求是需要在文件上添加一个右键菜单,展示所有调用了这个文件的列表。我们应该修改哪个模块? 显然应该是修改 typescript-language-features,因为只有它依赖了 ts server,其他模块拿不到函数调用关系的信息。
8 |
9 | 假设需要提供一个快捷键,把整个段落的代码选中。那么我们是应该修改 typescript-basics 还是 typescript-language-features? 这个情况下,修改 typescript-basics 就可以实现。因为这个功能仅仅需要对代码做词法分析,就可以识别出“段落”来。
10 |
11 | 但是上面两个修改都不需要修改 vscode 本身。如果把对 typescript 的功能修改,实现在 vscode 这个模块里则会显得非常奇怪。就像我们在前面计费模块的案例里看到的,像 vscode 这样的位于依赖关系底层的模块,应该尽量少的改动。如果改动了之后,会导致引入很多其他上层客户不需要的功能,则违背了 common reuse principle。
12 |
13 | 如果我们要给 *.spec.ts 的文件,添加如下的业务功能
14 |
15 | 
16 |
17 | test 上面的 “Run | Debug” 就是需要新增的功能。那么我们是应该修改 typescript-basics 还是 typescript-language-features 呢? 都不应该。因为这个功能需要依赖 mocha 的 test runner,要在点击按钮之后调用 mocha 执行测试。所以实现的方式是
18 |
19 | 
20 |
21 | 通过新增一个模块的方式,把新功能给实现了。这样 vscode / typescript-basics / typescript-language-features 都不需要修改。虽然最终呈现的效果是在用户打开了 *.spec.ts 文件之后,给这个编辑器增加了一个新的功能。vscode 是如何做到这一点的呢? 是因为 vscode 把编辑器上的功能做了“标准化的定义”。vscode 把自己的职责从实现需求,变成了制定标准。这些 editor 区域的标准化扩展接口就包括了
22 |
23 | * CodeLens
24 | * CodeAction
25 | * Formatting
26 | * SignatureHelp
27 | * ...
28 |
29 | 这些扩展接口可以是 typescript-language-features 提供一部分,然后 mocha-test-explorer 实现另外一部分,由 vscode 最终集成起来。但是这个集成和前面的“业务编排API”的例子不同。不同之处体现在了依赖关系上,vscode 是被 typescript-language-features 依赖,而不是 vscode 同时依赖了 typescript-language-features 和 mocha-test-explorer 去把这两个模块整合起来。
30 |
31 |
--------------------------------------------------------------------------------
/docs/Part1/VscodeExample/dependency1.drawio.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/Part1/VscodeExample/mocha.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/docs/Part1/VscodeExample/mocha.png
--------------------------------------------------------------------------------
/docs/Part2/AutonomyFirst.md:
--------------------------------------------------------------------------------
1 | # Autonomy 优先
2 |
3 | 回到微服务应该怎么拆分,多少个进程是合理的。
4 | 相比 [ProcessBoundary](./ProcessBoundary/README.md),我认为控制好 [FunctionBoundary](./FunctionBoundary/README.md) 和 [PluginBoundary](./PluginBoundary/README.md) 更有利于 Feedback。
5 | 相比 [MultiProcess](./MultiProcess/README.md),我认为一开始就设计好 [MultiTenancy](./MultiTenancy/README.md) 和 [MultiVariant](./MultiVariant/README.md) 更有利于 Feedback。
6 | 进程之所以好用是因为开源社区提供了很完善的基础设施。所以我们习惯了迁就于已有的技术设施,来切分进程以利用上这些现成的设施。
7 | 从而因为进程的切分来影响我们对Git仓库和团队的切分。
8 |
9 | 如果能够做好 [FunctionBoundary](./FunctionBoundary/README.md),[PluginBoundary](./PluginBoundary/README.md),[MultiTenancy](./MultiTenancy/README.md) 以及 [MultiVariant](./MultiVariant/README.md),怎么分进程根本就不是一个问题。
10 | 所有的代码都跑在一个进程内,一样可以把 Feedback 做得很好。
11 | 那微服务的拆分,其实就等价于Git仓库的拆分,也就是 Autonomy 章节中讨论的问题,怎么拆团队拆Git仓库的问题。
12 | 其主旨原则就一条:拆分之后,接口的定义要稳定,不要天天修改,导致频繁的跨团队强协作。我们也对怎么量化这个原则给出了指标计算的方法。
13 |
14 | 所以 Autonomy 优先,Feedback 的问题,交给基础架构部门去解决。
--------------------------------------------------------------------------------
/docs/Part2/ControlBoundary.md:
--------------------------------------------------------------------------------
1 | 业务逻辑无论如何做拆分,最终仍然是要跑起来,集成到一起去的。
2 | 无论是编辑时拆分成文件、文件夹、Git仓库,还是运行时拆分成进程,拆分无可避免地引入了“降低反馈速度”的副作用。
3 | 一旦产生了分工,就会有你不了解的部分。这也是分工的本意所在,控制知识边界,让普通人也可以参与劳动。
4 | 但是软件是要整体集成到一起才能发挥其全部价值。这种“整体”与“部分”的矛盾,造成了Feedback问题。
5 |
6 | 要减轻分工带来的负面影响,最重要的是做好“甩锅”。
7 | 换句话说就是要做好“隔离”,虽然我们把所有的部件都集成到了一起了,但是我们仍然要在“运行时”通过各种手段人为制造出边界,把责任隔离出来。
8 | 把责任隔离出来,就是把运行时的行为,与背后负责的团队与个人对应起来。
9 |
10 | 
11 |
12 | 用户看到的问题永远是前端的问题,那不可能所有的bug都是前端的锅。
13 | 前端也可能是一个多业务线共享的 App,出 bug 的地方可能是共享的地图模块呢?
14 | 反馈来自“运行”,然后一次次被传递。如果“运行时”没做好隔离,反馈分发流转给开发者的效率就会变低。
15 |
16 | 边界从大到小,我们可以分为
17 |
18 | * [进程边界](./ProcessBoundary/README.md)
19 | * [函数边界](./FunctionBoundary/README.md)
20 | * [插件边界](./PluginBoundary/README.md)
21 |
22 | Git 仓库独占一个进程未必是唯一的选项。把 Git 仓库以插件的形式,在进程内再切分出插件边界也是可以满足 Feedback 需求的。
--------------------------------------------------------------------------------
/docs/Part2/ControlChange.md:
--------------------------------------------------------------------------------
1 | 单体应用最大的原罪就是变更的粒度太大了。而大粒度变更是稳定性的最大敌人。
2 | 切分变更有三种主要的做法:
3 |
4 | * [多进程](./MultiProcess/README.md):把单体进程切分成多进程。一次只变更其中的一个。
5 | * [多租户](./MultiTenancy/README.md):把所有的业务数据分成租户。一次只升级一个租户的数据和代码。
6 | * [多变种](./MultiVariant/README.md):分租户还是粒度太粗了,比如说挂掉一个城市也是不可接受的。那么可以在线上同时运行多个版本的代码,然后逐步的切流量。
7 |
8 | 当然更极致的做法是同时分租户,也在租户升级的时候切流量。
--------------------------------------------------------------------------------
/docs/Part2/FeedbackMetrics.md:
--------------------------------------------------------------------------------
1 | 我们担心这样的症状:
2 |
3 | * 故障定位慢:线上出了bug,要花很长时间才能定位出问题的代码以及对应的开发者。
4 | * 获得真实反馈慢:代码写完了要等苹果审核发版,错过了这个版本又要等一个月公司才会发下个版本。
5 | * 本地测试难:稍微有点价值的测试都不是本地可以用 JUnit 写出来的。
6 | * 听不见炮火:管你前线洪水滔天,我这后台模块是管不着的
7 |
8 | Feedback 的愿景是尽量减少获得反馈的摩檫力。业务逻辑拆分为什么会影响到 Feedback 呢?这仍然是要归结为人的沟通效率问题。能用标准化的方式解决的,就可以减少沟通。能尽量减少信息传递次数的,就可以有效减少传递过程中的信息衰减。
9 |
10 | 度量指标包括:
11 |
12 | ## 工单流转时长
13 |
14 | 企业的外部用户创建的工单,如果最终发现需要开发来处置,到转到对应的开发手里。这个从工单创建时间,到开发开始处置的时间差,就是工单流转延迟。
15 | 度量这个指标主要是为了避免后台模块缺乏对企业最终用户的体验缺乏同情心,减少中间的层次。
16 | 这里工单指的是偶发的个案。对于大面积故障造成的工单,一天之内的工单合并为记录为一条。也就是数据采样的时候,一天只抽取一条工单纳入指标。
17 |
18 | ## 故障定位时长
19 |
20 | 孤立的一个进程很难完成所有工作。业务逻辑不可避免地要拆分成多个进程。如何找到出问题的进程。
21 | 一个进程也很难仅由一个 Git 仓库构建而来。业务逻辑不可避免地要拆分成多个 Git 仓库。如何找到进程的问题是哪个 Git 仓库造成的。
22 | 故障定位延迟是指故障从开始定位,到找到根本原因所花的时间。进程边界,Git 仓库的边界,越不依赖人的经验,越不依赖人的现场沟通,就越可能降低故障定位延迟。
23 |
24 | ## 代码集成时长
25 |
26 | 从修改一行代码,到把这行代码修改和其他进程集成到一起,用真实流量验证,这个端到端的延迟是多少。
27 | 开发自己的笔记本能把所有的进程都能启动起来是一种办法。
28 | 开发能用单元测试模拟试也是一种办法。
29 | 每个小时上一次线也是一种办法。
30 | 只要能对刚才改的那行代码,集成起来不出问题有信心就可以。
31 | 所以我们没有把“本地开发环境设置时间”做为一个指标,因为能够本地启动进程是手段,而集成到一起测才是实际目的。
32 |
--------------------------------------------------------------------------------
/docs/Part2/FunctionBoundary/README.md:
--------------------------------------------------------------------------------
1 | 除了进程之外,函数是一个所有编程语言,所有运行时平台都有的概念。
2 | 每次函数调用都有一个运行时的 StackFrame 的数据结构来代表这次调用,某种程度上这就造成了函数的边界。
3 |
4 | 假设我们要在函数这个层面获得Feedback,那么有如下两种糟糕的结果:
5 |
6 | * 日志量太大了:无时不刻不在发生函数调用。全部都记录下来,那看也看不过来呀。
7 | * 一个函数的调用记录啥都说明不了:假设你看到了一个错误报告,仅仅报告了最后一个被调用函数是什么,参数是什么,其余的信息都没有。显然这样的Feedback也是不够的。
8 |
9 | 所以函数边界的关键不在函数,而在“调用”。我们以 React 渲染界面为例,至少有三种类型的“调用”。
10 |
11 | # 同步调用栈
12 |
13 | 所有的编程语言都有 StackTrace 来记录同步的调用链。同步调用链是不需要额外参数来记录的,编程语言甚至CPU都有基础设施来跟踪 caller/callee 的关系。
14 | 比如我们调用 React.createElement 的时候,React 内部又会调用几个子函数,执行完了之后以 React.createElement 返回值的形式返回给我们一个 React Element。
15 |
16 | # 异步调用栈
17 |
18 | React 的界面刷新过程是异步的。当我们调用 this.setState 的时候,我们并不能拿到返回值,也不能肯定在函数调用返回的时刻,界面已经刷新完成了。
19 | 这个时候我们只能确定 React 已经把要拿什么新的 state 刷新界面这个事情记录下来了,将来会执行的。
20 | 那我们想要知道谁触发了渲染,从哪里调用过来的,怎么办?
21 | 这个时候就无法依赖 javascript 内置的同步调用链了,而需要依赖 React 提供的异步调用链。
22 | React 的开发者工具也提供了可视化的界面来展示这个异步调用链。
23 | 详情参见 https://gist.github.com/bvaughn/8de925562903afd2e7a12554adcdda16
24 |
25 | ```js
26 | import {
27 | unstable_trace as trace,
28 | unstable_wrap as wrap
29 | } from "scheduler/tracing";
30 |
31 | trace("Some event", performance.now(), () => {
32 | this.setState(newState)
33 | });
34 | ```
35 |
36 | 这样我们调用 `require('scheduler/tracing').unstable_getCurrent` 的时候,就可以从返回的 interactions 里找到 Some event 这个 interaction 了。
37 | 似乎这里并没有给 this.stateState 提供额外参数,那么 React 是如何把异步函数调用给串起来的呢?
38 | 这里 React 使用了 unstable_trace 设置的全局变量。如果不依赖全局变量,写法应该是
39 |
40 | ```js
41 | const newContext = context.trace('Some event');
42 | this.setState(newContext, newState);
43 | ```
44 |
45 | 以显式传递一个 context 参数的形式来把调用链给串起来。这种做法也是 Go 等异步编程语言的常见模式。
46 | 在非前端的场景下,一般都无法使用全局变量,所以 Java 等语言会用 Thread Local 来代替全局变量,实现与 React 类似的模式。
47 |
48 | # 组件树
49 |
50 | React 的每个组件都是一个函数。
51 |
52 | ```jsx
53 | function A() {
54 | return
55 | }
56 | function B() {
57 | return B
58 | }
59 | function C() {
60 | return C
61 | }
62 | ```
63 |
64 | 从语法层面上来看,这个组件就是一个函数调用栈。组件嵌套了组件,也就是函数嵌套调用了函数。
65 | 在 React 内部,要记录 A/B/C 三次调用,以及调用与被调用的 caller/callee 关系。
66 | 那为什么我们在 `` 的调用上,为什么看不到把 A 做为 caller 参数 `` 传递过去的呢?
67 | 这是因为 React 是一个“虚拟机”,`
` 是这个“虚拟机”支持的“代码”。
68 | 在解释执行 `
` 的过程中,React 自然可以任意把上下文的参数塞进去。
69 |
70 | 当然,实际执行的时候,React并不是虚拟机,`
` 也并不是虚拟机的执行代码。
71 | 但是 caller/callee 的关系是真的要被记录下来的,在查找问题的时候,其 caller/callee 的关系也是非常有用的。
72 | 之所以要把“组件树”假装成函数调用,是想要启发你 StackTrace 其实意味着什么。
73 |
74 | 当 React 组件的行为异常的时候:
75 |
76 | * 仅仅告诉你同步调用栈够吗?不够,因为不知道是谁触发了我重渲染,我的父组件又是谁
77 | * 仅仅告诉你异步调用栈够吗?不够,因为不知道具体是哪个直接的同步函数调用出了问题,也不知道是界面哪个角落的组件出的问题。
78 | * 仅仅告诉你是界面上哪个位置的组件出的问题够吗?不够,因为只知道哪里出了问题,并不能告诉我谁引起的问题
79 |
80 | 跟踪“调用”是为了调查“因果关系链”。只要是能回答“因果关系”的信息,都是 Feedback 所需要的信息。
81 |
82 | # 索引与落盘
83 |
84 | 无论是编程语言原生支持的同步调用栈,还是需要自研的异步调用栈,组件树,其目的都是隔离。
85 | 隔离就是给caller/callee 关系**建索引**,在找问题的时候尤其有用。
86 |
87 | 另外因为内存成本低很多,先把日志都整理好记录在内存中,如果有必要的时候再持久化下来。
88 | 目前的硬件还无法 7*24 的把调用栈落盘,或者说大部分业务的客单价从商业价值上还无法支持这样的成本。
89 | 这也是进程隔离的优势,进程间调用是精心设计的,其数据量也小得多。
--------------------------------------------------------------------------------
/docs/Part2/MultiProcess/README.md:
--------------------------------------------------------------------------------
1 | 因为大部分线上故障都是由变更引起的,所以SRE会非常强调部署流程的小心谨慎。
2 | 一旦发现有问题,就会被要求立即回滚。这也就导致了搭车上线是非常讨厌的事情,谁知道你搭车进来的改动会不会翻车。
3 | 所以拆分成多进程,各上各的就会变成非常强烈的需求。
4 |
5 | 多进程当然是有其合理性的,替换进程是最简单最可靠的变更形式。
6 | 开源的 kubernetes 等项目也提供了完善的基础设施来支持多进程的部署模式。
7 | 特别是没有单元测试情况下的PHP应用,是不是有语法错误都无法在部署之前保证,强烈依赖于上线过程的观察。
8 |
9 | 但是用拆分进程的方式来解决上线慢的问题也会有一些缺点:
10 |
11 | * 反馈不集中:小流量的时候,引起的小规模的故障可能观察不出来
12 | * 灰度数有限:集群规模比较小的时候,总共就几个进程,灰度的刻度就会很大
13 | * 回滚慢:进程替换需要时间
14 | * 灰度时间有限:上线观察一天已经很夸张了,要更长时间的观察是不好用上线的方式来实现的
15 | * 上线顺序:进程之间经常有数据依赖,并不能总是保持向后兼容。当要同时升级多个进程的时候,上线顺序的确定是比较头疼的事情
--------------------------------------------------------------------------------
/docs/Part2/MultiTenancy/README.md:
--------------------------------------------------------------------------------
1 | 多租户和多进程不同,租户可能是按游戏大区分,按城市分,也可能是按商家分。租户是业务相关的概念。
2 | 这意味着开源社区是不能给你现成的基础设施的,所有的多租户功能都需要自研。
3 | 拆分成多租户之后,变更就可以一个一个租户来做。挂了也只会影响一个租户,不太可能引起大面积的故障。
4 |
5 | 租户整体做升级也可以避免上线顺序的问题,把租户短暂停服,全部升级完了再继续提供服务。
6 | 可以只停服一个租户,可以让很多复杂的变更很好做,比如改变数据库的表结构。
7 |
8 | 传统的测试一般都是在一个离线的环境里进行。但是对于性能压测,复杂的多进程业务,搭建独立的离线环境复制在线环境是成本很高的事情。
9 | 保持两个环境的一致性也需要非常强的纪律和日复一日的努力。
10 | 直接 Test in Production 是越来越明显的趋势。
11 | 做全链路压测的时候,经常把修改的数据隔离在“影子表”里,避免污染正式数据。这其实就是多租户,只是把压测跑在一个隔离的租户里了。
12 | 各种 QA 账号,测试订单也可以用租户的方式来实现,而不需要 case by case 的硬编码在代码,甚至引起安全隐患。
13 |
14 | 同时跨数据中心搬迁等复杂的变更场景也需要数据是能切开的。从加快 Feedback,控制变更的角度,第一天就应该考虑多租户的问题。
15 | 如果不是一开始就切分了租户,后来添加进来的成本会非常高。
--------------------------------------------------------------------------------
/docs/Part2/MultiVariant/README.md:
--------------------------------------------------------------------------------
1 | 考虑以下三种做法:
2 |
3 | * 在代码中埋入 Feature Flag,通过配置中心下发开或者关,或者对一部分流量打开
4 | * 在本地的笔记本上启动一个独立的精简集群做测试开发
5 | * 在本地的笔记本上只启动一个进程,集群的其余的进程使用生产环境的
6 |
7 | 我把这三种做法都归纳为 Multi Variant,多个变种。它们都是在不改变进程的情况下,通过配置等方式更灵活地进行装配组合。
8 | 这样发布和上线就可以解开为两个操作了。上线就是上线,改变进程里的可执行代码。
9 | 上线可以不把 Feature Flag 打开,而是保持原有的行为。除非 Feature Flag 本身实现得有bug,上线过程的风险就小了很多。
10 | 然后再慢慢地打开 Feature Flag 的流量开关。
11 |
12 | Feature Flag 未必一定是用 if/else 实现的,它可以是基于插件机制来实现的。
13 | 根据不同的 Feature Flag 的配置信息,选择执行同一个插件的不同版本。
14 | 其实质是运行时的插件动态装配。把发布从进程粒度,降低到插件的粒度。
15 |
16 | Feature Flag 虽然好用,但是仍然要先完成上线。从 Feedback 周期来说仍然不够理想。
17 | 如何在上线之前就测试好呢?本地搭建一个完整的环境困难重重,不仅仅可能笔记本性能不够,而且还有一大堆的数据配置依赖。
18 | 理想情况当然是可以把业务拆成更小的部分,只做一部分的集成就能完成测试。
19 | 比如说一个app里包括了打车和外卖,那需要打车和外卖都打包进去吗?显然可以只跑打车的代码嘛。
20 | 但是部分集成不是免费的,每添加一种部分集成的跑法,就给代码增加了一个运行模式,是需要额外维护的东西。
21 | 当团队比较多的时候,只有维护生产环境是大家共同的目标,你自己搞出来的一个部分集成的环境,未必能够得到其他人的认同和资源支持。
22 |
23 | 所以 Test in Production 才会逐渐火起来,因为只有 Production 才是唯一公认必须保持稳定的环境。
24 | 在自己的笔记本上只启动自己修改了代码的进程,才更符合高效分工的原则。
25 | 根据概率论,一个系统的稳定性是其构成模块稳定性的乘积。
26 | 如果要本地搭建一个完整的集群,必然是一个很不稳定的东西。
27 | 在有了多租户的前提下,Test in Production 仅仅需要解决部分替换进程这个问题。
28 | 通过在 Http Header 中附加路由信息(例如 [Istio Request Routing](https://istio.io/latest/docs/tasks/traffic-management/request-routing/)),
29 | 是可以实现一个集群中其他进程都用生产环境,但是你修改的进程替换成你本地启动的。
30 |
31 | 我们总结一下
32 |
33 | * 多进程:控制的是代码的变更
34 | * 多租户:控制的是数据的变更
35 | * 多变种:控制的是配置的变更
36 |
37 | 理想的快 Feedback 工作环境里,上线应该是每个小时都发车,随意可以搭车的。
38 | 多个团队的Git仓库其实是跑在一个进程里。
39 | 每个团队通过 Feature Flag 来做自己的灰度测试,出了问题可以一键回滚。
40 | 一次可以只变更一个租户,确保不出大面积故障。开发者不需要费心维护自己私有的开发环境,而是直接 Test in Production。
41 | 需要全链路压测的时候,创建一个新租户,放心大胆地随便随便测。
--------------------------------------------------------------------------------
/docs/Part2/PluginBoundary/README.md:
--------------------------------------------------------------------------------
1 | 通过多进程,我们可以实现业务逻辑的动态组合。但是用普通函数也可以组合
2 |
3 | ```ts
4 | function f(g: () => void) {
5 | // do something
6 | g();
7 | // do something else
8 | }
9 | ```
10 |
11 | 这样 f 和 g 就组合起来了,我们称 g 为一个插件。
12 | 插件边界是指当 f 和 g 来自于不同的 git 仓库的时候,f 调用 g 就是跨越了 git 仓库的边界。
13 | 那怎么能实现 f 和 g 来自不同的 git 仓库呢?
14 |
15 | * 动态二进制链接:windows 的 .dll,linux 的 .so
16 | * 动态源码链接:javascript 可以远程下载 js 文件,然后动态执行
17 | * 静态二进制链接:windows 的 .lib,linux 的 .a 或者 .o
18 | * 静态源码链接:webpack
19 |
20 | 一些运行时环境,例如 iOS 是不鼓励从网络动态加载代码的。
21 | 插件并不一定等于动态化,插件完全可以是静态装配的。
22 |
23 | 无论代码是怎么组合成运行时的进程整体的,拼装后总是有拼接缝的。
24 | 那我们就可以在“插件loader”这样的地方去添加打日志与监控的代码。
25 | 并不是 RPC 调用才有错误率,请求延迟这些指标。插件也可以有。
26 |
27 | 虽然函数调用的频次是非常高,暴力记录所有的函数调用是吃不消的。
28 | 但是我们完全不用关心 Array.map,Set.add 这样的函数被调用了多少次,
29 | 只需要区分好哪些是插件,然后把这些高价值的插件函数给监控好就足够提供 Feedback 了。
30 | 不同 git 仓库的所有者,仍然可以从日志里拿到自己的日志,和进程隔离没有区别。
31 |
32 | 如果有强制的插件规范,那甚至可以做到“内存状态”的隔离,用编译检查等手段禁止全局变量,禁止偷偷地访问另外一个插件的内部状态。
33 | 一个进程要读取一份配置,只有第一次RPC的时候我们才能监控到,后续缓存在内存里的访问就是不可见的了。
34 | 而插件则不用担心跨插件调用的开销,我们可以把配置缓存在另外一个插件里,这样每次读取配置都是跨插件的调用,从而可以被监控到。
35 |
36 | 插件和进程的核心区别就是插件可以适用于更多的运行时(比如iOS,微信小程序),可以用于拆分运行时调用关系更频繁更紧密的界面和流程。
37 | 进程边界的优势来自于社区共识,提前提供了大量开源的优秀基础设施。
38 | 但是并不意味着,除了进程边界,我们不能在进程内再为每个 git 仓库划出边界来,只是要付出一些自研代价罢了。
39 | 社区共识是不够的,在函数边界(特别是同步调用栈之外的其他调用关系),以及插件边界上都没有足够强的规范。
40 | 只要能在公司或者部门级别建立好共识,函数边界和插件边界完全可以满足问题定位的需求,甚至比进程边界做得更好。
41 |
42 | 那么真正的障碍是什么?是变更自主性,变更的粒度和权责问题。这部分在控制变更里讨论。
--------------------------------------------------------------------------------
/docs/Part2/ProcessBoundary/README.md:
--------------------------------------------------------------------------------
1 | 把不同的 Git 仓库跑在不同的进程里。这样只要看是哪个进程出的问题,就可以知道是由哪个 Git 仓库引起的了。
2 | 进程边界有如下的好处:
3 |
4 | * 完善的跨进程调用监控:相比进程内调用,跨进程调用的监控基础设施要完善得多。因为处理的数据量要小得多。
5 | * 操作系统强制的配额和安全性:就是基础设施更好。都是提前做好,而且充分测试的。
6 | * 隔离的内存状态:进程之间不会共享内存,不会因为共享内存而产生逃逸监控的影响。
7 |
8 | 进程的缺点如下
9 |
10 | * 适用场景:在前端里启动独立的进程不是常规的做法。
11 | * 性能优化会漏掉依赖:经常我们会把配置等数据读取一次之后就缓存在进程内。这部分依赖就很容易逃逸出监控的范围。
12 | * 远离用户:拆分出来的进程往往是越来越靠后台,离用户越远,就越难以倾听到用户的声音。
13 |
14 | 我们的目标是在出问题之后,能从运行时的现象找到对应的Git仓库。但是这个目标真的一定需要使用进程做边界么?
--------------------------------------------------------------------------------
/docs/Part2/README.md:
--------------------------------------------------------------------------------
1 | # Part.2 只对自己写的代码负责
2 |
3 | 这个 Part 我们主要讨论如何拆分进程的事情。标题是“只对自己写的代码负责”,这里包含了两个关键词:
4 |
5 | * 负责:开发者应该对自己的线上服务负责,也就是所谓的 devops。而不是写好了代码之后,甩给运维去管。不仅仅要自己发布变更,也要接告警,定位问题。
6 | * 自己写的:搞别人的代码是很烦躁的,无论是发布变更,还是告警定位,我们都希望能快速验证自己写的代码是不是有问题,然后把锅甩出去。
7 |
8 | 要达到“只对自己写的代码负责”是非常高的追求,需要很强的基础设施的支撑。我们经常会抱怨单元测试不好做,线上bug无法复现之类的问题。
9 |
10 | ## 微服务
11 |
12 | 微服务的起源是 devops 运动。鼓励 dev 拥有自己的线上服务,dev 自己来做 ops 的工作从而减少沟通成本。
13 | 毫无疑问,微服务的合理性并不是从 Autonomy 或者 Consistency 出发,而是以 Feedback 为主要的出发点。
14 |
15 | 当一个 7*24 的互联网高并发应用稳定性成问题的时候,最合理的做法是什么?据 Google SRE 统计,线上70%的故障都是由某种变更而触发的。
16 | 控制变更的速度,尽量延长灰度发布的时间是最重要的事情。
17 | 如果变更的粒度只有进程,而进程又只有一个,势必上线的队列会过长。
18 | 此时拆分微服务就是延长灰度发布时间最有效的手段。
19 | 同时让每个 dev 直接负责线上服务的稳定性告警,可以极大加快故障的定位和处置速度。
20 |
21 | 那么是不是拆分进程一时爽,一直拆分一直爽呢?
22 | 很多过度使用微服务的分布式系统无一例外地遇到了严重的 Feedback 问题。
23 | 难道应该是强调一个业务就应该由一个团队端到端负责,重新打起 Monolith 的大旗吗?
24 | 这其实就等价于说,怎么分技术的进程,应该取决于业务部门的组织架构。
25 | 但是业务部门的组织架构也是三天两头调整的。就是技术部门想要跟着调,业务部门每调整一次,集群就完全重部署一次SRE也不会同意的。
26 |
27 | 那么怎么拆分进程才是合理的呢?为什么这么拆就是合理的呢?
28 |
29 | ## 拆分进程的目标
30 |
31 | 在讨论应该如何去拆之前,我们先来看一下[进程拆分不好导致的症状,以及度量 Feedback 维度做得如何的指标](./FeedbackMetrics.md)。
32 |
33 | ## 发布变更,告警定位该如何做?
34 |
35 | 只对自己写的代码负责要体现在发布变更,告警定位这两个环节里。
36 |
37 | * [发布变更](./ControlChange.md):变更之前可以工作,加入了我的变更之后不工作了,那就是我的变更引起的问题。如果不能有效地隔离自己的变更,就要被迫去处理别人写的代码。
38 | * [多进程](./MultiProcess/README.md):把单体进程切分成多进程。一次只变更其中的一个。
39 | * [多租户](./MultiTenancy/README.md):把所有的业务数据分成租户。一次只升级一个租户的数据和代码。
40 | * [多变种](./MultiVariant/README.md):分租户还是粒度太粗了,比如说挂掉一个城市也是不可接受的。那么可以在线上同时运行多个版本的代码,然后逐步的切流量。
41 | * [告警定位](./ControlBoundary.md):接到告警了如何能快速定位到问题。其核心就是需要在你的代码和别人的代码之间有统一方式定义的边界。不需要知道边界里面的代码是怎么写的,只要看一眼边界上的监控数据就能快速排除不是自己的问题。然后把锅甩出去。
42 | * [进程边界](./ProcessBoundary/README.md)
43 | * [函数边界](./FunctionBoundary/README.md)
44 | * [插件边界](./PluginBoundary/README.md)
45 |
46 | ## 拆进程不是唯一选择,应该 Autonomy 优先
47 |
48 | 回到微服务应该怎么拆分,多少个进程是合理的。
49 | 相比 [ProcessBoundary](./ProcessBoundary/README.md),我认为控制好 [FunctionBoundary](./FunctionBoundary/README.md) 和 [PluginBoundary](./PluginBoundary/README.md) 更有利于 Feedback。
50 | 相比 [MultiProcess](./MultiProcess/README.md),我认为一开始就设计好 [MultiTenancy](./MultiTenancy/README.md) 和 [MultiVariant](./MultiVariant/README.md) 更有利于 Feedback。
51 | 进程之所以好用是因为开源社区提供了很完善的基础设施。所以我们习惯了迁就于已有的技术设施,来切分进程以利用上这些现成的设施。
52 | 从而因为进程的切分来影响我们对Git仓库和团队的切分。
53 |
54 | 如果能够做好 [FunctionBoundary](./FunctionBoundary/README.md),[PluginBoundary](./PluginBoundary/README.md),[MultiTenancy](./MultiTenancy/README.md) 以及 [MultiVariant](./MultiVariant/README.md),怎么分进程根本就不是一个问题。
55 | 所有的代码都跑在一个进程内,一样可以把 Feedback 做得很好。
56 | 那微服务的拆分,其实就等价于Git仓库的拆分,也就是 Autonomy 章节中讨论的问题,怎么拆团队拆Git仓库的问题。
57 | 其主旨原则就一条:拆分之后,接口的定义要稳定,不要天天修改,导致频繁的跨团队强协作。我们也对怎么量化这个原则给出了指标计算的方法。
58 |
59 | 所以 [Autonomy](../Part1/AutonomyMetrics.md) 优先,[Feedback](./FeedbackMetrics.md) 的问题,交给基础架构部门通过改进基础设施来解决。
--------------------------------------------------------------------------------
/docs/Part3/Prefab.md:
--------------------------------------------------------------------------------
1 | ## 复用不复用那是产品经理的工作
2 |
3 | Consistency 和 Reuse 的出发点是不同的。
4 | 我对这两个词的感觉是,Reuse 是从所有代码中找重复,然后努力抽取出可复用的东西。
5 | Consistent 则是我先定义了一个标准,比如说UI规范,然后强制要应用到所有的页面上。如果没有应用上,那就得说明理由,引入的不一致是有意为之,还是偶然的设计失误。
6 | Consistent 隐含了先有共识(Consensus)的含义,就是产品和开发团队达成了什么是必须一致必须复用的共识。
7 | 而 Reuse 总有开发团队一厢情愿的意味在里面,依赖了每个人做新需求的时候去主动地消除重复。
8 |
9 | 以“省事”为理由,强行“复用”一套实现是长久不了的,这个出发点很容易被挑战。
10 | 只要不影响到用户可见的一致性,不影响到 Autonomy,不影响到 Feedback,实现写两遍又如何?
11 | 不要指望通过 reuse 一堆[相乘关系的 git 仓库](../Part1/Composition.md)来减少代码量,这不值得。产品经理设计出来的需求如果是没有规律性的,别强行复用。能用相加的关系组合好一堆 git 仓库就不错了。没有啥好 trade off 的,就是 autonomy 为先。打工人都希望做一些有leverage的事情,都希望多做一些可复用,可被渊源流传的事情,然而人要接受现实。我们业务开发和装修工人没啥区别,按面积收费的,涂一面墙,也就是影响了这一面墙。
--------------------------------------------------------------------------------
/docs/Part3/README.md:
--------------------------------------------------------------------------------
1 | # 突出大逻辑,隐藏小细节
2 |
3 | 这个 Part 是讲如何“分层”的。Git 仓库拆分的首要目标是[“代码防腐”](../Part2/README.md),其次才是“突出大逻辑,隐藏小细节”,因为我们使用的开源软件(编程语言,库,框架)会逐步把这些隐藏细节的东西都做进去。在未来我们会有更高级的语言和框架可选择,而不用自己来重复发明这些。代码分层是让代码更可读,锦上添花的事情,而不应该成为业务逻辑切分时的首要考虑。自研黑魔法框架往往是次优的选择,应优先选择开源社区的大众化技术方案。
4 |
5 | 隐藏小细节是为了
6 |
7 | * [提高信噪比](./SignalNoise.md):当我们读一段的代码时候,更大概率快速获得自己想要的信息,而不是被无关的噪音淹没
8 | * 提高可移植性:例如我们为微信小程序开发的代码,应该能移植到字节小程序上去,而不是和具体的平台完全绑死
9 | * 装配式低代码开发:如果能限定需求的多样性,发明领域特定语言(DSL)来装配“预制件”,从而比“现浇混凝土”实现常规需求更快
10 |
11 | 度量代码分层不需要新的指标,和“代码防腐”中提出的指标是一样的:
12 |
13 | * [度量 Autonomy 的指标](../Part1/AutonomyMetrics.md)
14 | * [度量 Consistency 的指标](../Part1/ConsistencyMetrics.md)
15 |
16 | ## 如何隐藏小细节
17 |
18 | 以下是常见的经典问题,我们有各种各样的“黑魔法”来达成隐藏细节的目的。
19 |
20 | TODO
21 |
22 | * 隐藏I/O
23 | * 隐藏“日志监控”细节
24 | * 隐藏“展示刷新”细节
25 | * 隐藏“数据库读写”细节
26 | * 隐藏“统计数据刷新”细节
27 | * 隐藏“集群变更”细节
28 | * 配置化声明式编程,把复杂的现象分解为“一般性”和“特殊性”两部分
29 | * 隐藏“优化参数拟合目标函数”细节
30 | * [需求一致得像一个模子出来的](./Prefab.md)
31 |
--------------------------------------------------------------------------------
/docs/Part3/SignalNoise.md:
--------------------------------------------------------------------------------
1 | 阅读下面这段代码
2 |
3 | ```java
4 | public class ReserveNumberService {
5 |
6 | private final static Logger LOGGER = LoggerFactory.getLogger(SelectNumberService.class);
7 |
8 | @Inject
9 | SqlMapClient sqlMapClient;
10 |
11 | public void reserverNumber(String userId, String number) {
12 | try {
13 | String oldNumber = (String) sqlMapClient.queryForObject("reservedNumber", userId);
14 | releaseOldAndReserveNew(userId, oldNumber, number);
15 | } catch (SQLException e) {
16 | LOGGER.error("failed to reserve number " + number + “ for user “ + userId, e);
17 | }
18 | }
19 |
20 | private void releaseOldAndReserveNew(String userId, String oldNumber, String newNumber) throws SQLException {
21 | sqlMapClient.startTransaction();
22 | try {
23 | if (oldNumber != null) {
24 | sqlMapClient.update("releaseNumber", oldNumber);
25 | }
26 | HashMap params = new HashMap();
27 | params.put("number", newNumber);
28 | params.put("usreId", userId);
29 | if (sqlMapClient.update("reserveNumber", params) == 0) {
30 | throw new RuntimeException(newNumber + " already reserved by someone else");
31 | }
32 | sqlMapClient.commitTransaction();
33 | } finally {
34 | sqlMapClient.endTransaction();
35 | }
36 | }
37 | }
38 | ```
39 |
40 | 你的第一印象是什么?我的印象是满屏的sqlMapClient。你能第一眼就说出这段代码中出现了哪些概念,又解决了什么问题吗?或许不是那么容易的事情。如果用DDD的方式来写,也许是这样:
41 |
42 | ```java
43 | public class PhoneNumber {
44 |
45 | private final String number;
46 | private User reservedBy;
47 |
48 | public PhoneNumber(String number) {
49 | this.number = number;
50 | }
51 |
52 | public void reservedBy (User user) {
53 | reservedBy = user;
54 | }
55 |
56 | public void release() {
57 | reservedBy = null;
58 | }
59 | }
60 | public class User {
61 |
62 | private PhoneNumber reservedNumber;
63 |
64 | public void reserve(PhoneNumber number) {
65 | if (reservedNumber != null) {
66 | reservedNumber.release();
67 | }
68 | number.reservedBy(this);
69 | this.reservedNumber = number;
70 | }
71 | }
72 | ```
73 |
74 | 通过阅读上面的代码,我们会发现其中牵涉到了两个领域概念,分别是User和PhoneNumber。要建模的问题其实是User如何Reserve一个PhoneNumber。业务逻辑是PhoneNumber只能被一个User预定,而且一个User只能预定一个PhoneNumber,如果要预定新的必须先释放旧的。
75 |
76 | 也许上面的那段业务分析可能就出现在selectNumber这个方法的注释里,也可能是在某某详细设计文档里。但是为什么不能让代码自身反映出这些业务上的概念呢?在我看来,让代码成为永不过期的文档就是DDD整套理论的出发点。
77 |
78 | 使用DDD,当我们写代码的时候,就可以关注在PhoneNumber和User的逻辑关系上。当我们和其他开发人员交流的时候,就可以讲PhoneNumber如何如何,User如何如何,而不用说某某表的某某字段是什么值。当我们和业务人员交流的时候,至少我们不用在对话里提到sqlMapClient如何如何。
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # 症状
2 |
3 | 如果没有做好业务逻辑拆分,可能在项目晚期造成以下三种问题:
4 |
5 | * 拆了微服务之后做一个需求要拉很多人,代码写进来了就再也删不掉了
6 | * 要么放任自流,1个App里有4种日期选择方式。要么用力过猛,抽象出来的营销接口动辄几百个参数
7 | * 线上出了问题很难定位到谁引起的,本地做不了任何有意义的测试,反馈周期特别长
8 |
9 | 为何随机选择了这三种问题并归纳为业务逻辑拆分问题呢? 因为我认为以上三种问题都是由同一个不易变化的本质约束所造成。这个本质约束就是人类的感知与沟通速度是很慢的。
10 | 所谓业务架构,其实质就是想尽一切办法减少沟通。只有沟通少,效率才会高,质量才会好。就是这么简单的一件事情。
11 |
12 | # 代码腐化
13 |
14 | 没有哪份代码一开始是不想好好写的。大家在开始落笔之前都知道会出现上述三种症状,并且都自认为做好了设计去避免这些问题。然而往往事情的发展不是如自己所想的那样。我们可能会把代码腐化归咎于自己不够努力,或者需求太多了做得太匆忙。如果当初更用心一点就不会这样。是吗?
15 |
16 | 如果你带了一个新人,他可能会问你“我这个需求的代码应该写在哪个Git仓库里?”。然后你会根据你的直觉做出一个判断。这种高度依赖于某个人“我觉得”的决策模式是长久不了的。只要摆放代码严重依赖于某个人的判断,腐化就是无法阻挡的趋势。那有没有什么办法可以像“防腐剂”那样,在一开始的时候放进去,然后就可以保持很长时间的“新鲜”呢?
17 |
18 | 实现方案也不复杂,代码分成两部分。可以随便乱写的,和不能随便乱写的,这两部分。不知道“组合代替继承”的小朋友,只能在可以随便乱写的那部分里发挥。如果大部分代码量都在“可以随便乱写”的那部分里,那么只要很少的人就可以看住“不可随便乱写”的那部分。
19 |
20 | 说简单也不简单,怎么做到呢?
21 |
22 | # 你说的东西可以落地吗?
23 |
24 | 大部分人的日常工作都是维护一个已有的项目,没有几个人能够参与到 Greenfield 项目的初始设计阶段。这也是大部分读者所懊恼的地方,“我读你的东西有什么用,我这项目就已经烂成了这个样子了,我也改不了”。我希望能够出一些可度量的指标。这样对于现有的项目,我们可以拿这些指标去度量这些问题有多严重。
25 | 当下次别人问你微服务为什么这么拆,而不是那么拆的时候,你可以给出令人信服的理由,而不是“我觉得”。
26 |
27 | 我不是销售像 Angular 那样可以直接 git clone 一份的代码框架给你。我想分享的是一套可以适用于任何语言和框架的分解业务逻辑的套路。从《Clean Architecture》和《Domain Driven Design》你应该已经学到了很多了。如果仍然不会拆,那么可以再读读《业务逻辑拆分模式》试试。
28 |
29 | * [拆分成什么呢?](./Modules.md)
30 | * [Part.1 代码防腐](./Part1/README.md)
31 | * [Part.2 只对自己写的代码负责](./Part2/README.md)
32 | * [Part.3 突出大逻辑,隐藏小细节](./Part3/README.md)
33 |
--------------------------------------------------------------------------------
/docs/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-slate
2 | title: 业务逻辑拆分模式
3 | description: https://autonomy.design
4 | google_analytics: UA-311653-15
5 | lang: zh-CN
6 | markdown: kramdown
7 |
--------------------------------------------------------------------------------
/docs/_layouts/default.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | {% seo %}
12 |
13 |
14 |
15 |
16 |
17 |
34 |
35 |
36 |
37 |
40 |
41 |
42 |
43 |
51 |
52 | {% if site.google_analytics %}
53 |
61 | {% endif %}
62 |
63 |
64 |
--------------------------------------------------------------------------------
/docs/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/docs/favicon.ico
--------------------------------------------------------------------------------
/docs/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/docs/favicon.png
--------------------------------------------------------------------------------
/docs/vue-db/CNAME:
--------------------------------------------------------------------------------
1 | vue-db.js.org
--------------------------------------------------------------------------------
/docs/vue-db/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-modernist
2 | markdown: kramdown
--------------------------------------------------------------------------------
/docs/vue-db/demo-counter/assets/index.63b6af3c.css:
--------------------------------------------------------------------------------
1 | .fade-enter-active{transition:all .3s ease-out}.fade-leave-active{transition:all .8s cubic-bezier(1,.5,.8,1)}.fade-enter-from,.fade-leave-to{transform:translate(20px);opacity:0}
2 |
--------------------------------------------------------------------------------
/docs/vue-db/demo-counter/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/docs/vue-db/demo-counter/favicon.ico
--------------------------------------------------------------------------------
/docs/vue-db/demo-counter/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Vite App
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/docs/vue-db/demo-flat-form/assets/index.8aac1eb8.css:
--------------------------------------------------------------------------------
1 | .row[data-v-20bc85b6]{display:flex;column-gap:8px}fieldset[data-v-554f00a0]{width:800px;display:flex;flex-direction:column;row-gap:8px;align-items:flex-end}#app{font-family:Avenir,Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-align:center;color:#2c3e50;margin-top:60px}
2 |
--------------------------------------------------------------------------------
/docs/vue-db/demo-flat-form/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/docs/vue-db/demo-flat-form/favicon.ico
--------------------------------------------------------------------------------
/docs/vue-db/demo-flat-form/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Vite App
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/docs/vue-db/demo-nested-form/assets/index.d502520f.css:
--------------------------------------------------------------------------------
1 | .fields[data-v-29a4da50]{display:flex;column-gap:8px}form[data-v-22008459]{width:800px;display:flex;flex-direction:column;row-gap:8px;align-items:flex-end}#app{font-family:Avenir,Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-align:center;color:#2c3e50;margin-top:60px}
2 |
--------------------------------------------------------------------------------
/docs/vue-db/demo-nested-form/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/docs/vue-db/demo-nested-form/favicon.ico
--------------------------------------------------------------------------------
/docs/vue-db/demo-nested-form/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Vite App
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/docs/vue-db/demo-nested-resource/assets/index.a79acb0e.css:
--------------------------------------------------------------------------------
1 | #app{font-family:Avenir,Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;color:#2c3e50;margin-top:60px}
2 |
--------------------------------------------------------------------------------
/docs/vue-db/demo-nested-resource/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/docs/vue-db/demo-nested-resource/favicon.ico
--------------------------------------------------------------------------------
/docs/vue-db/demo-nested-resource/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Vite App
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/docs/vue-db/demo-server-side-render/assets/index.06d14ce2.css:
--------------------------------------------------------------------------------
1 | #app{font-family:Avenir,Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-align:center;color:#2c3e50;margin-top:60px}
2 |
--------------------------------------------------------------------------------
/docs/vue-db/demo-server-side-render/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/docs/vue-db/demo-server-side-render/favicon.ico
--------------------------------------------------------------------------------
/docs/vue-db/demo-server-side-render/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Vite App
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/docs/vue-db/demo-todo-client/assets/index.2dca2adb.css:
--------------------------------------------------------------------------------
1 | #app{font-family:Avenir,Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-align:center;color:#2c3e50;margin-top:60px;width:800px;display:flex;flex-direction:column;row-gap:8px;align-items:flex-start}
2 |
--------------------------------------------------------------------------------
/docs/vue-db/demo-todo-client/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/docs/vue-db/demo-todo-client/favicon.ico
--------------------------------------------------------------------------------
/docs/vue-db/demo-todo-client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Vite App
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/docs/vue-db/demo-todo-local/assets/index.2dca2adb.css:
--------------------------------------------------------------------------------
1 | #app{font-family:Avenir,Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-align:center;color:#2c3e50;margin-top:60px;width:800px;display:flex;flex-direction:column;row-gap:8px;align-items:flex-start}
2 |
--------------------------------------------------------------------------------
/docs/vue-db/demo-todo-local/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/docs/vue-db/demo-todo-local/favicon.ico
--------------------------------------------------------------------------------
/docs/vue-db/demo-todo-local/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Vite App
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/docs/vue-db/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | title: vue-db
4 | titleLink: https://github.com/taowen/vue-db
5 | description: Get rid of as many mutable states as possible
6 | ---
7 |
8 | ## About
9 |
10 | The sole goal of [vue-db](https://github.com/taowen/vue-db/tree/main/packages/vue-db/src/index.ts) is to unleash [vue 3 reactivity system](https://vuejs.org/api/reactivity-core.html) full potential to get rid of as many mutable states as possible. When other state management library encrouages maintaining a separate copy of data store, vue-db tries to do the opposite.
11 |
12 | * direct cross component data sync, such as form
13 | * load data from backend and keeping it up to date
14 | * type-safe RPC with graph query opt-in
15 | * server side rendering (SSR) data fetching
16 |
17 | It looks like a lot, but it is a 500 line library only depending on vue. Install it via `npm install vue-db`, then register to vue app
18 |
19 | ```ts
20 | import * as vdb from 'vue-db'
21 |
22 | app.use(vdb);
23 | ```
24 |
25 | ## Form
26 |
27 | Instead of using https://vuex.vuejs.org/ to hold the state, vue-db use the vue component instance itself as data store.
28 |
29 | * user type inputs into the form components through ui
30 | * in computed property, use `vdb.load` or `vdb.query` to locate the data source and keep data in sync
31 | * when submit, use `vdb.walk` to dump the form state out and show validation error back
32 |
33 | Checkout following examples
34 |
35 | | code | live | demo |
36 | | --- | --- | --- |
37 | | [counter](https://github.com/taowen/vue-db/tree/main/packages/demo-counter) | [counter](https://autonomy.design/vue-db/demo-counter) | `vdb.load` with $root from current page |
38 | | [flat form](https://github.com/taowen/vue-db/tree/main/packages/demo-flat-form) | [flat form](https://autonomy.design/vue-db/demo-flat-form) | `vdb.walk` to dump form state |
39 | | [nested form](https://github.com/taowen/vue-db/tree/main/packages/demo-nested-form) | [nested form](https://autonomy.design/vue-db/demo-nested-form) | `vdb.load` with $parent allowing multiple form instances |
40 | | [todo list](https://github.com/taowen/vue-db/tree/main/packages/demo-todo-local) | [todo list](https://autonomy.design/vue-db/demo-todo-local) | `vdb.waitNextTick` to add new todo item |
41 |
42 | ## Async data binding
43 |
44 | Data from backend need to be loaded asynchronously. Instead of using a mutable https://vuex.vuejs.org/ store to hold the backend data, vue-db provides async data binding to bind vue component data with backend table.
45 |
46 | * `vdb.defineResource` to describe the data to be loaded from server table. table name is just a string, the server can interpret it as anything. vue-db does not specify the rpc protocol, and does not include any server side implementation.
47 | * `vdb.query` or `vdb.load` the resource defined, bind to vue component data
48 | * render page with the data. as async data loading takes time, this time the data will be empty array containing nothing.
49 | * server responded with data, which triggers the vue component to re-render again
50 | * `vdb.defineCommand` to define a function that can be used to call server to update data
51 | * user clicked some button calling the command, which use its `affectedTables` definition to trigger component rerender
52 |
53 | Checkout following examples
54 |
55 | | code | live | demo |
56 | | --- | --- | --- |
57 | | todo [client](https://github.com/taowen/vue-db/tree/main/packages/demo-todo-client) [server](https://github.com/taowen/vue-db/tree/main/packages/demo-todo-server) | todo [client](https://autonomy.design/vue-db/demo-todo-client) server | `vdb.defineResource` and `vdb.defineCommand` to bind with backend data |
58 |
59 | ## Type-safe RPC
60 |
61 | If both server and client are written in typescript, the `.d.ts` file can be used as type-safe RPC data schema. vue-db allow you to `import type` and hand-write a RPC stub with very little code, instead of resorting to full blown code generation solution. Also `vdb.defineResource` support declaring nested resource, allow client to query for a object graph in one RPC roundtrip. However, the wire-protocol and server side implementation is excluded from the scope. vue-db is just a tiny library depending only on vue 3, it will not enforce a server/client framework to you.
62 |
63 | Checkout following examples
64 |
65 | | code | live | demo |
66 | | --- | --- | --- |
67 | | [nested resource](https://github.com/taowen/vue-db/tree/main/packages/demo-nested-resource) | [nested resource](https://autonomy.design/vue-db/demo-nested-resource) | `vdb.defineResource` refer other resource |
68 |
69 | ## SSR
70 |
71 | Fetching initial data for server side rendering is a hard job. It normally requires you to extract out async data dependency into a central place, which makes CSR and SSR code different. vue-db aims to making same component code run in both client and server. You no longer need to lift the data fetching logic to page level, every component can declare its own async data dependency.
72 |
73 | * async data fetched in server side
74 | * intercept component `render` function to dehydrate the state into `data-dehydrated` attribute of rendered html element
75 | * client side got the html and start hydration
76 | * define component `beforeMount` lifecycle hook, read `data-dehydrated` and set state into component data
77 |
78 | vue-db can work with any SSR framework, we recommend [vue-fusion](https://github.com/taowen/vue-fusion)
79 |
80 | Checkout following examples
81 |
82 | | code | live | demo |
83 | | --- | --- | --- |
84 | | [static page](https://github.com/taowen/vue-db/tree/main/packages/demo-static-page) | static page | renderToString in node with async data provided by `vdb.query` |
85 | | [server side render](https://github.com/taowen/vue-db/tree/main/packages/demo-server-side-render) | [server side render](https://autonomy.design/vue-db/demo-server-side-render) | async data `vdb.query` in server side, then hydrated in client side |
--------------------------------------------------------------------------------
/feedback/README.md:
--------------------------------------------------------------------------------
1 | 从 Feedback 的角度来看
2 |
3 | * [模块切分的好坏标准是什么?](#criteria-of-modularization)
--------------------------------------------------------------------------------
/modularization.md:
--------------------------------------------------------------------------------
1 | ## Autonomy
2 |
3 | 所有人似乎都在修改同一个文件,例如 order.php。所有需求似乎都堆积在少数几个人的身上。所有团队似乎都等着同一个模块的上线,但是一天只有24小时,没有足够的时间去在生产环境验证新部署的可靠性。公司里的 PMO 越来越多,项目的感谢名单越来越长。
4 |
5 | ## Consistency
6 |
7 | 很多地方都在写非常类似的代码,比如每个 RPC 调用之后,都要手工再写一行日志。所有的列表页面,都要重复实现一次分页,排序等非常类似的功能。当出现性能问题,稳定性问题的时候,似乎需要修改一亿个地方才能改完。每当你发现 ctrl-c / ctrl-v 的代码的时候,代码已经被复制过 N 份了,甚至已经上线了。这个时候让人家去返工,纯属没事找事。所有的产品经理都视你为敌,因为你总是以复用之名,漠视他们的体验微创新的巨大价值。
8 |
9 | ## Feedback
10 |
11 | 单元测试不要说一个函数了。就是一个 git 仓库也往往是跑不起来的。不要说在开发笔记本上跑不起来了,就是在机房的测试环境里都经常跑不起来的,依赖联调作战。全球全宇宙的孤本可能就是那个所谓的生产环境了。出了错也无法很快定位到是哪个模块出的问题。要么是没有足够的日志,要么是日志太多了,把信号淹没在噪音的海洋里了。获得 Feedback 的时间以天为单位,甚至以周为单位。
12 |
13 | # 模块化的三个目的
14 |
15 | * Autonomy:减少沟通成本,一个需求改的模块数尽可能的少,一个模块的负责团队需要的知识边界尽可能的小。
16 | * Consistency:在保障 Autonomy 的情况下,通过模块复用,减少不必要的不一致性。
17 | * Feedback:降低获得反馈的成本,让新人能够快速上手。最有效的“文档”是可工作的软件本身提供的交互式学习环境。拆成模块之后可能就不需要整体跑起来也可以获得反馈。
18 |
19 | Modularization 是很容易达成的,可以以任意地方式切分出各种形态的模块。然而评判 Modularization 的收益是非常困难的,大致可以归纳为以上三个难以量化的方面。这也就使得 Modularization 更像一门艺术而不是科学,谁也无法说服别人自己的 Modularization 方案比他们的要“好”。同时这三个方面的收益都是“降低成本”的收益,以下是常见的不切实际的预期:
20 |
21 | * 降低新需求的交付时间:这很难达到,新需求往往是全新的。不能为了快而强行复用。
22 | * 增加收入:降本和增收是两码事
23 | * 获得比较竞争优势,达成业务成功:相比内燃机,集成电路等技术带来的成本急剧下降,Modularization 的改良对象是人类的协作,是不可能有特别大幅度的成本降低的。
24 |
25 | 既然只是降低有限成本的技术,为什么还值得软件开发人员去追求呢? 因为人不是没有情感的机器,从 Autonomy, Consistency, Feedback 三个方面,从业者可以收获人类本能的喜悦。对于从业者自身的情感价值,恐怕要远大于对资本方的价值。没有证据可以证明,把人异化为螺丝钉,996 的工作制不能交付出有商业价值的软件。也没有证据证明,高市值的软件企业不用堆人的方式工作,可以获得更高的市场估值。以 Consistency 为例,拉一个横向团队,发动一堆人都改一遍可能并不会带来特别大的成本。业务逻辑本来就是“没有逻辑”,强行归纳出规律本质上实在抑制业务创新。技术人员追求复用带来的 Consistency 一部分是对称美学带来的愉悦感驱使,而不完全是降成本为出发点。单元测试带来的秒级别的快速 Feedback 也是让人上瘾的机制。当浏览器重加载时间超过 5s 之后,人类可以明显感受到焦虑感的非线性上升。这些都是人类不是机器,人类有情感追求的例证。
26 |
27 | 假定 Modularization 仍然是值得追求的技术,那么为什么是 Autonomy, Consistency, Feedback,而不是其他的分法呢? 可以看如下的模块依赖关系图
28 |
29 | 
30 |
31 | 假设对于 B 来说
32 |
33 | * Autonomy:是指不用关心自己边界之外的事情。比如 C 和 D,相对 B 来说,就是完全没有关系的模块。也就是“确保不应该产生依赖的模块,不产生依赖”
34 | * Consistency:是指一个模块改了,所有依赖的地方都连着变了。比如改了 A,也就是改了 B、C、D。也就是“确保应该复用的模块,被复用”
35 | * Feedback:是指每个模块能多高效率的获得反馈。B是连着A,就可以获得反馈,还是要ABCDE都集成到一起才能获得反馈。如果获得整体反馈之后,是否可以把反馈拆解到模块。也就是“模块如何集成起来工作,又如何把反馈拆解回模块”
36 |
37 | 可见 Autonomy, Consistency, Feedback 完整覆盖了依赖的上游,下游,以及集成。这里剥离了具体的业务,是为了从抽象的层面看到这个分类法的完备性。接下来我们以具体的案例来看,从 Autonomy, Consistency, Feedback 三个视角,如何优化。
38 |
--------------------------------------------------------------------------------
/old-README.md:
--------------------------------------------------------------------------------
1 | # 从实际业务案例,讲解应该如何拆分模块
2 |
3 | 有太多的文章教你怎么组织代码了。但是这些文章大都是系统A,模块B的抽象写意派。虽然看着很有道理的样子,但就是看不懂。
4 | 本文的特点是有十多个带有具体业务场景的例子。从如何接新需求的角度来分析模块应该怎么拆分。
5 | **主要的内容都在例子里**,请不要直接看结论,相信我,只看结论等于没看。
6 |
7 | * [模块化的三个目的](./modularization.md)
8 | * [Autonomy: 减少沟通成本,一个需求改的模块数尽可能的少,一个模块的负责团队需要的知识边界尽可能的小。](./autonomy/README.md)
9 | * Consistency: 在保障 Autonomy 的情况下,通过模块复用,减少不必要的不一致性。
10 | * Feedback: 降低获得反馈的成本,让新人能够快速上手。最有效的“文档”是可工作的软件本身提供的交互式学习环境。拆成模块之后可能就不需要整体跑起来也可以获得反馈。
11 | * [Patterns](./patterns/README.md)
12 |
13 |
--------------------------------------------------------------------------------
/patterns/README.md:
--------------------------------------------------------------------------------
1 | 能不能把“行不通”的种种障碍逐一分析以下,给每种类型的障碍提供一个切实可行的解决方案呢?
2 | 也就是我们能不能给一个设计模式的列表,所谓设计模式就是“问题清单”。
3 |
4 | ## 如何发明底层抽象?
5 |
6 | [【阅读该模式】](./how-to-invent-abstraction)
7 |
8 | 如何才能像大师一样,上来就知道抽象的接口应该如何定义? 我为什么总是想不出来该怎么抽象?
9 |
10 | ## 分模块之后代码不好读了怎么办?
11 |
12 | [【阅读该模式】](./how-to-maintain-readability)
13 |
14 | 为了代码复用,拆了很多个模块,导致代码不好阅读了怎么办? 之前代码虽然写的挫,但是 ctrl+f 在一个文件里就可以找到对应的代码,现在找段代码可费劲了。
15 |
16 | ## 代码写成活的比写成死的要麻烦多了怎么办?
17 |
18 | [【阅读该模式】](./how-to-lower-plugin-tax-rate)
19 |
20 | 开槽,开插件,都是为了把代码写得更灵活。但是每一开一个扩展点,就需要写一堆样板代码。如何才能降低“插件税”呢?
21 |
22 | ## 怎么扩展额外的字段呢?
23 |
24 | [【阅读该模式】](./how-to-add-field)
25 |
26 | 经常看见一个表里面有 extra_fields 之类的字段,里面放一个大 JSON。每个需求都要加新字段怎么弄?
27 |
28 | ## 怎么支持新的订单类型?
29 |
30 | [【阅读该模式】](./how-to-add-new-order-type)
31 |
32 | 为了支持新的业务,往往需要给 OrderType 这个字段上添加新的订单类型。为了不影响已有的业务,还经常要加 isNewBiz 这样的“Flag”来标识新的业务场景。除了一直加新的Flag,就没别的办法了吗?
33 |
34 | ## UI组合为何难以落地?
35 |
36 | [【阅读该模式】](./ui-composition-obstacles)
37 |
38 | 松耦合模块边界的最佳范例就是基于 UI 的组合。但是为什么在过去的历史经验里,这样的模块切分的方式很难落地? 有没有什么具体的技术方案可以借鉴?
39 |
40 | ## 事件驱动为何难以有收益?
41 |
42 | [【阅读该模式】](./event-driven-obstacles)
43 |
44 | 事件驱动是大家谈论解耦的时候寄予众望的技术。但是具体到实际的项目中,经常发现事件驱动没有发挥出什么作用。为什么会这样?
45 |
--------------------------------------------------------------------------------
/patterns/event-driven-obstacles/README.md:
--------------------------------------------------------------------------------
1 | # 问题
2 |
3 | 事件驱动是大家谈论解耦的时候寄予众望的技术。但是具体到实际的项目中,经常发现事件驱动没有发挥出什么作用。为什么会这样?
4 |
5 | # 分析
6 |
7 | ## 基于事件的数据库同步
8 |
9 | 事件驱动经常变成一种数据库的同步技术。构造一个 Read Model 是有价值的,也是一种重要的解耦合手段。但是事件驱动的 Read Model 同步未必是最佳的实现技术。
10 |
11 | 【TODO,给个例子】
12 |
13 | ## 不要返回值?
14 |
15 | 核心的问题在于事件驱动是没有返回值的。那业务逻辑上这个返回值怎么反馈给用户呢? 除了发通知,发短信等天然的异步场景,大部分业务流程都是要反馈结果,通知用户下一步操作的。事件驱动最大的难题在于产品的交互设计上。
16 |
17 | 【TODO,给个例子】
--------------------------------------------------------------------------------
/patterns/how-to-add-field/README.md:
--------------------------------------------------------------------------------
1 | # 问题
2 |
3 | 经常看见一个表里面有 extra_fields 之类的字段,里面放一个大 JSON。每个需求都要加新字段怎么弄?
4 |
5 | # 分析
6 |
7 | ## 加字段就是加业务流程
8 |
9 | 加字段只是一个表象,任何字段都有一堆相关联的业务逻辑。
10 | 本质上数据要持久化是因为业务流程需要中断引入的。
11 | 比如顾客点菜让厨师去制作,如果厨师就站在旁边,直接告诉他去做就行了。
12 | 厨师不在旁边,就得先把菜写在单子上,传给后厨去制作。
13 | 因为多个角色的工作是先后进行的,中间需要一个载体来承载过去的“需求/承诺/契约/合同”,所以才需要持久化。
14 | 加字段,代表了新的业务流程。
15 |
16 | ## 通过加表来实现加字段
17 |
18 | 最常见的扩展需求是要订单加字段。订单上记录了顾客的需求,记录了处理过程,记录了客服的处置,每个需求都可以找到一个给订单加字段的理由。
19 | 我们除了订单,还需要报价单,还需要送货记录,还需要退款记录,还需要客服补偿申请。这些表的名字看起来都挺合理,为什么就一定要往订单上堆呢。
20 | 加字段都可以转化为加表来实现。
21 | 从依赖管理的角度来说,是新增模块来满足新需求,而不是修改底层模块来满足新需求。
22 |
23 |
--------------------------------------------------------------------------------
/patterns/how-to-add-new-order-type/README.md:
--------------------------------------------------------------------------------
1 | # 问题
2 |
3 | 为了支持新的业务,往往需要给 OrderType 这个字段上添加新的订单类型。为了不影响已有的业务,还经常要加 isNewBiz 这样的“Flag”来标识新的业务场景。除了一直加新的Flag,就没别的办法了吗?
4 |
5 | # 分析
6 |
7 | ## 这里有两个问题
8 |
9 | 添加新的 OrderType,是为了有地方可以用 `if (OrderType === 'DirectBuy') {}` 这样的条件判断,然后目的是为了实现差异化的业务逻辑。
10 | 我们知道,if/else 都可以用面向对象的继承关系来表达。所以可以用 `DirectBuyOrder extends Order` 来代替这个 if/else 的判断。
11 | 但是这么做是行不通的,原因有两条
12 |
13 | * 如果又有一个需求是 Vip 用户的订单给个不同的行为,那么就有 VipOrder 和 VipDirectBuyOrder 了。这样扩展下去还不如写 if/else。
14 | * 持久化到数据库之后,要重新加载回来,还是得有一个 OrderType 的字段来判断订单类型才行。
15 |
16 | ## 组合代替继承
17 |
18 | 除了继承之外,我们还有一种选择是组合。给订单一个“子单据”列表:
19 |
20 | * DirectBuyRecipt
21 | * VipBenefit
22 |
23 | 新的字段,新的行为添加到这些新加的表里。通过查询订单的“子单据”列表,可以找到这些附加的对象。但是这样会有一个缺陷是,调用 Order 上的行为就麻烦了很多。所以要把这些子单据的查找封装到 Order 的行为里,比如
24 |
25 | ```ts
26 | function proceedToCheckout() {
27 | const list = this.listSubEntities();
28 | for (const subEntity of list) {
29 | if (subEntity.canHandle('proceedToCheckout')) {
30 | return subEntity.proceedToCheckout();
31 | }
32 | }
33 | throw new InternalError();
34 | }
35 | ```
36 |
37 | 这样我们就可以用新增子单据的方法,来对 Order 的行为进行扩展,而不用一直扩展 OrderType 了。
38 | 为了效率的原因,listSubEntities 新增的查询也是可以避免的。我们可以把子单据的id和类型信息冗余一份到订单上做为“缓存”。
39 | 这样在不查询存储的情况下,就可以直接拿订单本身的信息去判断要转发给哪个子单据处理。
40 | 这个缓存实质上是对子单据数据的 prefetch,可以和业务逻辑没有任何关系,是一个基础设施的IO优化。
41 |
42 | ## 最终用户编程
43 |
44 | 用户在运行时选择对象进行行为的组装,这个不仅仅代替了 OrderType 这样的静态组装,而且打开了“最终用户编程”的道路。
45 | 店铺装修,营销玩法等很多业务场景下,其实质都是在给最终用户更多的选择,给他们工具去拼装自己的“OrderType”。
--------------------------------------------------------------------------------
/patterns/how-to-invent-abstraction/README.md:
--------------------------------------------------------------------------------
1 | # 问题
2 |
3 | 如何才能像大师一样,上来就知道抽象的接口应该如何定义? 我为什么总是想不出来该怎么抽象?
4 |
5 | # 分析
6 |
7 | 大师也经常想当然
8 |
9 | ## 从底往上
10 |
11 | 大师们鼓吹从底往上构建抽象,把软件工程当数学定理证明一样来搞,这条路是走不通的。业务逻辑和数学不同,业务逻辑就是世界上最没有逻辑可言的东西。
12 |
13 | David Parnas 写过一篇文章“[Designing Software for Ease of Extension and Contraction](./designing-software-for-ease-of-extension-and-contraction.pdf)”。里面提到了虚拟机的愿景,一帮程序员写一个虚拟机,给另外一帮程序员来用。那我怎么知道这个虚拟机的指令集应该设计成什么样呢?
14 |
15 | Trygve Reenskaug 写过一本书“[Working with objects - The OOram Software Engineering Method](./working-with-objects-the-ooram-software-engineering-method.pdf)”。里面有一张图展示了Trygve, MVC 之父对未来架构的愿景
16 |
17 | 
18 |
19 | 这种自底而上,逐层构建抽象的做法是看起来很美好的。但是实践中,我们怎么能知道“抽象”是正确的呢?怎么能凭空发明出来呢?
20 |
21 | ## 高内聚
22 |
23 | 大师们还曾经鼓吹过高内聚
24 |
25 | Glenford J. Myers 写过一本“[Composite/Structured Design](./composite-structured-design.djvu)”。其中提出了一个 Module Strength 的概念。就像我们没有离开地球表面,是受到了地球引力的吸引一样。模块没有散架,是被“高内聚”了。他进而把模块强度分了一个强度等级
26 |
27 | * 最强的内聚:Functional strength and informational strength
28 | * Communicational strength
29 | * Procedure strength
30 | * Classical strength
31 | * Logical strength
32 | * 最弱的内聚:Coincidental strength
33 |
34 | 实践中,这往往会变成给某个模块先取一个名字,然后下一个定义说“xxx类型的业务都往里面放”。从名字开始,望名生义的往里面装东西。
35 | 这种以“高内聚”为出发点的思路和自底而上设计是犯了同一个错误,从一个抽象概念开始。无论你定义多么清楚,名字多么具体。
36 | 抽象的东西总是可以有无数种解读。没有什么天然的“高内聚”。只是恰好在过去的业务里,这一块没有怎么变化过罢了。
37 |
38 | ## 不要去发明抽象
39 |
40 | 这个问题的解决方案就是不要自底而上。而是先使用,再复用。你要先看见了多个变种的具体的业务,实打实地知道在每个业务下所需要的参数是为了什么。然后贯彻本文所述的所有依赖管理原则去构建模块,“抽象”是会自然而然地生长出来的。哪里有什么永恒真理一般的抽象,不过是为了复用一块代码给打出来的“补丁”。在不同的复用目标下,“抽象”也会是不同的。
41 |
--------------------------------------------------------------------------------
/patterns/how-to-invent-abstraction/composite-structured-design.djvu:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/patterns/how-to-invent-abstraction/composite-structured-design.djvu
--------------------------------------------------------------------------------
/patterns/how-to-invent-abstraction/designing-software-for-ease-of-extension-and-contraction.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/patterns/how-to-invent-abstraction/designing-software-for-ease-of-extension-and-contraction.pdf
--------------------------------------------------------------------------------
/patterns/how-to-invent-abstraction/layers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/patterns/how-to-invent-abstraction/layers.png
--------------------------------------------------------------------------------
/patterns/how-to-invent-abstraction/working-with-objects-the-ooram-software-engineering-method.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taowen/modularization-examples/1e774898007eabbb0237485bf89358b59c12a084/patterns/how-to-invent-abstraction/working-with-objects-the-ooram-software-engineering-method.pdf
--------------------------------------------------------------------------------
/patterns/how-to-lower-plugin-tax-rate/README.md:
--------------------------------------------------------------------------------
1 | # 问题
2 |
3 | 开槽,开插件,都是为了把代码写得更灵活。但是每一开一个扩展点,就需要写一堆样板代码。如何才能降低“插件税”呢?
4 |
5 | # 分析
6 |
7 | 开一个插件的成本分为两部分
8 |
9 | * 把接口以某种方式定义出来
10 | * 把模块装配好
11 |
12 | UI 组合,以及发 Event 看起来不像函数调用,但其实只是受限的函数调用
13 |
14 | * render(props),UI 组件的特点是返回值是固定的,就是一个视觉区块。
15 | * publish(event),Event 就是一个没有返回值的函数调用
16 |
17 | 所以可以认为把代码写活,其实就等价于抽取一个函数出来,在某个时刻装配模块,选择这个函数的实现。
18 |
19 | ## 降低抽取接口的成本
20 |
21 | 用过 Intellij 写 Java 的同学可能用过 Extract Method 的重构快捷方式。最简单的抽一个扩展点的方式,就是用 Extract Method 重构菜单,把一段语句抽取到一个独立的函数里。这个函数签名,就是接口定义。这个是最最底线的成本。
22 |
23 | 在这个之上,很多模块化方案可能会要求新增一个 interface 文件,或者在web页面操作一下,注册一个接口。或者加一些 manifest 文件。这些额外的操作都是插件税。
24 |
25 | ## 降低模块装配成本
26 |
27 | 模块的打包和部署可以有非常多的选择。
28 |
29 | * 在源代码层面进行文本操作
30 | * .a 文件,静态链接
31 | * .so 文件,动态链接
32 | * 面向对象的多态 / 函数式编程的高阶函数
33 | * 微服务 RPC
34 |
35 | linker 是所有程序员都熟悉的概念,无论是 .a 还是 .so。我们可以在源代码的文本层面实现一个和 linker 一样的“函数链接器”。比如有一个 a.js 文件,定义了
36 |
37 | ```js
38 | function hello() {
39 | world();
40 | }
41 |
42 | function world() {
43 | // placeholder
44 | }
45 | ```
46 |
47 | 我们有另外一个 b.js 文件,定义了
48 |
49 | ```js
50 | @override
51 | function world() {
52 | console.log('world');
53 | }
54 | ```
55 |
56 | 通过某种类似 linker 的文本构建工具,把 a.js + b.js 输出成 c.js。因为 world 函数同名且标记了 `@override`,所以 b.js 中的定义就覆盖了 a.js:
57 |
58 | ```js
59 | function hello() {
60 | world();
61 | }
62 |
63 | // override by b.js
64 | function world() {
65 | console.log('world');
66 | }
67 | ```
68 |
69 | 装配后的结果可以直接被阅读,比运行时装配要更直观。
70 | 这里 js 文件只是例子。文本操作可以普适于任何有文本源代码的编程语言。
71 | 比如 angular 的 template 文件,也可以用类似的技术来实现模块的静态组装。
72 | 这种模块装配的实现技术在所有打包部署的选项里是各项代价都是最低的。
73 |
74 | ## 静态装配
75 |
76 | 在“[可逆计算的技术实现](https://zhuanlan.zhihu.com/p/163852896)”作者提出的面向 AST 的源代码 transformer 也是类似的。只是要处理 Tree 的话,会比 Flat List 要复杂一些,需要定位到 Tree 的节点,然后再应用 Transform 操作。
77 |
78 | 比“可逆计算”更出名的是 Hyper/J 和 AspectJ。它定义了 PointCut / Advice / JoinPoint 等概念。最初版的 AspectJ 是由 Source-code Weaver 实现的。其实就是把两个文本文件,粘合成一个文本文件。但是大部分 Java 项目都不想增加一个“静态代码生成”的步骤,所以实际上 AspectJ 的 Class load time Weaver 更常用,但是也就使得其定位和 Spring 的 IoC 是大部分重叠的。
79 |
80 | 从 AspectJ 的历史可以看出,大部分语言的工具链生态里是不赞成“代码生成”的。这个也导致了如果要采用静态装配缺少工具链的支持。
81 | 如果工具支持跟不上,静态装配带来的好处不仅仅无法显现,而且会有诸多的坏处。
82 | 任何模块化方案,都需要一个模块化的底座平台,以及生产力工具。插件税的高与低,就与我们投入多少时间和成本建设这个底座平台有关系。
83 |
84 |
--------------------------------------------------------------------------------
/patterns/how-to-maintain-readability/README.md:
--------------------------------------------------------------------------------
1 | # 问题
2 |
3 | 为了代码复用,拆了很多个模块,导致代码不好阅读了怎么办? 之前代码虽然写的挫,但是 ctrl+f 在一个文件里就可以找到对应的代码,现在找段代码可费劲了。
4 |
5 | # 分析
6 |
7 | 过去的模式是,生产环境看到了一个页面,肯定对应的代码都在代码里的一个函数里。如果点击了一个按钮,这个按钮的 handler 肯定也是写在代码里的一个函数里。
8 | 也就是但凡是同一个时刻执行的 CPU 指令,都可以在同一个源代码文件里找到原因。
9 | 这种一对一的映射关系是非常利于人类的理解的。只要做了模块化,其实质就是把代码从这个文件移动出去。
10 | 必然就是在一个文件里 ctrl+f 不到了。
11 |
12 | 产生这个现象的第二个原因是之前,我使用了某个函数,比如 `login()`,会在文件头部有 import 的声明,说明这个 login 函数是哪里提供的。但是如果你使用的是运行时装配的模块化技术,比如面向对象的多态,函数式编程的高阶函数,这里的 login 可能只有一个定义,你是无法在编辑的时候找到这个 login 的实现代码的。模块化并不等于用运行时模块去装配,可读性降低很多时候是因为过于依赖运行时装配导致了。Java 系的代码里经常出现滥用 interface 和依赖注入的代码,很多都是没有必要的。
13 |
14 | 第三个原因是模块之间的边界不恰当。这个不恰当体现为一个模块没有办法独立讲一个完整的故事。当然这是一个非常主观的判断,就像“不好读”也是一个无法量化的主观判断。但是有一个侧面印证的办法,就是观察一个程序员在读一段代码的过程中,多少次进行了代码跳转。如果模块边界比较恰当,那么在阅读的时候应该是可以从上往下比较顺畅的读完的。如果边界切得犬牙交错,就经常看到一个地方不明白,就要跳过去看看实现代码。
15 |
16 | 第四个原因是为了复用,经常需要插入扩展点。这些扩展点必然会是一部分使用方需要,另外一部分使用方式不需要的。这些额外的扩展点,往往其默认实现都是一些空函数。这就导致在代码阅读的时候,要知道一个函数调用是有功能实现在里面,还是留了一个空白的扩展点。当阅读一段代码,要跳过很多个空函数的时候,阅读体验是下降了的。这个现象和上一个原因其实是同时出现的。都是模块之间的边界切得不合理导致的。
17 |
18 | # 解决方案
19 |
20 | 尽量少用运行时的模块化方案(面向对象的多态,函数式编程的高阶函数),静态组装的模块更好阅读。模块的边界多尝试几种,一般来说得让一个模块能独立讲一个完整的故事,要不然阅读起来就要跳来跳去。
--------------------------------------------------------------------------------
/patterns/ui-composition-obstacles/README.md:
--------------------------------------------------------------------------------
1 | # 问题
2 |
3 | 松耦合模块边界的最佳范例就是基于 UI 的组合。但是为什么在过去的历史经验里,这样的模块切分的方式很难落地? 有没有什么具体的技术方案可以借鉴?
4 |
5 | # 分析
6 |
7 | 这个原因是非常多方面的
8 |
9 | ## 按职能分工
10 |
11 | 一个人的时间和精力都是有限的。因为 UI 层要学习的技术实在太多了,一个合格的前端开发要学太多的东西了。所以为了最大化利用这些人已经学到的知识,按职能分工仍然是现下最佳的分工模式。当我们把一个团队指定为只做前端之后,他们自然会把所有和前端的工程都放到一起来。这么做是对的,但是和用 UI 组合来切分模块往往是冲突的。
12 |
13 | ## 为角色用户负责
14 |
15 | 为了让系统的每个角色的用户都有一个负责的团队为他们服务,经常分为 B 端系统,C 端系统,客服系统。这种划分方式显著提升了用户的满意度,因为有专门的团队为他们的目标努力。但是这也带来了系统上的耦合。比如客服系统可能要支持很多种类型的订单,光是对接这些系统,处理订单字段的新增和修改,就要忙死了。按角色用户组建专门的团队也是对的,单这往往也带来切分模块的阻力。
16 |
17 | ## UI 技术的易变性
18 |
19 | UI 三天两头都在变。Android / iOS 一直在升级自己的框架,小程序,快应用又纷至沓来。这导致 UI 层的框架没多久就过时了。
20 |
21 | ## 应用审核
22 |
23 | UI 层的传统是预先打包好成一个整体,有一个发版的概念。这个发版周期就是一种耦合。更糟糕的是,无论是 iOS 应用,还是微信小程序,都要再过一道审核,这些审核都是反对动态加载新功能的,一定要静态把功能都打包好。
24 |
25 | UI 组合很大一个被诟病的地方就是要用 lib 引用,要跟着应用打包发版,这个极大限制了每个模块团队的自主性。
26 |
27 | ## 微前端
28 |
29 | 广义上的微前端还包括Android热加载框架等技术。这些方案都对 UI 组合有帮助:
30 |
31 | * https://github.com/didi/VirtualAPK
32 | * https://github.com/umijs/qiankun
33 | * https://github.com/berialjs/berial
34 | * https://github.com/rajasegar/awesome-micro-frontends
35 |
36 | 微前端和微服务一样,就是为了让每个模块可以自主把握自己的发布周期。也可以更好的撇清责任边界,定位故障。
37 | 随着 UI 层技术逐渐地稳定下来,可以看到这个领域会有更多的选择,更多成熟的方案。
38 | UI 组合技术也会随着微前端概念又一次红火起来。
39 |
40 |
--------------------------------------------------------------------------------