我们一起来读书吧 关注:111贴子:1,407
  • 1回复贴,共1

架构整洁之道1-6章读书笔记

只看楼主收藏回复

商业竞争的本质在于成本和效率,软件架构架构也是如此, 终极目标是用最小的人力成本满足架构和维护该系统。因此,如果一个产品的版本迭代成本巨大,修改的代码比新增的代码还多,那么这个产品的架构一定是失败的。
衡量一个软件架构的优劣可以用一个简单且有效的模式,就是看随着团队和软件规模的扩大,工程师的个人产出有没有下降。也就是1+1是否等于2,还是远远小于2.
工程师常常说,我们需要先保证交付需求,至于设计和代码质量我们可以未来在重构,工程师在说这样的话时可能真的这么想的,但很快就会忘记了。所以,作为工程师,永远不要给自己代码重构的机会,工程是需要为代码重构行为付出代价。
在实践中应该怎么做呢?
1,这开始编码之前,是否经过精心的设计?
2,实际的编码和自己的设计是否差距甚远?
3,别的工程师是否可以根据这个设计进行开发,而在集成过程中无需修改?
4,工程师的主要日常工作是不是仅仅按照需求文档编写代码并修复bug?
行为价值 VS 架构价值
行为价值是为了完成产品目标,但架构价值是除了完成目标外,还以更小的成本达到可维护性,这样做可能会花费一些时间,但都是值得的。简单来说,就是“紧急”和“重要”哪个更有优先。业务部门有时候没有能力评估架构的重要性,因此工程师需要坚守底线,“重要”比“紧急”更优先。不能为了快牺牲了质量或者陷入扩展、维护的泥潭。这需要工程师和业务部门“坚持斗争”。
编程范式
1,结构化编程
它采用子程序、块结构、for循环以及while循环等结构,来取代传统的goto,是对代码无限跳转的一种限制。很明显,不加限制的goto语句让代码难以控制和维护,几乎所有的语言都支持结构化编程范式,这是现代编程语言的基础。
2,面向对象
面向对象的本质是什么?封装、继承、多态?封装的目的有两个,1:让模块使用者使用更简单,暴露的信息越少使用起来越方便;2:非开源代码隐藏代码实现。在这方面,C语言更胜一筹,它可以通过头文件和指针来隐藏源码的任何实现细节,因此封装性不是面向对象的本质。那么集成呢?如果大家了解brew的话会非常清楚这一天,brew系统是使用C语言实现的,完全模拟了继承,因此继承也不是面向对象的本质,那么是多态吗?如何理解多态?
多态首先是一种面向接口的编程思想,这种思想在累的实现上表现为多态,而在模块的集成上一样可以有多态。例如书中举例在unix上IO操作,输入来源STDIN,我们只需要对它定义一种接口,也是一种约定。这个接口的使用者只需要简单地调用这些接口而无需关心它的具体实现。
多态(或称面向接口)的应用举例:
例1:STDIN,在unix中,一切的数据交换都成为流(stream),所有的流都遵守了一个约定,也就是实现了一套接口,open、close、read、write、seek,因此对流的操作可以任何切换和移植
例2:我在去年开发了一套Flutter异常采集和上报的组件,这个组件首先分成成两部分,采集和上报,两者相互独立。其次,对上报而言采用的就是面向接口的设计,可以上报的控制台、弹窗、文件、百度性能平台等,只要你愿意,甚至可以多种途径的组合。
/// 上报异常的接口
abstract class FXCErrorReporter {
Future<bool?> reportException(
FXCExceptionData data, Map<String, dynamic> extra);
}
/// 控制台输出
class FXCReporterConsole implements FXCErrorReporter
/// 弹窗
class FXCReporterDialog implements FXCErrorReporter
/// 文件
class ECReporterFile implements FXCErrorReporter
例3:前段时间我们在开发群聊功能时候有过一个设计方面的讨论,就是如何把一个较为复杂的功能进行分层,对于一个IM模块而言至少有三个部分:UI、IM消息的接收和发送、数据存储。但无论如何,对于负责UI开发的同学应该只关注数据的拉去、发送和展示,至于这些数据是从IM来的还是哪里来的无需关心,数据的存储更不需要关系。从这个角度看,这也是一种多态,UI层和下层之间应该有一套接口,它提供拉消息、发消息的功能,仅此而已。具体这些消息哪来的,是从缓存读取的还是IM接收的,又是如何发出去的,上层为什么要关心呢。
因此,多态是只是一种手段,每一种语言有自己的实现,C语言可以使用函数指针,C++可以使用虚函数表,Objc用runtime,但不管如何,多态不是面向对象的首创,只是把它发扬光大而且借助于编译器让它更加好用。而面向对象的本质就是通过多态这种手段对代码中的依赖关系进行控制。让编程变得插件化、可插拔。
3,函数式编程
软件的bug产生的原因通常都是变量的值变化导致的,因此函数式编程主张不存储变量,通过一层又一层的函数来完成计算,一个函数的输出作为另一个函数的输入。在架构实践中可以应用的是可变性隔离,也就是把可变的和不可变的隔开,这是一个重要思想。同样上面的群聊模块也可以从这个角度出发,不可变的部分是UI、IM模块、可变的部分很少就是用户的操作记录和消息缓存。


IP属地:河南1楼2022-11-15 23:25回复
    软件设计的五大原则
    软件中层架构原则:
    1. 使软件可容忍被改动 (可维护性)
    2. 使软件更容易被理解 (可读性)
    3. 构建可在多个软件系统中使用 (复用性)
    SRP, Single Resoponlity Principle, 单一职责原则
    单一职责原则,字面意思上理解起来是每个模块只做一件事,但这这是这个原则的一部分。“职责”,可以理解为类或者模块“改变的原因”,如果你能想到多多于一个动机去修改这个类或者模块,那么它就具备类多于一个职责。
    一个应用实例是,一个用于编辑和打印报表的模块,可能有两个修改的原因,第一,修改报表的内容,第二,修改报表的格式。这两方面的修改是由于两个完全不同的原因,一个是报表内容修改,也就是本质的修改,另一个是报表格式修改,也就是表现形式的修改。单一功能原则认为,这两方面的问题实质上是分离的功能,它们应该分离在不同的类或者模块里。
    OCP, Open Close Principle, 开闭原则
    所谓的开闭原则是对扩展开放,对修改关闭。如果我们设计一个模块总会对他修修补补,甚至推倒重来,这样的设计无疑是失败的。
    LSP, Liskov Substitution Principle, 里氏替换原则
    里式替换原则有两层定义:
    A. 如果S是T的子类,则T的对象可以替换为S的对象,而不会破坏程序
    B. 所有引用其父类对象方法的地方,都可以透明的替换为其子类对象
    也就是所有使用T类方法的代码都可以将T类对象换成其他T类子类对象。
    如果继承是为了实现代码重用,也就是为了共享方法,那么共享的父类方法就应该保持不变,不能被子类重新定义。子类只能通过新添加方法来扩展功能,父类和子类都可以实例化,而子类继承的方法和父类是一样的,父类调用方法的地方,子类也可以调用同一个继承得来的,逻辑和父类一致的方法,这时用子类对象将父类对象替换掉时,当然逻辑一致,相安无事.
    如果继承的目的是为了多态,而多态的前提就是子类覆盖并重新定义父类的方法,为了符合LSP,我们应该将父类定义为抽象类,并定义抽象方法,让子类重新定义这些方法,当父类是抽象类时,父类就是不能实例化,所以也不存在可实例化的父类对象在程序里。也就不存在子类替换父类实例时逻辑不一致的可能
    ISP, Interface Separation Principle,接口隔离原则
    接口隔离原则拆分非常庞大臃肿的接口成为更小的和更具体的接口,这样客户将会只需要知道他们感兴趣的方法。这种缩小的接口也被称为角色接口,接口隔离原则的目的是系统解开耦合,从而容易重构,更改和重新部署。其实就是只向客户提供最小的接口集。
    DIP, Dependence Inversion Principle,依赖翻转原则
    通常,在软件设计中高层次代码往往依赖低层次代码提供的功能,这样在开发中随着低层实现的修改,高层代码往往要做相应的修改,这就是一种强耦合,而此原则讲究的是高层次代码值依赖于抽象接口,并面向这些接口进行编程。而低层代码则依赖这些接口并实现它们,这样就变成了低层依赖了高层,或者说依赖了高层依赖的接口,从而实现依赖翻转。


    IP属地:河南2楼2022-12-11 22:03
    回复