乐趣区

关于软件:阿里研究员警惕软件复杂度困局

简介: 对于大型的软件系统如互联网分布式应用或企业级软件,为何咱们经常会陷入复杂度陷阱?如何辨认复杂度增长的因素?在代码开发以及演进的过程中须要遵循哪些准则?本文将分享阿里研究员谷朴关于软件复杂度的思考:什么是复杂度、复杂度是如何产生的以及解决的思路。较长,同学们可珍藏后再看。

写在后面

软件设计和实现的实质是工程师互相通过“写作”来交换一些蕴含丰盛细节的抽象概念并且一直迭代过程。
另外,如果你的代码生存期个别不超过 6 个月,本文用途不大。

一 软件架构的外围挑战是快速增长的复杂性

越是大型零碎,越须要简略性。

大型零碎的实质问题是复杂性问题。互联网软件,是典型的大型零碎,如下图所示,数百个甚至更多的微服务互相调用 / 依赖,组成一个组件数量大、行为简单、时刻在变动(公布、配置变更)当中的动静的、简单的零碎。而且,软件工程师们经常自嘲,“when things work, nobody knows why”。

如果咱们只是写一段独立代码,不和其余零碎交互,往往设计上要求不会很高,代码是否易于应用、易于了解、易于测试和保护,基本不是问题。而一旦遇到大型的软件系统如互联网分布式应用或者企业级软件,咱们经常陷入复杂度陷阱,下图 the life of a software engineer 是我很喜爱的一个软件 cartoon,十分形象的展现了复杂度陷阱。

做为一个有谋求的软件工程师,大家必定都思考过,我手上的我的项目,如何防止这种仿佛难以避免的复杂度窘境?

然而对于这个问题给出答案,却出其不意的艰难:很多的文章都给出了软件架构的设计倡议,而后正如软件畛域的经典论著《No silver bullet》所说,这个问题没有神奇的解决方案。并不是说那么多的架构文章都没用(其实这么办法多半都有用),只不过,人们很难真正去 follow 这些倡议并贯彻上来。为什么?咱们还是须要彻底了解这些架构背地的思考和逻辑。所以我感觉有必要从头开始整顿这个逻辑:什么是复杂度,复杂度是如何产生的,以及解决的思路。

二 软件的复杂度为什么会快速增长?

要了解软件复杂度会快速增长的实质起因,须要了解软件是怎么来的。咱们首先要答复一个问题,一个大型的软件是建造进去的,还是成长进去的?BUILT vs GROWN,that is the problem.

1 软件是长进去的,不是建造进去的

软件不是建造进去的,甚至不是设计进去的。软件是长进去的。

这个说法初看上去和咱们平时的意识仿佛不同,咱们经常谈软件架构,架构这个词仿佛蕴含了一种建造和设计的象征。然而,对于软件系统来说,咱们必须意识到,架构师设计的不是软件的架构,而是软件的基因,而这些基因如何影响软件将来的状态则是难以预测,无奈齐全管制。

为什么这么说?所谓建造和“成长”差别在哪里?其实,咱们看明天一个简单的软件系统,的确很像一个简单的建筑物。然而把软件比作一栋摩天大楼却不是一个好的比喻。起因在于,一个摩天大楼无论如许简单,都是当时能够依据设计出残缺详尽的图纸,按图精确施工,保证质量就能建造进去的。然而事实中的大型软件系统,却不是这么建造进去的。

例如淘宝由一个单体 PHP 利用,通过 4、5 代架构一直演进,才到明天服务十亿人规模的电商交易平台。支付宝,Google 搜寻,Netflix 微服务,都是相似的历程。

是不是肯定要通过几代演进能力构建进去大型软件,就不能一次到位吗?如果一个团队来到淘宝,要拉开架势依据淘宝交易的架构从新复制一套,在事实中是不可能实现的:没有哪个守业团队能有那么多资源同时投入这么多组件的开发,也不可能有一开始就朝着超级简单架构开发而可能胜利的实现。

也就是说,软件的动静“成长”,更像是上图所画的那样,是从一个简略的“构造”成长到简单的“构造”的过程。随同着我的项目自身的倒退、研发团队的壮大,零碎是个逐步成长的过程。

2 大型软件的外围挑战是软件“成长”过程中的了解和保护老本

简单软件系统最外围的特色是有成千盈百的工程师开发和保护的零碎(软件的实质是工程师之间用编程语言来沟通形象和简单的概念,留神软件的实质不是人和机器沟通)。如果认同这个定义,构想一下简单软件是如何产生的:无论最终如许简单的软件,都要从第一行开始开发。都要从几个外围开始开发,这时架构只能是一个简略的、大量程序员能够保护的零碎组成架构。随着我的项目的胜利,再去逐步细化性能,减少可扩展性,散布式微服务化,减少性能,业务需要也在这个过程中一直产生,零碎满足这些业务需要,带来业务的增长。业务增长对于软件系统迭代带来了更多的需要,架构随着适应而演进,投入开发的人员随着业务的胜利减少,这样一直迭代,才调演进出几十,几百,甚至几千人同时保护的简单零碎来。

大型软件设计外围因素是管制复杂度。这一点十分有挑战,根本原因在于软件不是机械流动的组合,不能在当时通过精心的“架构设计”躲避复杂度失控的危险:雷同的架构图 / 蓝图,能够长出完完全全不同的软件来。大型软件设计和实现的实质是大量的工程师互相通过“写作”来交换一些蕴含丰盛细节的抽象概念并且互相一直迭代的过程[2]。稍有过错,零碎复杂度就会失控。

所以说了这么多是要停留在形而上吗?并不是。咱们的论断是,软件架构师最重要的工作不是设计软件的构造,而是通过 API,团队设计准则和对细节的关注,控制软件复杂度的增长。

  • 架构师的职责不是试图画出简单软件的大图。大图好画,靠谱的零碎难做。简单的零碎是从一个个简略利用 一点点长进去的。
  • 当咱们发现自己的零碎问题多多,别怪“当初”设计的人,坑不是一天挖出来的。每一个设计决定都在奉献复杂度。

三 了解软件复杂度的维度

1 软件复杂度的两个体现维度:认知负荷与协同老本

咱们剖析了解了软件复杂度快速增长的起因,上面咱们天然心愿能解决复杂度快速增长这一看似永恒的难题。然而在此之前,咱们还是须要先剖析分明一件事件,复杂度自身是什么?又如何掂量?

代码复杂度是用行数来掂量么?是用类的个数 / 文件的个数么?深刻思考就会意识到,这些外表上的指标并非软件复杂度的外围度量。正如后面所剖析的,软件复杂度从根本上说能够说是一个主观指标(先别跳,急躁读上来),说其主观是因为软件复杂度只有在程序员须要更新、保护、排查问题的时候才有意义。一个不须要演进和保护的零碎其架构、代码如何关系也就不大了(尽管事实中这种状况很少)。

既然“软件设计和实现的实质是工程师互相通过写作来交换一些蕴含丰盛细节的抽象概念并且一直迭代过程”(第三次强调了),那么,复杂度指的是软件中那些让人了解和批改保护的艰难水平。相应的,简略性,就是让了解和保护代码更容易的因素。

“The goal of software architecture is to minimize the manpower required to build and maintain the required system.”Robert Martin, Clean Architecture [3].

因而咱们将软件的复杂度合成为两个维度,都和人了解与保护软件的老本相干:

  • 第一,认知负荷 cognitive load:了解软件的接口、设计或者实现所须要的心智累赘。
  • 第二,协同老本 Collaboration cost:团队保护软件时须要在协同上额定付出的老本。

咱们看到,这两个维度有所区别,然而又互相关联。协同老本高,让软件系统演进速度变慢,效率变差,工作其中的工程师压力增大,而长期难以获得停顿,工程师偏向于来到我的项目,最终造成品质进一步下滑的恶性循环。而认知负荷高的软件模块让程序员难以了解,从而产生两个结果:(1) 保护过程中易于出错,bug 率故障率高;(2) 更大机率 团队人员变动时被摈弃,新成员抉择重整旗鼓,原有投入被节约,甚至更高蹩脚的是,代码被摈弃然而又无奈下线,成为定时炸弹。

2 影响到认知负荷的因素

认知负荷又能够合成为:

  • 定义新的概念带来认知负荷,而这种认知负荷与 概念和物理世界的关联水平相干。
  • 逻辑合乎思维习惯水平:正反逻辑差别,逻辑嵌套和独立原子化组合。继承和组装差别。

(1)不失当的逻辑带来的认知老本

看以下案例[7]:

A. Code with too much nesting

response = server.Call(request)
 
if response.GetStatus() == RPC.OK:
  if response.GetAuthorizedUser():
    if response.GetEnc() == 'utf-8':
      if response.GetRows():
        vals = [ParseRow(r) for r in
                response.GetRows()]
        avg = sum(vals) / len(vals)
        return avg, vals
      else:
        raise EmptyError()
    else:
      raise AuthError('unauthorized')
  else:
    raise ValueError('wrong encoding')
else:
  raise RpcError(response.GetStatus())

B. Code with less nesting

response = server.Call(request)
 
if response.GetStatus() != RPC.OK:
  raise RpcError(response.GetStatus())

if not response.GetAuthorizedUser():
  raise ValueError('wrong encoding')

if response.GetEnc() != 'utf-8':
  raise AuthError('unauthorized')
 
if not response.GetRows():
  raise EmptyError()

vals = [ParseRow(r) for r in
        response.GetRows()]
avg = sum(vals) / len(vals)
return avg, vals

比拟 A 和 B,逻辑是齐全等价的,然而 B 的逻辑显著更容易了解,天然也更容易在 B 的代码根底上减少性能,且新增的性能很可能也会维持这样一个比拟好的状态。

而咱们看到 A 的代码,很难了解其逻辑,在保护的过程中,会有更大的概率引入 bug,代码的品质也会继续好转。

(2)模型失配:和事实世界不完全符合的模型带来高认知负荷

软件的模型设计须要合乎事实物理世界的认知,否则会带来十分高的认知老本。我遇到过这样一个资源管理零碎的设计,设计者从数学角度有一个十分优雅的模型,将资源账号 用合约来表白(下图左侧),账户的 balance 能够由过往合约的累计取得,确保数据一致性。然而这样的设计,齐全不合乎用户的认知,对于用户来说,感触到的应该是账号和交易的概念,而不是带着简单参数的合约。能够设想这样的设计,其保护老本十分之高。

(3)接口设计不当

以下是一个典型的接口设计不当带来的了解老本。

class BufferBadDesign {explicit Buffer(int size);// Create a buffer with given sized slots
  void AddSlots(int num);// Expand the slots by `num`
  // Add a value to the end of stack, and the caller need to
  // ensure that there is at least one empty slot in the stack before
  // calling insert
  void Insert(int value);

  int getNumberOfEmptySlots(); // return the number of empty slots}

心愿咱们的团队不会设计出这样的模块。这个问题能够显著看到一个接口设计的不合理带来的保护老本晋升:一个 Buffer 的设计裸露了外部内存治理的细节(slot 保护),从而导致在调用最罕用接口“insert”时存在陷阱:如果不在 insert 前查看空余 slot,这个接口就会有异样行为。

然而从设计角度看,保护底层的 Slot 的逻辑,也内部可见的 buffer 的行为其实并没有关联,而只是一个底层的实现细节。因而更好的设计应该能够简化接口。把 Slot 数量的保护改为外部的实现逻辑细节,不对外裸露。这样也齐全打消了因为使用不当带来问题的场景。同时也让接口更易于了解,升高了认知老本。

class Buffer {explicit Buffer(int size); // Create a buffer with given sized slots  
  // Add a value to the end of buffer. New slots are added   
  // if necessary.   
  void Insert(int value);
}

事实上,当咱们发现一个模块在应用时具备如下特点时,个别就是难以了解、容易出错的信号:

  • 一个模块须要调用者应用初始化接口能力失常行为:对于调用者来说,须要调用初始化接口看似不是大的问题,然而这样的模块,带来了多种后患,尤其是当存在多个参数须要设置,互相关联关系简单时。配置问题应该独自解决(比方通过工厂模式,或者通过独自的配置零碎来治理)。
  • 一个模块须要调用者应用后做清理 / finalizer 能力失常退出。
  • 一个模块有多种形式让调用者实现完全相同的性能:软件在保护过程中,呈现这种情况可能是因为初始设计不当起初批改设计 带来的冗余,也可能是设计原版的缺点,无论如何这种模块,带着强烈的“坏滋味”。

完全避免这些问题很难,然而咱们须要在设计中尽最大致力。有时通过文档的解释来补救这些问题是必要的,然而好的工程师 / 架构师,应该苏醒的意识到,这些都是“坏滋味”。

(4)一个简略的批改须要在多处更新
简略批改波及多处更改也是常见的软件维护复杂度因素,而且次要影响的是咱们的认知负荷:保护批改代码时须要破费大量的精力确保各处须要批改的中央都被关照到了。

最简略的情景是代码当中有反复的“常数”,为了批改这个常数,咱们须要多处批改代码。程序员也晓得如何解决这一问题,例如通过定义个 constant 并处处援用防止 magic number。再例如网页的格调 / 色调,每个页面雷同配置都反复设置同样的色调和格调是一种模式,而采纳 css 模版则是更加易于保护的架构。这在架构准则中对应了数据归一化准则(Data normalization)。

略微简单一些的是相似的逻辑 / 或者性能被 copy-paste 屡次,起因往往是不同的中央须要略微不同的应用形式,而过来的维护者没有及时 refactor 代码提取公共逻辑(这样做往往须要更多的工夫精力),而是省工夫状况下抉择了 copy-paste。这就是常说的 Don’t repeat yourself 准则:

Every piece of knowledge must have a single, unambiguous, authoritative representation within a system[8]

(5)命名

软件中的 API、办法、变量的命名,对于了解代码的逻辑、范畴十分重要,也是设计者清晰传播用意的要害。然而,在很多的我的项目里咱们没有给 Naming / 命名足够的器重。

咱们的代码个别会和一些我的项目关联,然而须要留神的是我的项目是形象的,而代码是具体的。我的项目或者产品能够随便一些命名,如阿里云喜爱用中国现代神话(飞天、伏羲、女娲)命名零碎,K8s 也是来自于希腊神话,这些都没有问题。而代码中的 API、变量、办法不能这样命名。

一个不好的例子是前一段咱们的 Cluster API 被命名为 Trident API(三叉戟),构想一下代码中的对象叫 Trident 时,咱们如何了解在这个对象应该具备的行为?再比照一下 K8s 中的资源:Pod, ReplicaSet, Service, ClusterIP,咱们会留神到都是清晰、简略、间接合乎其对象特色的命名。名实相符能够很大水平上升高了解该对象的老本。

有人说“Naming is the most difficult part of software engineering9”,或者也不齐全是个玩笑话:Naming 的难度在于对于模型的深刻思考和形象,而这往往的确是很难的。

须要留神的是:

(a)Intention vs what it is

须要防止用“是什么”来命名,要用“for what / intention”。“是什么”来命名是会很容易将实现细节。比方咱们用 LeakedBarrel 做 rate limiting,这个类最好叫 RateLimiter,而不是 LeakedBarrel:前者定义了用意(做什么的),后者 形容了具体实现,而具体实现可能会变动。再比方 Cache vs FixedSizeHashMap,前者也是更好的命名。

(b)命名须要合乎以后形象的层级

首先咱们软件须要始终有清晰的形象和分层。事实上咱们 Naming 时遇到困难,很多就是因为软件曾经不足明确的形象和分层带来的表象而已。

(6)不晓得一个简略个性须要在哪些做批改,或者一个简略的改变会带来什么影响,即 unknown unknowns

在所有认知复杂度的体现中,这是最坏的一种,可怜的是,所有人都已经遇到过这样的状况。

一个典型的 unknown unknown 是一部分代码存在这样的状况:

  • 代码不足充沛的测试笼罩,一些重要场景依赖维护者手工测试。
  • 代码有暗藏 / 不易被发现的行为或者边界条件,与文档和接口形容并不合乎。

对于维护者来说,改变这样的代码(或者是改变影响到了这样代码 / 被这样代码影响到了)时,如果依照接口形容或者文档进行,没发现暗藏行为,同时代码又不足足够测试笼罩,那么就存在未知的危险 unknown unknowns。这时呈现问题是很难防止的。最好的形式还是要尽量避免咱们的零碎品质劣化到这个水平。

上线时,咱们最大的噩梦就是 unknown unknowns:这类危险,咱们无奈预知在哪里或者是否有问题,只能在软件上线后遇到问题才有可能发现。其余的问题 尚可通过致力来解决(认知老本),而 unknown unknowns 能够说曾经超出了认知老本的范畴。咱们最心愿防止的也是 unknown unknowns。

(7)认知成本低要不易出错,而不是无脑“简化”

从认知老本角度来说,咱们还要意识到,掂量不同计划 / 写法的认知老本,要思考的是不易出错,而不是外表上的简化:外表上简化可能带来实质性的复杂度回升。

例如,为了表白时间段,能够有两种抉择:

// Time period in seconds.
void someFunction(int timePeriod); 
// time period using Duration. 
void someFunction(Duration timePeriod);

在下面这个例子外面,咱们都晓得,应该选用第二个计划,即采纳 Duration 作 time period,而不是 int:只管 Duration 自身须要一点点学习老本,然而这个模式能够防止多个工夫单位带来的常见问题。

3 影响协同老本的因素

协同老本则是增长这块模块所须要付出的协同老本。什么样的老本是协同老本?(1)减少一个新的个性往往须要多个工程师协同配合,甚至多个团队协同配合;(2) 测试以及上线须要协调同步。

(1)零碎模块拆分与团队边界

在微服务化时代,模块 / 服务的切分和团队对齐,更加有利于迭代效率。而模块拆分和边界的不对齐,则让代码保护的复杂度减少,因这时新的个性须要在跨多个团队的状况下进行开发、测试和迭代。

另外一个角度,则是:

  • Any piece of software reflects the organizational structure that produces it.

或者就是咱们常说的“组织架构决定零碎架构”,软件的架构最初会围绕组织的边界而变动(当然也有文化因素),当组织分工不合理时,会产生反复的建设或者抵触。

(2)服务之间的依赖,Composition vs Inheritance/Plugin

软件之间的依赖模式,常见的有 Composition 和 Inheritance 模式,对于 local 模块 / 类之间的依赖还是近程调用,都存在相似模式。

上图左侧是 Inheritance(继承或者是扩大模式),有四个团队,其中一个是 Framework 团队负责框架实现,框架具备三个扩大点,这三个扩大点有三个不同的团队实现插件扩大,这些插件被 Framework 调用,从架构上,这是一种相似于继承的模式。

右侧是组合模式(composition):底层的零碎以 API 服务的形式提供接口,而下层利用或者服务通过调用这些接口来实现业务性能。

这两种模式实用于不同的零碎模型。当 Framework 偏差于底层、不波及业务逻辑且绝对十分稳固时,能够采纳 inheritance 模式,也即 Framework 被集成到团队 1,2,3 的业务实现当中。例如 RPC framework 就是这样的模型:RPC 底层实现作为公共的 base 代码 /SDK 提供给业务应用,业务实现本人的 RPC 办法,被 framework 调用,业务无需关注底层 RPC 实现的细节。因为 Framework 代码被业务所依赖,因而这时业务心愿 Framework 的代码十分稳固,而且尽量避免对 framework 层的感知,这时 inheritance 是一种比拟适合的模型。

然而,咱们要慎用 Inheritance 模式。Inheritance 模式的常见陷阱:

(a)要避免出现治理倒置

即 Framework 层负责整个零碎的运维(framework 团队负责代码打包、构建、上线),那么会呈现额定的协同复杂度,影响零碎演进效率(构想一下如果 Dubbo 的团队要求负责所有的应用 Dubbo 的利用的打包、公布成为一个大的利用,会是如许的低效)。

(b)要防止毁坏业务逻辑流程的封闭性

Inheritance 模式如果使用不当,很容易毁坏下层业务的逻辑形象完整性,也即“扩大实现 1”这个模块的逻辑,依赖于其调用者的外部逻辑流程甚至是外部实现细节,这会带来危险的耦合,毁坏业务的逻辑封闭性。

如果你所在的我的项目采纳了插件 /Inheritance 模式,同时又呈现下面所说的治理倒置、毁坏封闭性状况,就须要反思以后的架构的合理性。

而右侧的 Composition 是更罕用的模型:服务与服务之间通过 API 交互,互相解耦,业务逻辑的完整性不被毁坏,同时框架 /Infra 的 encapsulation 也能保障。同时也更灵便,在这种模型下,Service 1, 2, 3 如果须要也能够产生互相调用。

另外《Effective Java》一书的 Favor composition over inheritance 有很好的剖析,能够作为这个问题的补充。

(3)可测试性有余带来的协同老本

交付给其余团队(包含测试团队)的代码应该蕴含充沛的单元测试,具备良好的封装和接口形容,易于被集成测试的。然而因为 单测有余 / 模块测试有余,带来的集成阶段的复杂度升高、失败率和返工率的升高,都极大的减少了协同的老本。因而做好代码的充沛单元测试,并提供良好的集成测试反对,是升高协同老本晋升迭代效率的要害。

可测试性有余,带来协同老本升高,往往导致的破窗效应:上线越来越靠运气,unknown unknowns 越来越多。

(4)文档

升高协同老本须要对接口 /API 提供清晰的、一直放弃更新统一的文档,针对接口的场景、应用形式等给出清晰形容。这些工作须要投入,开发团队有时不违心投入,然而对于每一个用户 / 应用方,须要依赖钉钉上的询问、或者是依附 ATA 文章(多半有 PR 性质或者是曾经过期,没有及时更新,毕竟 ATA 不是产品文档),协同老本太高,对于零碎来说呈现 bug/ 使用不当的几率大为减少了。

最好的形式:(1)代码都公开;(2)文档和代码写在一起(README.md,*.md),随着代码一起提交和更新,还计算代码行数,多好。

4 软件复杂度生命周期

复杂度的好转到肯定水平,肯定进入有诸多 unknown unknown 的水平。好的工程师肯定要能辨认这样的状态:能够说,如果不投入力量去做肯定的重构 / 革新,有过多 unknown unknowns 的零碎,很难防止失败的厄运了。

这张图是要表明,软件演进的过程,是一个“情不自禁”就会滑向过于简单而无奈保护的深渊的过程。如何要防止失败的厄运?这篇文章的篇幅不容许咱们展开讨论如何防止复杂度,然而首要的,对于真正重要的、长生命周期的软件演进,咱们须要做到对于复杂度增量零容忍。

5 Good enough vs Perfect

软件畛域,从效率和品质的折中,咱们会提“Good enough”即可。这个实践是没错的。只不过事实中,咱们极少看到“overly good”,因为过于谋求 perfection 而影响效率的状况。大多数状况下,咱们的零碎是基本没做到 Good enough。

四 对复杂度增长的对策

每一份新的代码的引入,都在减少零碎的复杂度:因为每一个类或者办法的创立,都会有其余代码来援用或者调用这部分代码,因此产生依赖 / 耦合,减少零碎的复杂度(除非之前的代码适度简单 unncessarily complex,而通过重构能够升高复杂度),如果读者都意识到了这个问题,并且那些辨认减少复杂度的关键因素对于大家有所帮忙,那么本文也就达到了指标。

而如何 Keep it simple,是个十分大的话题,本文不会开展。对于 API 设计,在 [5] 中做了一些总结,其余的心愿后续有工夫能持续总结。

有人会说,我的项目交付的压力才是最重要的,不要站着谈话不腰疼。理论呢?我认为相对不是这样。少数状况下,咱们要对复杂度增长采纳靠近于“零容忍”的态度,防止“能用就行”,起因在于:

  • 复杂度增长带来的危险(unknown unknowns、不可控的失败等)往往是后知后觉的,等到问题呈现时,往往 legacy 曾经造成一段时间,或者坑往往是很久以前埋的。
  • 当咱们在代码评审、设计评审时面临一个个抉择时,每一个 Hack、每一个带来额定老本和复杂度的设计仿佛都显得没那么有危害:就是减少了一点点复杂度而已,就是一点点危险而已。然而每一个失败的零碎的问题都是这样一点点积攒起来的。
  • 破窗效应 Broken window:一个修建,当有了一个破窗而不及时修补,这个修建就会被侵入住认为是无人居住的、风雨更容易进来,更多的窗户被人无意突破,很快整个修建会减速破败。这就是破窗效应,在软件的品质管制上这个效应十分失当。所以,Don’t live with broken windows (bad designs, wrong decisions, poor code) [6]:有破窗尽快修。

零容忍,并不是不让复杂度增长:咱们都晓得这是不可能的。咱们须要的是尽力管制。因为进度而长期突破窗户也能承受,然而要尽快补上。

当然文章一开始就强调了,如果所写的业务代码生命周期只有几个月,那么多半在代码变得不可保护之前就能够下线了,那能够不必关注太多,能用就行。

最初,作为 Software engineer,软件是咱们的作品,心愿大家都置信:

  • 真正的工程师肯定在意本人的作品:咱们的作品就是咱们的代码。工匠精力是对每个工程师的要求。
  • 咱们都能够带来扭转:代码是最偏心的工作场地,代码就在那里,只有咱们违心,就能带来变动。

Reference
[1]John Ousterhout, A Philosophy of software design
[2]Frederick Brooks, No Silver Bullet – essence and accident in software engineering
[3]Robert Martin, Clean Architecture
[4]https://medium.com/monsterculture/getting-your-software-architecture-right-89287a980f1b
[5]API 设计最佳实际思考 https://developer.aliyun.com/article/701810
[6]Andrew Hunt and David Thomas, The pragmatic programmer: from Journeyman to master
[7]https://testing.googleblog.com/2017/06/code-health-reduce-nesting-reduce.html
[8]https://en.wikipedia.org/wiki/Don%27t_repeat_yourself
[9]http://www.multunus.com/blog/2017/01/naming-the-hardest-software/
[10]https://martinfowler.com/bliki/TwoHardThings.html

退出移动版