每天一本书:提问的艺术
如何提问
第一步:要从问句开始,而不是用阐述或者命令
比如,你是公司的老总,对公司的客服不满意,如果你对客服们说“我们需要改进客服质量”,这个效果可能不会很好。
你可以问他们:“你怎么评价我们今天的客服表现?”这个提问会让客服人员自己琢磨一下:今天能给自己打几分?哪儿做得好?哪儿做得不好?这比你去灌输一些评价,效果要好。处理家庭关系时也能用这个办法。
第二步:问一些每个人都会想到的、最基本的问题(司空见惯的问题)
很多事看久了,就习惯了,不太去琢磨它到底是怎么回事了。但有时你仔细思考一下,才发现对一些事儿,其实我们并不太明白它们到底意味着什么。
比如现在最流行的一个词儿是“创新”。在工作里经常有人说:“我们需要创新。”但是每天听,都听疲沓了,而且搞来搞去,也创新不出什么东西。那么你再听到有人这样说的时候,可以问一问他:“你眼中的创新究竟是什么样的呢?”可能一下子就把人问住了,然后就开始琢磨:是啊,究竟什么样呢?这就不是空喊口号了,开始考虑实际了。
提问方法
- 封闭式提问
- 开放性提问
- 追问式提问
封闭式提问
什么叫封闭型提问呢?就是问得非常具体,对方只能正面回答,给出直接的答案。比如,关于创新、团队的提问,其实都属于封闭型的提问。
最典型的封闭型提问,是“是非题”,对方只能回答“是”或者“不是”。
什么时候需要用到封闭型提问呢?一般是在发生“大猩猩式的斗殴”的时候。
这儿说的不是真的斗殴,是一种比喻。研究生物的人发现,公猩猩在打架的时候,喜欢互相示威,互相围着转,一边转着,一边在手里抓着土,往天上扬,没别的意思,就是吓唬对方,然后转了半天也打不起来。所以“大猩猩式的斗殴”指的就是兜圈子,不干实事。
当你和别人谈一件事,别人总是兜圈子时,就适合用封闭型提问,直接要求他给答案。举个例子,公司里开会,讨论的是“客户至上”的话题,一开就是很长时间,总也没有实质性的结论。那么这时候就适合提出封闭型的问题:我们今天要做什么样的决定?是提高客户保有率,还是交叉销售更多的产品?提问越具体,越容易从泛泛的讨论中跳出来。
开放性提问
在人际交往中,就需要经常用到第二种提问方式,就是开放性提问。
所谓开放性,就是没有固定的答案,可以随便说。
美国著名作家梭罗独自住在波士顿附近的瓦尔登湖畔,他写了一本书叫《瓦尔登湖》,梭罗特别爱写日记,有一天他写了这么一句话:“今天,我得到的最大恭维就是有人问我,我是怎么想的,并真诚地聆听了我的答案。”梭罗写出了人的一种心理,就是希望被聆听。
这可能有点违反直觉,因为我们总是下意识地以为,和别人聊天时,说得越多,越不容易冷场,越显得尊重别人。但其实不是,绝大多数人实际上更愿意表达,如果你一直在滔滔不绝地说话,别人插不上嘴,等于剥夺了别人表达的权利,对方就会觉得不舒服。
所以会聊天的人,一定是善于倾听的人。怎么做到这一点呢?还是靠提问,而且得靠开放性的提问。有一次,本书的作者走访安利集团的创始人,他就问了对方一句话,就让这个大老板开始滔滔不绝。他是这么问的:“请告诉我,你当初是如何开始的?”然后,对方就开始讲故事,讲一个大学肄业的人最终如何打造了市值上百亿美元的集团。
“你是如何开始的”,这个问题可以衍生出很多问题,比如“你们是怎么相爱的”“你在哪里长大的”“你是和谁学的弹琴”。再比如,“你还有什么迫切想实现的愿望吗”“对你来说最高兴的一天是哪一天”“你一生中最值得骄傲的成就是什么”。开放性的问题不能用简单的“是”或“否”来回答,它需要更清楚的解读,会带来更丰富的互动。
无论是和成功人士还是普通人聊天,这种问题都会让对方发现自己不平凡的一面,愿意与你开心分享他们的故事。
追问式提问
沟通不是单方面的事情,它确实需要倾听,但又不能只是倾听。有时候为了发现并且解决一些问题,就需要进行追问。
有一次,一个国际性企业发现自己的销售出现了问题,觉得需要进行改进性的培训,就找到本书的作者,托他办个培训班。作者没有一上来就答应,而是问了销售部主管五个问题。
第一个问题:为什么你们在全球销售市场中都成为领先者了,还需要销售培训呢?对方说,因为需要不断提高销售人员的能力;
他接着问了第二个问题:为什么需要提高销售能力呢?对方又说:这样销售人员在开发新客户方面会更有效率;
接着是第三个问题:为什么需要增加新客户的开发呢?对方说,因为现在的客户不足以支撑公司的增长目标;
第四个问题又来了:为什么不能让客户增长得更快呢?对方说:我们每年都20%的客户流失。
好,最后一个问题:为什么客户会流失?最终,公司的销售人员给出了答案,他们的产品质量和物流有问题,所以客户才不满意。
就这样,五个问题追问下来,没有做销售培训的必要了,解决产品质量和物流漏洞才是关键。这就是追问的效果,它会帮助你找到问题的核心所在。
总结
三种提问方式都说到了,我们来总结一下:
- 封闭式的提问,适用于得到明确答案时提出;
- 开放性提问,适合需要深谈的人际沟通时提出;
- 追问,适合在复杂情况中寻找核心问题时提出。
提问这件事,说起来简单,做起来难。我们经常会被一些事情困扰,想要去解决它们,但是往往找不到原因,更常见的情况是,我们连问题本身是什么都描述不清楚。
企业组织
问自己五个问题:
- 你的愿景是什么?
- 什么是你愿意投入身心去打造最重要的关系?
- 什么能创造客户价值?
- 你期望得到的结果是什么?
- 你的计划是什么?
核心指向:你为什么要这么做?你的存在是什么价值?你会对别人会产生什么样的影响?
Kotlin Inline
Usage
One of them is high order functions which lets you pass functions as parameters as well as return functions. But the fact that they are stored as objects may make the use disadvantageous at times because of the memory overhead.
The thing is, each object is allocated space in memory heap and the methods that call this method are also allocated space.
1 | fun main(args: Array<String>) { |
A way to get around the memory overhead issue is, by declaring the function inline. inline annotation means that function as well as function parameters will be expanded at call site that means it helps reduce call overhead.
The goal of this post is to get a basic understanding of inline in Kotlin so as to be able to identify how and when to use it in our code in future.
1 | inline fun someMethod(a: Int, func: () -> Unit):Int { |
Example
国际 International
第一国际(1864-1876年)
社会主义先觉者很早就主张各国工人运动有国际的联络和国际的组织之必要了。把这个必要说得最明白,最不含糊的,是马克思和恩格斯。远在1847年,即距今99年前,马克思和恩格斯在他们起草的「共产党宣言」中就断言「无产阶级无祖国」,就喊出「全世界无产者联合起来」口号。自此以后,他们的思想和斗争就浸透这种国际主义精神。
到了1864年,这种国际主义精神第一次表现为组织形式。这年,英法德意四国工人代表在伦敦开会,决议创立一个「Intelnational WorkingMen’s Association」,为诸国工人团体联络机关。这个新团体底名称直译应为「工人底国际的联合会」,这里「国际」二字是作形容词用的。即「英法德意诸国工人联合会」之意。马克思当时代表德国工人参加这个新团体工作,渐渐以科学的社会主义思想指导整个组织了。不久,会务大大开展,小国工人也来参加这个组织,遂成为世界上一种势力,为诸国政府所畏惧。会名太长,有时人们取它的第一个字,简称为「International」(「国际的」)以后成了习惯,整个团体就叫做「国际」了。这是第一次,这个形容词变成了名词。
1871年,巴黎工人暴动,「国际」底法国支部不仅参加,而且占据领导地位。可是这次有名的「巴黎公盟」失败了,「国际」受了极大打击,外面资产阶级政府底取缔,里面小资产阶级思想底进攻,遂使组织力量一天比一天衰弱,终于在1876年正式宣布解散。马克思领导的这个「工人国际会」在工人运动历史上被称为「第一国际」,以别于后来的几个工人国际组织。
第二国际(1889-1914年)
第三国际(1919-1943年)
第四国际
Java SLF4J
Introduction
Simple Logging Facade for Java (SLF4J)
The Simple Logging Facade for Java (SLF4J) serves as a simple facade or abstraction for various logging frameworks (e.g. java.util.logging, logback, log4j) allowing the end user to plug in the desired logging framework at deployment time.
Configuation
pom.xml
1 | <properties> |
log4j2.xml
1 | <?xml version="1.0" encoding="UTF-8"?> |
注:如果不添加additivity=”false”属性,输出重复log
Enable AsyncLogger
JVM启动参数(boot.ini)加上“-DLog4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector”
classpath中添加文件“log4j2.component.properties”,文件增加以下内容:“Log4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector”
手工指定部分Logger采用异步方式log4j2.xml配置文件中使用AsyncRoot/AsyncLogger替代Root/Logger
Usage
1 | val logger = LogManager.getLogger() |
1 | -Dlog4j.configurationFile=log4j2-dev.xml |
Reference
Kotlin Annotation
Maven Configuration
Commmand
1 | mvn '-Dtest=com.anda.engine.disruptor.*Test' test |
Dependencies
Exclude
1 | <exclusions> |
Scope
- compile:默认选项,编译测试运行都有效。项目在编译,测试,运行阶段都需要这个artifact对应的Jar。
- provided:在编译和测试时有效。在编译测试阶段,我们需要这个artifact对应的jar包在classpath中,而在运行阶段,假定已经提供了这个jar包,所以无需这个artifact对应的Jar。
- runtime:在测试和运行时有效。
- test:仅在测试时有效。
- system:在编译和测试时有效,与本机系统关联,可移植性差。
Unit Test
Plugin
1 | <plugin> |
Build
Package Kotlin Project With Jar File
1 | <plugin> |
在pom.xml同级目录下,新建assembly.xml,内容如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
<id>dependencies</id>
<formats>
<format>jar</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<dependencySets>
<dependencySet>
<outputDirectory>/</outputDirectory>
<useProjectArtifact>true</useProjectArtifact>
<unpack>true</unpack>
<scope>runtime</scope>
</dependencySet>
<dependencySet>
<outputDirectory>/</outputDirectory>
<useProjectArtifact>true</useProjectArtifact>
<unpack>true</unpack>
<scope>system</scope>
</dependencySet>
</dependencySets>
</assembly>
Including Library for Spring Boot
1 | <plugin> |
Build Library
- 导出的jar没有包含依赖的denpendency,导致其他项目引入的时候报错。
解决方案:添加maven-assembly-plugin插件进行打包。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</execution>
</executions>
</plugin>
此外,需要注意的是,maven-assembly打包出来的是一个可运行的jar,即包含了Kotlin标准库的代码,而实际上需要的仅仅是一个Library。
针对标准库添加provided关键字1
2
3
4
5<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
<scope>provided</scope>
</dependency>
Repository
MacOS
1 | # 查看maven安装目录 |
添加Aliyun Repository,sudo vim /usr/local/Cellar/maven/3.5.4/libexec/conf/settings.xml1
2
3
4
5
6
7
8<mirrors>
<mirror>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<mirrorOf>central</mirrorOf>
</mirror>
</mirrors>
Aeron Practice
Design Overview
https://github.com/real-logic/aeron/wiki/Design-Overview
TCP or UDP
TCP
For real-time video conference application, how do you choose between TCP and UDP?
本题的关键在于比较TCP和UDP的特点,并且根据real-time video conference这个特定的应用场景进行选择。前面提到过,TCP的重传机制会增加延迟,所以不适用于当前场景。
其次,视频音频编码本身可以容忍数据出错甚至数据丢失。因此,并不需要采用TCP进行可靠的数据传输。当某一视频帧出现丢包时,可以直接跳过这一帧或者继续播放上一帧。
再次,一旦出现网络堵塞的状况,发送端应该主动丢弃一部分数据。原因是,即使这些视频帧发送到了接收端,也可能已经“过期”了,不会被解码显示。
采用自己设计的UDP更便于实现对数据包的控制。然而,即使使用UDP,也需要实现TCP的某些模块:比如需要flow control和congestion control来判断接收端的播放情况和网络情况,并且也需要反馈机制判断接收端的接收状况。尽管对于当前场景我们不需要ACK每个数据包,但是接收端可以反馈当前收到的最新完整视频帧的序号。这样,如果一旦发生丢包,发送端可以以接收端收到的最新视频帧为基础,压缩后继的视频。
UDP
If you are designing a reliable UDP, what should you do?
通常,所谓的reliable都是指接收端能够将收到的数据情况反馈给发送端。由于我们已经知道一种可靠的传输协议,TCP,故reliable UDP的设计完全可以参考TCP的设计方式,引入ACK,flow control,congestion control等模块。模块的实现可以直接模仿TCP。Reliable UDP的核心在于反馈机制,这里给出几个可能的实现方式。
由于reliable要求在接收端能够恢复数据包的顺序,故发送端每个数据包都需要有sequence number。现在着重讨论反馈机制:
- 最朴素的ACK方式:发送端每发送一个数据包,都需要接收端返回ACK,一旦超时,发送端重新发送数据包,直到该数据包被接收端ACK。该方法效率不高,因为之后的所有数据包都被当前数据包block,并且每次返回ACK增加了overhead。
- Block/bit map ACK:发送端发送一批数据包,例如32个,编号0~31。接收端发回的ACK中用32bits(4bytes)的bit map表示收到了哪些数据包,发送端再一次性重发所有未被收到的数据包。该方法能够更加充分地利用带宽,在发送端一次性传输更多的数据。但缺点是在发送端接收端都需要更深的buffer,暂存正在传输的所有数据。
- ACK last packet:发送端可以在发送最后一个数据包时要求接收端反馈ACK,并重发丢失的数据包。这样做的好处可以减少由ACK造成的data overhead,但需要通过buffer暂存数据。
事实上,可以结合方法2和方法3,在每一批数据包的最后一个置位request ACK flag,要求接收端返回bit map ACK。更进一步地,可以根据丢包率及延迟,估计网络状况,动态地调整bit map的大小:在网络状况好的情况下,用更大的bit map,即同时发送更多数据。否则,减小发送数据量。事实上,这种对于网络状况的自适应也相当于实现了congestion control。
OS Related Considerations
Operating system socket buffers have an impact on some of the settings within Aeron.
SO_RCVBUF can impact loss rates when too small for the given processing. If too large, this buffer can increase latency. Values that tend to work well with Aeron are 2MB to 4MB. This setting must be large enough for the MTU of the sender. If not, persistent loss can result. In addition, the receiver window length should be less than or equal to this value to allow plenty of space for burst traffic from a sender.
SO_SNDBUF can impact loss rate. Loss can occur on the sender side due to this buffer being too small. This buffer must be large enough to accommodate the MTU as a minimum. In addition, some systems, most notably Windows, need plenty of buffering on the send side to reach adequate throughput rates. If too large, this buffer can increase latency or cause loss. This usually should be less than 2MB.
Linux
As was mentioned above, changing the location of the buffers for Aeron can be a good thing. For Linux, this means that /dev/shm will be the location of the buffers if present.
Linux normally requires some settings of sysctl values. One is net.core.rmem_max to allow larger SO_RCVBUF and net.core.wmem_max to allow larger SO_SNDBUF values to be set.
Windows
Windows tends to use SO_SNDBUF values that are too small. It is recommended to use values more like 1MB or so.
Mac/Darwin
Mac tends to use SO_SNDBUF values that are too small. It is recommended to use larger values, like 16KB.
Media Driver
Media Driver instances sit on a box an send/receive UDP packets over the network, whilst ensuring that the mapped files and cleaned and rotated.
If you’ve got multiple publishers or subscribers sitting in different processes on the same box then probably your best bet is to run separate a instance of the media driver and have it manage all the processes.
If you’re going to just have a single process on a machine with the publishers and subscribers inside then its probably easiest to just keep it embedded within the process. Just my opinion of course
Aeron instances in application, commonly referred to as “clients”, communicate with Media Drivers via a set of buffers.
The location of these buffers is normally in the OS file system. By default, the java.io.tmpdir or /dev/shm/ is used to hold these files.
How to Run Aeron Media Driver
To run the Aeron Media Driver as a foreground process, use the script provided with the driver. The script provides the appropriate configuration for the driver. You can provide your own configuration via environment variables:
- AERON_DIR(Method:aeronDirectoryName())
The path to the directory where the Aeron Media Driver needs to store its files. On Linux, the directory inside /dev/shm/ is recommended. If you provide your own path, make it the same for the driver and any microservice that operates with this driver.
If it is not specified then the default value provided by the Aeron is used.
- AERON_SO_BUFFER
The size in bytes of the send and receive socket buffers. The length of the buffer must be a power of two. On Linux, it must not exceed the kernel configuration parameters:
net.core.wmem_max
net.core.rmem_max
The default value is 4194304.
- ERON_TERM_BUFFER
The size in bytes of the Term (a section of data within a stream) buffer. The length of the buffer must be a power of two and must be the same length on both ends.The default value is 67108864.
- AERON_MTU
The length of MTU in bytes.
The default value is 65504.
https://docs.genesys.com/Documentation/EZP/9.0.0/Deploy/AeronMediaDriver
shm on Linux System
在Docker中,/dev/shm 默认大小是64MB,完全不够用,因此需要调整大小。然而,在Kubernetes中是不持支持shm-size参数的,所以只能通过启动脚本来修改容器/dev/shm的大小。
前提条件:–priviledge=true
1 | shm_dir=/dev/shm |
running docker command
1 | docker run -d --privileged image:tag |
https://tw.saowen.com/a/d4e0d2129b3fcaa1597c3860ac1f4e77753e738073b67ae599b8b4b10d0b8ee2
Media Driver on Docker
1 | FROM java:openjdk-8-alpine |
1 | #!/bin/sh |
Unit Testing Attention
- 在每个单元测试用例执行前,最好重启并清空MediaServer缓冲区,否则遗留的数据可能会被下一个用例读取到。
MediaServer Code
Shm Memory
Each node will reserve aeron.term.buffer.length 12, that said; its default value is 16m hence occupying 192m per JVM in the cluster per shared driver (1 per physical machine), thats is to say that if you have 10 JVMs spread out in 4 physical machines and each machine with a shared driver, each machine will need 192m 10 JVMs which is 1920m, making each machine require at least 4g+ of RAM.
Why 4g+? is not only 1920m, it is 1920m + some overhead > 2g, that IMHO was too much, hence I tuned down aeron.term.buffer.length to 4m hence making it possible to run 10 JVMs among 4 physical machines (in my case virtual machines) with 2g RAM.
LowLatency
1 | val ctx = MediaDriver.Context() |
JVM参数
- -XX:+UnlockDiagnosticVMOptions
- -XX:GuaranteedSafepointInterval=300000
- -XX:BiasedLockingStartupDelay=0
Aeron
Buffering Considerations
The length of term buffers is controlled by aeron.term.buffer.length and aeron.ipc.term.buffer.length and aeron.term.buffer.max.length properties.
- aeron.term.buffer.length
- aeron.term.buffer.max.length
Kuberntes
Configration
How increase FragmentHandler’directBuffer size
1 | val context = MediaDriver.Context() |
Multiple Destinations
Both Publications and Subscriptions in Aeron can support the concept of multiple simultaneous destinations.
For Publications, this means the outgoing stream is sent to each destination individually.
For subscriptions, this means the incoming stream(s) may be received by a number of individual endpoints.
测试结果表明,在点对点的传输速度可以达到 150 MB/s ~ 160 MB/s,而如果增加了publisher,数据每秒传输速度下降。
单条消息大小 75 Byte,190 W ~ 210 W 条 / sec
ExclusivePublication and Publications
Publications
Publications are thread safe within and across processes. A Publication object can be used concurrently from many threads.
When separate processes add the same Publication (channel and stream id) then they will map to the same underlying memory-mapped file and can be safely used concurrently.
Messages offered to the same publication will be serialised. Publications with a different channel and stream id are completely independent from each other and operate in parallel.
ExclusivePublication
Refer
https://github.com/real-logic/aeron/wiki/Best-Practices-Guide
https://stackoverflow.com/questions/32243664/what-is-the-largest-message-aeron-can-process
https://github.com/real-logic/aeron/wiki/Multiple-Destinations
What's Architecture Design
What’s Architecture Design
Terms
系统:泛指由一群有关联的个体组成,根据某种规则运作,能完成个别元件不能单独完成的工作的群体。
模块:划分的目的是职责分离。
组件:划分的目的是复用。
软件架构:软件系统的顶层结构。
- 系统由一群关联个体组成。
- 明确系统包含的个体,例如子系统、模块、组件等等。
- 明确个体运作和协作的规则。
Architecture Design Purpose
架构设计的目的:解决复杂度带来的问题。
复杂度的来源
- 高性能
- 高可用
- 可扩展性
- 低成本
- 安全
- 规模
常见误区
- 因为架构很重要,所以要做架构设计
- 不是每个系统都要做架构设计吗?
- 为了高性能、高可用、扩展,所以要做架构设计,上来就要实现”三高“,结果会出现架构设计无比复杂。
Difference between framework and architecture
框架关注的是规范,架构关注的是结构
Design Principle
- 合适原则
- 简单原则
- 演化原则
合适原则
宣言:合适优于业界领先。
脚踏实地做事
- 将军难打无兵之仗:没那么多人,却想干那么多活,是失败的第一个主要原因。
- 罗马不是一天建成的:没有那么多积累,却想一步登天,是失败的第二原因。
- 冰山下面才是关键:没有那么卓越的业务场景,却幻想灵光一闪成为天才,是失败的第三个主要原因。
简单原则
宣言:简单优于复杂
”复杂“在制造领域代表先进,在建筑领域代表领先,但在软件领域,却恰恰相反,代表的是”问题“。
软件领域的复杂性体现:
- 结构的复杂性
- 组件数量多
- 组件之间关系复杂
- 逻辑的复杂性
- 遵循KISS原则(Keep It Simple, Stupid!)
带来的问题:
- 组件越多,就越有可能其中某个组件出现故障,从而导致系统故障。
- 某个组件改动,会影响关联的所有组件,这些被影响的组件同样会继续递归影响更多组件。
- 定位一个复杂系统中的问题总是比简单系统更加困难。
演化原则
宣言:演化优于一步到位
Windows系统的发展历史,Android系统发展历史
对于软件来说,变化才是主题。软件架构需要根据业务的发展而不断变化。设计Windows和Android的人都是顶尖的天才,即便如此,他们也不可能在1985年设计出Win8,不可能在2009年设计出Android 6.0。
如果没有把握”软件架构需要根据业务发展不断变化“这个本质,在做架构设计的时候就会容易陷入一个误区:试图一步到位设计一个软件架构,期望不管业务如何变化,架构都稳如磐石。
生物演化:
- 生物要适应当时的环境。
- 生物需要不断地迭代繁殖,将有利的基因传承下去,将不利的基因剔除或修复。
- 当环境变化时,生物要能够快速改变以适应环境变化;如果生物无法调整就被自然淘汰;新的生物会保留一部分原来被淘汰的生物的基因
软件架构设计:
- 设计出来的架构要满足当时的业务需要。
- 架构要不断地在实际应用过程中迭代,保留优秀的设计,修复有缺陷的设计,改正错误的设计,去掉无用的设计,使得架构逐渐完善。
- 当业务发生变化时,架构要扩展、重构、甚至重写;代码也许会重写,但有价值的经验、教训、逻辑、设计等却可以在新架构延续。