老Y是一个有着十多年阅历的软件开发工程师。下面是老Y和代码架构规划有关的故事。
在软件开发者的职业生涯当中,必定会遇到许多关于代码架构方面的名词,比方“可保护性”、“可扩展性”、“高内聚低耦合”、“组件化”、“分层架构”、“SOLID”等等。在短短几十年的计算机历史中,呈现了许多有关软件架构的经典作品,对架构进行了极端全面又深化的评论,这些作品根本来自于在软件职业有着数十年阅历的大神们。已然现已有了这些全面、体系、深化的架构作品,为什么还要写这篇文章?
从小有句话咱们或许听了无数遍——实践是检验真理的唯一标准。即使读过了这些作品,听了大神们的分享,咱们仍然不能说自己把握了软件架构的规划。恰似那些自己不亲身踩过坑就无法懂得的道理一般,只要在编码的道路上不断实践,不断将理论与实践结合,才干够做到真正的把握。所以本文仅站在一个普通软件开发者的视点,从架构规划上的一些阅历和踩过的坑动身,来谈谈代码规划,评论在寻求更好的架构规划道路上的那些痛苦与挣扎,以及恍然大悟的时间。
老Y说:在写代码的路上,一向要记住一点,“代码是给人看的,而不是机器”,所以那些奇技淫巧并不引荐。
坐井观天
老Y在大学时是电信专业,和编程有关的课程只要C和C ,C 仍是一门选修课,关于网络、操作体系等全然不知,一向认为学好C/C ,有必定的逻辑思维,就算是进入了软件开发职业的大门。彼时写的代码都是一个个允长却又逻辑简略的函数,像是用编程言语写的菜谱。
数千行的main
估量和许多人相同,老Y在校园做过最大的项目是结业规划,选题是依据计算机视觉的交通流量检测体系。在有了计划后,老Y根本没想着要怎么去规划这套体系,而是“文思泉涌”般马上开端编码。因为其间要运用许多的算法,处理流程也十分长,所以终究的代码量有数千行。但那时老Y在开发上并没有模块的概念,所以整个代码库只要一个main.cpp文件,几千行代码堆满其间。文件中充满了无数的函数、全局变量,老Y根本不知道什么是规划,它就像是逻辑的天然倾注,也只要老Y自己才干看了解,可保护性极差。后来他才知道,这种编码还有一个名称,叫过程式编程。老Y乃至还把C 当成C言语来运用,摒弃面向对象特性。
虽然代码写的很差,但在这次的毕设过程中,老Y对软件开发有了开端的认识。“程序=数据结构 算法”,忘了在哪里看过的说法,这次他对这句话颇有了解。因为毕设项目中需求运用许多的算法,所以老Y寻觅了许多的论文,并完成其间的算法。为了让算法能够更简略传递数据,老Y也运用了多种数据结构,最终如同整个项目都由它们构成。记住一次和老Z去吃饭的路上,老Y说这时候才了解“程序=数据结构 算法”。不过现在看来,这句话是在必定的语境下呈现,存在片面性。
MFC启蒙
大部分软件开发都离不开GUI,而老Y开始接触的GUI结构是MFC(Microsoft Foundation Class),估量许多人都没听过,它曾是微软关于Windows编程的官方C GUI库,集成在Visual Studio中,就比方现在UIKit集成在Xcode中相同。MFC或许是许多人了解架构的启蒙,老Y也不破例。
在新建工程时,MFC会默认创立CDocument、CFrame、CApp等文件,开发者只需依葫芦画瓢,在不同当地填写不同的代码即可。但在运用了很长时间后,老Y也没了解为什么要这么划分,反而认为这样散落在不同文件中的规划,充满了诸多不便。而且MFC中包括了许多tricky的宏界说,导致对其运转机制的了解更是难上加难。
直到老Y后来读了侯捷的《深化浅出MFC》,才知道MFC规划的精妙。即使现在MFC现已退出历史舞台,但它不失为学习架构规划的一个十分好的比方,作为微软的官方GUI库,它也在软件开发的历史上留下了浓墨重彩的一笔。顺便一说,侯捷写/译了许多关于C 的经典作品,特别拿手源码分析,比方《STL源码分析》、《Effective C 》、《深度探索C 面向对象模型》(老Y曾被这本书摧残的起死回生),能够说是在C 能够在我国传达最重要的人物,任何一个深化学习过C 的软件开发者或许都听过他的姓名。老Y也是在读完《STL源码分析》之后,写下了知无涯之std::sort源码分析。
MVC or MFC
老Y在结业找工作时阅历过一次形象深化的面试,面试官问,MVC是什么?咱们或许觉得这是送分题,老Y其时却为他的浅薄无知付出了代价。他大吹牛皮的想,是不是面试官说错了,应该是MFC吧,而且真的把自己的主意说了出来,现在常常回想起来都觉得无比尴尬,不知道面试官其时心里活动是什么,只会觉得这傻小子无知且迷之自傲吧。但或许是其它方面打动了面试官,最终老Y拿到了这家互联网公司的offer。
那是第一次有人问老Y代码规划的问题,曩昔更多是问算法、数据结构等计算机根底。老Y在准备面试的过程中,也从来没准备过相关问题,这才闹了笑话。
老Y说:不管校招仍是社招,面试都会看全体体现,不会因为一两个问题没回答好而不经过。
初出茅庐
老Y结业后,参加那家面试时问MVC的互联网公司,做移动端视频App开发。此刻老Y现已有了开端的模块认识,会将不同功用放在不同文件中,而不是像毕设时将全部的代码堆在一个文件。这么多年曩昔,有一个项目中的源代码老Y还时常回想起,叫movie_data.c
,它相当于MVC中的model层,作者便是老Y。
从文件命名中能够看出,它用于处理视频的数据,包括多种场景,从服务端请求、到xml解析、到model的转化等。老Y还明晰的记住其时每个场景的函数,都是一个长达十几种条件分支的巨大switch case
,每个场景都有着相似的代码结构。
// movie_data.c
void function1() {
...
switch (type) {
case A: {
}
case B: {
}
...
case Z: {
}
}
}
void function2() {
...
switch (type) {
case A: {
}
case B: {
}
...
case Z: {
}
}
}
...
void functionN() {
...
switch (type) {
case A: {
}
case B: {
}
...
case Z: {
}
}
}
信任你现已能够看出这儿的问题,老Y在其时现已觉得写起来很古怪,不仅每个函数允长,且存在许多重复的分支逻辑。改动起来也很痛苦,每添加一个type,就得全部的办法都加一条case句子。但其时因为开发时间紧张,又没有任何架构规划方面的理论支撑,老Y并不知道怎么优化。现在回头来看,全部显得那么简略。能够将同一类型的处理封装到一个模块中(那时用的是C,没有class),然后再依据不同的场景类型来注册handler即可。这是老Y首次在工作中认识到除了有数据结构和算法以外,还有架构。
老Y说:每个人都是如此摸爬滚打成长的,时间处于无知与更大的无知之中。
读万卷书
代码大全
后来老Y参加了一家外企,因为节奏比互联网慢许多,所以老Y便运用闲暇时间,恶补了许多本应在校园期间就把握的知识,将各个范畴的经典作品看了一个遍,这其间就包括了对他影响最大的《代码大全》。除了感叹于它的大而全,其间形象最深的有三点:
- 代码规划便是办理杂乱度
- 表驱动法
- DRY准则
第一点办理杂乱度,如上面的movie_data.c
,它将处理type的杂乱度露出给了每个场景的办法,所以每个场景都需求去switch case一番。再如毕设期间长达几千行的main.cpp
,也是因为杂乱度过高,所以需求将它拆到不同的模块中去。移动端开发的MVC、MVVM等,将模块笼统成Model、View、Controller等,都是期望下降某一个模块的杂乱度,在一个模块进行修正时,不需求感知其它模块。代码中的入参也相同,超过了必定的数量后,如7个,那么就需求考虑将它们放在不同的结构中了。乃至全部规划形式都是用于办理软件杂乱度的手法。
第二点是表驱动法,这是一个至今仍然在许多运用且极端高效的办法。仍是以movie_data举例,能够将每个场景的处理封装到对应的办法中,然后经过表查询来处理:
// movie_data.c
void registerHandlers() {
handler1Map = { // 将类型与对应的处理封装到字典中,运用时直接经过类型查到对应的handler
A: handler1A,
B: handler1B,
...
Z: handler1Z
};
handler2Map = {
A: handler2A,
B: handler2B,
...
Z: handler2Z
};
...
handlerNMap = {
A: handlerNA,
B: handlerNB,
...
Z: handlerNZ
};
}
void function1() {
Handler handler = handler1Map[type];
handler();
}
void function2() {
Handler handler = handler2Map[type];
handler();
}
...
void functionN() {
Handler handler = handlerNMap[type];
handler();
}
能够看到,上面的代码明晰了许多。每个场景的funciton不再需求去处理switch case的逻辑,全部的switch case都经过表查询handler1Map[type]
处理,函数内部的杂乱度马上降了下来。
乃至能够进一步的将同一种类型的处理进行封装,以削减不同类型之间的耦合。
// modelA.c
void registerAHandlers() {
// 全部A相关的处理都坐落该模块中,对其它类型都不感知
handlerA[] = [
handler1A,
handler2A,
...
handlerNA
];
}
...
// modelZ.c
void registerZHandlers() {
handlerZ[] = [
handler1Z,
handler2Z,
...
handlerNZ
];
}
// movie_data.c
void registerHandlers() {
handlersMap = {
A: handlerA,
B: handlerB,
...
Z: handlerZ
};
}
void function1() {
Handlers handlers = handlerMap[type];
Handler handler = handlers[0];
handler();
}
void function2() {
Handlers handlers = handlerMap[type];
Handler handler = handlers[1];
handler();
}
...
void functionN() {
Handlers handlers = handlerMap[type];
Handler handler = handlers[2];
handler();
}
再进一步的优化能够在拿到数据后马上获取该类型的全部handlers,后续全部的function中不再传递type,而是直接传递handlers。假设是面向对象言语,能够在创立多个子类,然后由工厂办法依据type创立一个子类的实例。
对本书形象较深的第三点是DRY准则,咱们或许都听过它,Don’t Repeat Yourself,字面意思十分简略,不要重复。重复的意思能够有许多种,比方不要有重复的代码,强调代码的可复用性。工作中有许多这样的场景,要新开发一个功用,发现之前有人写过相似的办法,那么是直接快速拷贝一份代码,改几行?仍是对办法进行笼统,将可复用的部分下沉,提取出参数来区别不同的逻辑?相同运用到上面的movie_data中,前期的movie_data每个场景都有许多的switch case句子,每次进到一个函数中都要处理一轮,重复的逻辑处理。
DRY并不必定是在开发过程中才经常用的准则,它也能够用到其它场合。老Y有段时间需求在一个App上查询社康是否有某个疫苗,查询了全市许多社康都找不到,因为很着急打这个疫苗,所以他接连每天都要查找一遍。操作不算杂乱,但便是比较繁琐,App不支撑查询功用,需求手动切换社康查看。为了避免每一次的手动操作,老Y花了大约一天的时间写了个爬虫,每天守时将全市的社康都查一轮。这两种处理方式的总耗时或许差不多,也许写爬虫还会多一些,但其间的思维却有着比较大的差异。
老Y说:最终再强烈引荐咱们去读一读这本900多页的“砖头”,以上的介绍不及此书的万分之一,作者不仅把软件开发的方方面面讲了个遍,而且文笔极好,读起来十分顺畅。
重构
除《代码大全》以外,老Y在此期间还读了许多规划形式相关的书,比方四人帮的《规划形式》、《重构》、《代码整齐之道》、《HeadFirst规划形式》等,积累了许多关于代码规划的理论知识。
在全部这些书中,老Y形象最深的是在《重构》中读到的“坏滋味”。假设让咱们说出一道菜为什么好,或许会有点难度,但假设让咱们说出一道菜哪里不好,那可就简略多了,或许这是人类的天性,咱们关于找茬愈加拿手,对坏滋味或许比好滋味愈加灵敏,而当咱们发现这道菜中的全部坏滋味并逐个去除,这道菜便离甘旨不远了。代码亦是如此,假设了解了这些准则,在实践中不断训练自己找到代码中“坏滋味”的才干,发现之后,再尝试用这些准则与形式对其进行重构,写出好代码的或许性便大大提高。
老Y说:23个规划形式是“术”,是手法,7大规划准则是“道”,把握它们之后,想要进行架构规划还需求知道力该往哪里使,所以需求找出“坏滋味”的“嗅觉”。最终再加上一点“洁癖”,这是动力。
行万里路
纸上谈来终觉浅,觉知此事要实践。有了许多的理论“锤子”支撑之后,老Y便开端寻觅全部能看到的“钉子”,开端了行万里路的阶段。
了解笼统
在全部下降代码杂乱度的办法中,笼统也许是最重要的一个,乃至能够说软件的实质便是笼统。咱们往常会笼统出来各种函数、类、数据结构,其核心意图仍然是为了下降杂乱度。
整个软件开发都是在做笼统,从底层往上层看,高档言语是对汇编的笼统,下降对汇编了解的杂乱度;高档言语提供的标准库,它是笼统出来下降开发者对底层的杂乱度;网络模型是笼统,它用七层笼统顺次将上一层的杂乱度操控在必定范围内;根底库是笼统,比方网络库将网络相关的才干依据网络模型进行封装,下降了网络运用的杂乱度;事务模块开发也是笼统,事务代码经常会划分为原子才干层、组件层、事务层等,抱负状况下每一层都只会与它下一个层级打交道,然后极大的下降每一层的杂乱度。
相同,不管是规划准则仍是规划形式实质上是讲怎么做笼统,比方前面说到的DRY准则,意图便是将重复的部分笼统成可复用的逻辑,让每一个上层仅关怀这个复用逻辑即可。前面说到的表驱动法也相同,它将switch case笼统到了一个字典中,使每个场景不必去关怀不同type的分支逻辑。
在这家外企,老Y经过几个项目对笼统有了进一步的了解。
老Y说:软件开发目标是下降杂乱度,全部手法的实质是笼统。
日志体系
首先是日志体系,它是每个软件中必不行少的基石。读到这儿,先停一下,假设要你要来规划一个日志体系时,你会怎样做?
之前老Y写过一篇文章来评论日志,从0开端一步步搭建起日志体系。它从简略的printf
动身,逐步添加需求,在一个个问题得处理之后,便笼统出许多概念,比方TraceLevel、Marker、Appender、Formatter、Category等。这看起来十分杂乱,添加了开发者的了解本钱,但当其背后的考虑在提醒之后,你便能够了解它的强壮。正是这些概念的引进,使得日志体系能够满意各种需求,且能够协助开发者方便的进行扩展。Python的logging规划也是相似。
值得注意的是,它们并不是一开端就规划成这样,而是跟着需求的添加,一步步笼统而成。软件开发并不发起过度规划,它发起的是最高效的满意当下需求的一起,又具备较好的可保护性与扩展性。比方假设是写一个操控台的demo,直接用printf
就够了,不需求引进那么杂乱的概念。
下图是核磁共振设备上运转了几十年的日志体系,这也是老Y迄今为止遇到最为杂乱的日志规划。它不仅有一个总的日志文件,每个模块还有各自的日志,一起左面是一台Windows,右边是一台Linux,两者能够完美的协作,并能够在Windows上实时的查看Linux上的运转日志。这其间许多的运用了观察者形式,如Appender。更多细节能够在深化了解log机制一文找到。
老Y说:相似于上图的体系看起来极端杂乱,直接去看代码肯定会懵掉,在了解各种概念之后再去阅览源码,全部会变得简略许多。
单元测试
单元测试结构也是每个言语所必不行少的组成部分,它为单元测试提供了最重要的支撑。相同,在这儿停下来考虑一下,假设要你来规划单元测试结构,你会怎么做?
了解它的同学或许会了解到结构中相同笼统了许多的概念,比方TestSuite、TestCase、TestRunner、TestResult等,而且这些概念在简直全部言语的单元测试结构都相似,比方Java的JUnit、C 的CppUnit、Xcode的XCTest等,原因是它们都源自于同一个库,JUnit(再往前可延伸到SmallTalk的测试结构)。能够看看它的作者,Kent Beck与Erich Gamma(后者是《规划形式》一书的作者、Eclipse的作者、vscode的作者),《代码整齐之道》的第十五章《JUnit内幕》叙述了结构规划背后的故事,两位大神竟然是在一次3小时的飞机旅程中写出了这个结构。
上面是它的类承继关系图,每个类都有其单一的职责,并运用组合与承继,结合在一起:
- TestCase是一个详细的测试用例
- TestSuite是多个TestCase的组合
- TestRunner担任运转全部的Test/TestSuite
- TestFactory是一个创立工厂等
从图上也能够看出这儿运用了几种规划形式:
- TestSuite是组合形式,将TestCase组装成一个树状结构
- TestFactory工厂形式
- 另外还有未画出来的成果告诉,采用了观察者形式
小结
以上两个体系都是短小精干的代码库,读起来很快,画几个UML图就能够十分简略的了解它们的规划。它们熟练的运用了各种规划准则与形式(究竟其间之一是规划形式的作者对吧),读完定有收获。
多媒体缓存
好的命名是成功的一半
假设说软件开发中什么最难,命名或许首战之地。信任咱们都有过被各种仓库、类、函数、变量命名摧残过的阅历,老Y也不破例。这儿想叙述一段老Y有关命名的阅历,看起来仅仅一个命名的问题,实际上不同的命名让整个代码的效果有着彻底不同的解说。
老Y其时面临的问题是要重构支付宝App中的多媒体缓存,这个库要处理包括文件、图片、语音、视频等多媒体资源的缓存。这儿有一个逻辑很杂乱,每个资源有两个id,一个叫做cloudId,它是该资源在服务端的id,能够用这个id去下载对应的资源。另外还有一个id叫localId,它是资源在上传之前,存储于本地的id,比方对相册中的图片进行紧缩之后,会放入缓存中,需求一个key来获取该资源,这个key便是localId,表明其在本地的资源id,比及上传完结后,会从服务端获取一个对应的资源id,即cloudId,终究被相关到本地的缓存资源中。事务侧能够用这两个id中的任何一个来获取缓存资源。原有的存储与查询接口都别离有localId与cloudId的不同API,给运用方也带来了必定的了解本钱。
读到这儿你是否嗅出了很重的“坏滋味”?
跟着事务对缓存运用的杂乱度越来越高,原有的缓存面临着重构,规划数据库的字段与缓存接口时,不管怎么老Y都接受不了这两个命名,localId与cloudId,总觉得它们很古怪,带有太强的完成细节。老Y认为一个更好的规划是不应该露出内部的完成细节。而且作为一个缓存库,不应该了解local与cloud的概念,全部存储的内容在缓存这个笼统层就仅仅是一个资源而已,local与cloud归于更上层的事务逻辑。所以他反复拉着同事评论,这两个字段也迟迟未能确定下来。
怎么让一个缓存资源能够被两个不同的id找到?如安在缓存资源中将这两个id相关起来?这就成了老Y规划时考虑十分久的问题,假设这个问题不处理,重构再往前推动也面临着很大的困难,老Y认为这儿是规划的要害所在,它也决议着底层的数据库规划,所以并不仅是一个姓名那么简略。
念念不忘,必有回想。近一周后的一天,老Y灵光一闪,想到了Linux Shell中的alias,它可认为一个指令创立别号,不管是输入别号仍是原有的指令,都表明同一个意思。老Y想,这儿本来的localId与cloudId是否能够采用相同的方式,互为别号,老Y为自己的这个主意赞不绝口。因此就有了这样的数据库字段规划:key作为资源的常规标识,一起添加一个叫alias_key的字段,作为该资源的别号,与key有平等效果。如此一来,下载资源时,资源会以key存在在缓存中,这儿是key即原有的cloudId字段。在上传时,处理完的相册图片能够采用key来存储资源,即原有的localId字段。在上传成功后,要相关服务端回来的cloudId时,就能够将cloudId设置到alias_key的字段。这样不管是数据库,仍是接口,都对事务层的逻辑不再感知,不管是local或许是cloud,缓存只认key和alias_key,相当于经过提供alias的才干,让缓存能够处理原有的localId与cloudId,看起来高雅了许多。
接口规划上也只要依据key来查询资源或许存储资源的接口,一起添加设置别号的接口,用于相关两个key,仅此而已。运用key仍是alias_key去查询,这些逻辑全都被封装在缓存的内部进行处理,事务侧不再需求考虑这个key是local仍是cloud,极大的简化了接口,方便了事务侧的运用。
在突破了这个“难题”之后,后续的规划就变得简略起来,老Y也一气呵成完结了整个多媒体缓存的规划。后边也公然遇到另外的需求也需求两个key,而且不再是localI与cloudId。需求是当一个用户替换了头像,在新头像下载完结之前采用旧的头像,此刻能够用这一套缓存很方便的处理,即能够给旧头像设置alias_key,当新头像未能下载成功时,缓存会从alias_key中去查找到旧头像,所以快速的完成了这个需求,乃至不必修正一行代码。
这个命名问题给老Y留下了很深的形象,让他深信对命名的坚持是有意义的,而且它不仅是一个名称的问题,而是或许左右整个规划的要害,它决议了规划笼统的好坏。
老Y说:假设没有阅历过命名上的费尽心机,或许也永久无法体会想出一个合适姓名时的愉悦感。
职责链
继续多媒体缓存的规划,除了命名以外,老Y还有一个形象深化的规划问题。
图画缓存的查找在多媒体图画事务中占据着核心的位置,全部看到图片的当地都离不开它,它的性能左右着全体产品的体会。能够想象假设在刷小红书时,图片卡顿对体会造成的负面影响。
图画查询的逻辑也极端杂乱,大致存在这样的查询次序:对应图画的q值->等比图->原图->大图->其它更大尺寸需裁剪的图->图画key的别号等,由前向后,当前的条件不满意时fallback到下一个,当用本来的key没查届时,需求再用alias_key重新查一轮,没有准确的查询届时还需求找到比它大的图再进行裁剪处理。这儿再停顿一下,假设是你,你会怎样写这段查询逻辑?
直观的写法是采用一个巨大的查询逻辑,顺次依照上面的次序进行查询,这儿是否又嗅到了“坏滋味”?
Image *getImage(char *key, Size size, int qValue) {
Image *cache = getCache(key, size, qValue);
if (cache == NULL) {
cache = getCacheWithAliasKey(key, size, qValue);
if (cache == NULL) {
cache = getScacledImage(key, size);
if (cache) {
// crop image
} else {
cache = getOriginalImage(key, size);
if (cache) {
// crop image
} else {
cache = getBigImage(key, size);
...
}
}
}
}
return cache;
}
上面的代码能够看到查询逻辑十分杂乱,每个查询的代码会遭到其它查询的影响。该办法本身现已足够臃肿,假设要调整查询次序时都需求在该办法中去修正,简略出错。这样将导致代码极难保护,且难以扩展。
咱们来看怎样对这个问题进行笼统,这儿的每一个查询能够作为一个单独的类/办法,它们不需求关怀其它的查询是怎样进行的,只需求给定入参,并回来一个成果即可,所以能够笼统成一个查询类,每个查询都对应着一个子类。而查询像是一个链条相同,当前查询完结之后,假设未完结就进行下一步的查询,直到全部的过程都完结后,回来成果即可,老Y想起规划形式中的职责链形式。在图画缓存创立时,将这个职责链组装起来,后续全部的查询只需过一遍职责链。从扩展性上来说,调整某个查询的逻辑时只需求在对应的查询子类中处理,不影响其它的查询。假设要修正查询次序或许添加新的查询,只需在构建职责链时处理。
class Querier {
public:
Image *query(char *key, Size size, int qValue) {
Image *image = getCache(key, size, qValue);
if (image == NULL) {
image = getCacheWithAliasKey(key, size, qValue);
if (image == NULL) {
if (next) {
// 职责链的下一个节点
image = next->query(key, size, qValue);
}
}
}
return image;
}
// 真正的完成在这儿,子类承继
virtual Image *queryImage(char *key, Size size, int qValue) = 0;
Querier(Querier *next): _next(next) {}
private:
Querier *_next;
};
class QValueQuerier {
public:
Image *queryImage(char *key, Size size, int qValue) {
// 依据Q值查询
}
};
class ScacledImageQuerier {
public:
Image *queryImage(char *key, Size size, int qValue) {
// 查询等比图
// 裁剪
}
};
// 其它查询器
...
Querier *querierBuilder {
// 初始化缓存时构建查询链
Querier *querier = new QValueQuerier(new ScacledImageQuerier(xxx));
return querier;
}
// 查询
Image *getImage(char *key, Size size, int qValue) {
return querier->query(key, size, qValue);
}
经过对每种查询的笼统,封装出一个职责链形式的查询链路。看起来代码如同长了许多,但每个查询之间彻底阻隔,杂乱度操控在查询内部。而关于图画缓存来说,它的笼统层所关怀的是这个查询逻辑,并不关怀每个查询详细的完成,所以关于它来说杂乱度也很低,只在于查询链的构建。代码结构十分明晰,易保护,易扩展。
再聊职责链
职责链是一个十分常用的规划形式,老Y除了在上面的多媒体缓存中用到以外,还有一个场景也让其形象深化。
其时在做支付宝的AR,这儿仅评论其间关于相机的处理部分,下图是一个典型的AR场景,画面上或许会有一些输入源,如相机画面、3D模型、水印、贴图等,画面也会有一些特效处理,比方美颜、前置相机的镜像等,终究画面能够预览到屏幕上,也能够保存到文件中。
开始的规划很简略,提供了一个ARCameraView,它的功用十分“全面”,能够操控相机、画面添加美颜、录制视频、烘托上屏等。关于运用者来说,看起来是比较简略的,只需求不停的设置属性,它像是一个Facade,全部的完成都在ARCameraView的内部处理。
有嗅到其间的坏滋味吗?
看起来很简略也很直接对吧,可这个规划存在许多问题:
- 它的扩展性十分差,每新增一个才干,比方添加一个滤镜、特效等,都需求开放新的接口,终究这个ARCameraView将变得十分巨大,接口也许多;
- 关于事务方而言,想要自界说流程好不容易,因为许多逻辑都是固定的;
- 关于完成而言,ARCameraView的内部完成十分杂乱,假设想改一下烘托的流程将大动干戈,shader的杂乱度极高,接近不行保护的状况。
所以在一个五一的假期,老Y铁了心将这个类进行了重构。思维参阅了移动端有名的烘托结构GPUImage,将全体烘托链路笼统成一个职责链,每个节点只专心于自己的逻辑。并引进三个概念,Source、Functor与Output:
- Source是数据源,比方相机、图片、视频等,它们是数据的源头,发生数据给后边的节点进行消费;
- Functor相似于一个handler,它接纳一个数据,处理完结后再输出一个数据,比方滤镜、美颜等才干;
- 最终的Output是输出结点,一般坐落链路的最终,接纳前面节点发生的数据,不再向后传递新的数据,比方录制、预览等。
经过这样的笼统之后,ARCameraView的杂乱度得到了显著的下降,可扩展性得到大幅提高,开放的接口也急剧削减,每种才干只需求关怀本身内部的逻辑。事务侧在初始化时将这些才干组装成一条职责链,开启相机之后便开端加载数据。当新增一种才干时,比方抠像特效,只需求添加一个抠像的functor即可,事务层将它刺进到链路中,现有的才干和链路不需求任何修正。
对了,关于给这个代码库取名老Y也极费心思。因为这儿的整条链路都是在和GPU打交道,所以取名Texel,它本意是GPU纹路中的一个叫“纹素”的概念,对应于图画中的Pixel“像素”,老Y乃至后来还将它印到了球衣上。
状况机
状况机是十分常见的一种规划形式,老Y曾在一个视频播映器的项目中运用过。由所以依据体系的AVPlayer进行封装,所以不需求涉及到播映器解码等更底层的才干。其时还没有在线播映的才干,仅仅是从服务端下载视频文件,下载完结后才干够播映。假设这儿不必状况机,会怎样完成,比较直观的完成如下:
enum Status {
Inited,
NotAvailable,
Available,
...
};
void playVideo() {
switch (status) {
case Inited: {
// get video and play
break;
}
case NotAvailable: {
// downloadVideo and play
break;
}
case Available: {
// get video and play
break;
}
...
}
}
void downloadVideo() {
switch (status) {
case Inited: {
// get video
break;
}
case NotAvailable: {
// download
break;
}
case Available: {
// do nothing
break;
}
...
}
}
是否嗅到了“坏滋味”?
没错,这就和前面的movie_data相同,每一个办法都包括了许多的状况分支,每种状况都要处理不同的逻辑,极端杂乱。换一种思路来看,假设不是从事情动身,而是从状况动身来考虑,每个状况应该呼应不同的事情,播映器的状况也会跟着不同事情的驱动而发生改变。那样每种状况就只用关怀怎么呼应对应事情,以及会向哪个状况扭转。
找到了一张当年画的图,现在看起来仍是很简略。再从实质上来看,状况机仍然是在下降杂乱度,它将状况内部的处理逻辑封装到一个类中,每个状况仅呼应特定的事情,而且只知道它或许跳到哪个状况,关于其它的状况彻底不必了解。而假设是采用过程式的处理方式,或许要在每个办法的内部添加判断,导致每个办法都变得很杂乱。
老Y说:因为规划形式许多,经常会呈现想到用某个规划形式能够完成这个功用,可是忘了该形式详细怎么完成,此刻能够再去翻翻规划形式的介绍。所以不需求彻底记住全部规划形式的完成,更重要的是要知道每个规划形式能够处理什么问题。
随后的故事
老Y在随后的工作中还遇到了许多代码架构的规划工作,从AR到游戏引擎,从职责链到跨渠道DSL调度结构,此刻的问题现已不能用单一的规划形式处理,而是会许多的运用规划准则、规划形式的组合。但它们的实质是相同的,便是倾其所能来下降杂乱度,假设将它们逐个拆解下来,终究仍是会回归到这些准则与形式当中。这儿因为篇幅的原因,不再对后边的几个大型项目展开。
老Y说:架构才干是无数次费尽心机的堆积,它是笼统才干与事务了解的结合。
谈谈事务开发与架构
许多同学是偏事务的开发,有时会自我调侃便是个画UI的,如安在这种相似“搬砖”的工作内容下提高本身的架构才干?
老Y想将事务的开发(也不仅是事务开发,全部开发都是如此)分红四个层次:
- 能打:王宝强的封于修,动作凶恶凌厉
- 又帅又能打:赵文卓的聂风、法海,不仅能打而且动作潇洒
- 又帅又能打又有内在:李连杰的黄飞鸿,动作潇洒自不必说,更是一代宗师
- 又帅又能打又有内在又有影响力:李小龙,让世界认识了我国功夫
第一个层次是开发的根本要求,保证需求高质量交付,完美完成需求文档。但在代码规划上考虑的比较少,功用能够正常运转少出bug。
第二个层次在层次一的根底上更进了一步,除高质量交付以外,它在代码规划上有更多的要求,想方设法让自己的代码愈加高雅,具有可保护性、可扩展性,看着赏心悦目。小到一个变量的命名,大到全体结构的规划,都对自己提出了高要求,恰似“洁癖”一般,不达到自己满意的程度就浑身难受。
层次三在二的根底上,会深化到运用的各种库,去了解它们的原理、规划,把握之后在自己的开发中能够举一返三,能够说它是通往下个阶段必不行缺的。移动端开发有许多的优异开源库,如iOS的SDWebImage、AFNetworking、Masonary、Lottie、YYCache等等。比方说SDWebImage,为什么要规划成多级缓存,它再和YYCache相比,淘汰策略有何不同,是否有更好的规划?它们都是将理论与实践结合起来最好的典范。开源代码是最好的学习材料,老Y从GPUImage中学了职责链,从mediapipe中学了Scheduler,从游戏引擎中学会大型结构的规划等。现在就能够行动起来,能够找一个自己常用的三方库,去看它的源码,画出UML与时序图,在把握了理论之后,这是最好的学习架构的方式,没有之一。
最终一个层次是将自己开发过程中可复用的部分沉积为SDK,乃至开源出去给到其它人运用。咱们日常运用了许多的开源库,许多都是来自于在实际的开发过程中,将问题笼统到必定的层次,然后整理成一个专门的结构来处理相似的问题,继而开源,这方面的比方数不胜数,比方:
- Airbnb的lottie
- Facebook的react native和fishhook
- 微软的PLCrashReporter
- 阿里的Weex
- 微信的JSPatch
- …
这个层次要求会很高,它需求你对一个范畴有比较深的了解以及出色的架构才干才或许做到。但能够先从自己的代码能够比较好的复用做起,能够是小到一个UI组件的复用,比方一个特定场景的控件,也能够是大到一个动画结构如Lottie,要害是锻炼这种思维。不积跬步,无以至千里。
老Y说:最好的代码架构是重构出来的,并不是一蹴即至,所以在刚开端不需求过度规划,去考虑未来许多年的改变。当事务越来越杂乱,人们不再满意于当下的规划,依据对事务的了解逐渐迭代架构以满意当下与将来的需求,必要时或许全部推翻重来。
写在最终
即使读过一百本书,听过无数次分享,或许不阅历过一次就无法真正的学会一件事,人的大脑如同会对这些不发生于本身的道理天生就持抵抗情绪相同,“我知道”与“我真的知道”之间隔着一道必须经过亲身阅历才干跨过的距离。架构亦是如此,关于架构的作品读起来有时会让人觉得过于高屋建瓴,有时会全面又深化细节,假设没有实践的支撑,许多理论难以发生共鸣。所以开发者必须长时间战斗在编码一线,不断去考虑、总结、沉积,没有谁能够仅凭理论就拥有不错的架构才干。
以上这些阅历正是老Y在编码路上一次次既痛苦又兴奋的时间,从架构规划的视点来说,这些观点既不全面,也或许有失偏颇,乃至它们仅仅停留在代码规划的层面,远未谈到架构。关于大部分人来说,这些阅历也过于稀松往常。如文章开头所问,为何还要写?
关于老Y来说,每一次阅历都对他了解架构发生了深远的影响,所以即使曩昔多年,它们仍然历历在目,就像乔布斯所说的Collecting the dots,在未来的某一刻它们或许会发挥意想不到的效果。关于文章前的你,这些阅历能够展示出老Y在面临这些规划问题时怎么考虑,对初窥架构之门的你也许会有所启示,协助你找到归于自己的那些烙印时间,因此成文。
(全文完)
feihu
2024.03.06 于 Shenzhen
一点引荐
源码
源代码是最好的学习代码架构的方式,没有之一,这儿仅罗列几个,以浅到深,咱们能够选择阅览。一起,咱们能够直接看开发过程中经常运用的三方库,找到其源码,以及一些源码分析的文章结合着看,会比较高效。
- AFNetworking:iOS渠道有名的网络结构
- GPUImage:移动端有名的链式GPU烘托结构
- YYCache:iOS渠道上的多级高性能缓存
- ARKit:Apple的ARKit结构
- CppUnit:C 的单元测试结构,短小精干的结构,体现了许多代码规划的考虑
- Mediapipe:Google的跨渠道机器学习结构
- Git:能够看Linus撸出来的第一个版别
- GamePlay3D:跨渠道的小型游戏引擎,但五脏俱全
书
再来引荐几本和架构有关的经典书本:
- 《规划形式》:四人帮关于规划形式的开山祖师,不过太干以致于看起来有点单调,可作为手册,结合下面的HeadFirst一起看
- 《HeadFirst规划形式》:愈加浅显易懂,引荐这本
- 《重构》:给了许多的典范,手把手教你怎么进行代码重构
- 《代码整齐之道》和《架构整齐之道》:关于寻求极致代码和架构的两本书,后者的引荐序是刚刚脱离的左耳朵耗子
- 《代码大全》:和架构关系不大,但逢人必推,它是从校园迈入职场必读的一本书