1、OOA、OOD、OOP三个连在一起就是面向对象分析、设计、编程(实现),正好是面向对象软件开发要经历的三个阶段。面向对象分析就是要搞清楚做什么,面向对象设计就是要搞清楚怎么做,面向对象编程就是将分析和设计的结果翻译成代码的过程。面向对象分析和设计的最终产出是类的设计,包括程序被拆解为哪些类,每个类有哪些属性方法、类与类之间如何交互等。
2、面向对象编程的四大特性:封装、继承、多态和抽象。
2.1、封装也称为信息隐藏或数据访问保护,类通过暴露有限的访问接口,授权外部仅能通过类提供的方式(或者叫做函数)来访问内部信息或者数据。封装特性需要编程语言本身提供一定的访问权限控制来支持,如C++中的private和public关键字。类仅通过有限的方法暴露必要的操作,也能提高类的易用性。如果不这样做,调用者就需要对业务细节有足够的了解。这对调用者不仅是一种负担,还会加剧调用者误用的概率,并且也不利于更换类的实现细节。
2.2、抽象强调的是如何隐藏方法的具体实现,调用者只需要关注方法能提供哪些功能,并不需要知道这些功能是如何实现的。在面向对象编程中,常常借助编程语言提供的接口类或者抽象类这两种语法机制来实现抽象这一特性。类的方法是编程语言中的"函数"这一机制来实现的。通过函数包裹具体的逻辑,这本身就是一种抽象。调用者不用关注函数内部的实现逻辑,只需要关注函数的命名、注释和文档了解其提供的功能就可以直接使用了。抽象和封装都是人类处理复杂性的有效手段。在面对复杂性系统时,人脑能承受的信息复杂度是有限的,所以我们必须忽略一些非关键性的实现细节。抽象作为一种只关注功能点而不关注实现的设计思路,正好帮助我们的大脑过滤掉许多非必要的信息。很多设计原则都体现了抽象这种设计思想,比如基于接口而非实现编程、开闭原则、代码解耦等。在定义类的方法时,也需要抽象思维,不要在方法定义中暴露太多的实现细节,以保证在某个时间点需要改变方法的实现逻辑时不需要去修改方法的定义。
2.3、多态:指子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现。多态除了可以通过继承实现,还可以通过ducking-type实现:两个类只要具有相同的方法,就可以实现多态,并不会要求两个类之间有任何关系。ducking-type是一些动态语言所特有的语法机制(golang作为一门静态语言,也可以使用ducking-type)。多态能够提高代码的可扩展性和复用性。
封装主要强调如何隐藏信息、保护数据,抽象主要如何隐藏方法的具体实现,调用者只需要关注方法提供了哪些功能,不需要知道这些功能的具体实现逻辑。抽象的意义:一方面提高代码的可扩展性、维护性,修改接口的实现不需要修改定义,减少代码的改动范围;另一方面,它也是处理复杂系统的有效手段,能有效地过滤不必要关注的信息。
3、从类的继承层次上来看,抽象类是一种自下而上的设计思路,先有子类的重复代码,然后再抽象成上层的父类(即抽象类)。而接口恰好相反,它是一种自上而下的设计思路。我们在编程的时候,一般都是先设计接口,再去考虑具体的实现。接口仅仅是对方法的抽象,是一种has-a的关系,表示具有某一组行为特性,是为了解决解耦问题,隔离接口和具体的实现,提高代码的可扩展性。
4、面向接口而非实现编程,应用这条原则可以将接口和实现相分离,封装不稳定的实现,暴露稳定的接口。上游系统面向接口而非实现编程,不依赖于不稳定的实现细节,这样当实现细节发生变化时,上游系统的代码基本不需要做改动,以此来降低耦合性,提高扩展性。实际上,基于接口而非实现编程这条原则的另一个表述方式是“基于抽象而非实现编程”。在软件开发中,最大挑战之一就是需求的不断变化。越抽象、越顶层、越脱离具体某一实现的设计,越能提高代码的灵活性,越能应对未来的需求变化。好的代码设计不仅能应对当前的需求,而且在需求发生变化的时候,仍然能够在不破坏原有代码设计的情况下灵活应对。而抽象是提高代码扩展性、灵活性、可维护性最有效的手段之一。
5、在编写代码时,要遵从“基于接口而非实现编程”的原则,需要做到下面3点:1)函数的名称不能暴露任何实现细节;2)封装具体的实现细节;3)为实现类定义抽象的接口,具体的实现类都依赖统一的接口定义,使用者依赖接口而不是具体的实现来编程。
6、面向对象分析的产出是详细的需求描述,面向对象设计的产出就是类,在面向对象设计缓解,我们将需求描述转化为具体的类设计,将这一环节拆解细化后分为:1)划分职责进而识别出有哪些类;2)定义类的属性和方法;3)定义类与类之间的交互关系;3)将类组装起来提供执行入口。
7、一种类的识别方法:把需求描述描述中的名词罗列出来,作为可能的候选类,然后再进行筛选。
8、单一职责原则:一个类或者一个模块只负责完成一个职责(或者功能)。模块可以视为比类更加粗粒度地代码块,模块中包含多个类,多个类组成一个模块。单一职责的理解:不要设计大而全的类,要设计粒度小功能单一的类。换个角度来说,一个雷包含了两个或者两个以上业务不相干的功能,则其职责不够单一,应该将它拆分成多个功能更加单一、粒度更细的类。不同的应用场景、不同阶段的需求背景下,对同一个类的职责是否单一的判定可能都是不一样的。在某种应用场景或者当前的需求背景下,一个类的设计可能已经满足单一职责原则了,但如果换个应用场景或者在未来的某个需求背景下,可能就不满足了。
9、在开始时,我们可以先写一个粗粒度地类,满足业务需求。随着业务的发展,如果粗粒度的类越来越庞大,代码越来越多,此时我们可以将粗粒度得嘞拆分成几个更细粒度的类,这就是所谓的持续重构。
10、开闭原则:软件实体(模块、类、方法等)应该对“扩展开放,对修改关闭”。添加一个新功能应该是在已有代码的基础上扩展代码(新增模块、类或者方法),而非修改已有的代码(修改模块、类、方法等)。在识别出代码中可变和不可变的部分之后,我们需要将可变部分封装起来,隔离变化,提供抽象化的不可变接口,供上层系统使用。当具体实现发生变化的时候,我们只需要基于相同的抽象接口,扩展一个新的实现,替换掉老的实现即可,上层系统的代码几乎不需要修改。
11、里氏替换原则:子类对象能够替换程序中父类对象出现的任何地方并且能保证原来程序的逻辑行为不变以及正确性不被破坏。虽然从定义和代码实现来看,多态和里氏替换有点类似,但是它们的关注角度是不一样的。多态是面向对象编程的一大特性,也是面向对象编程语言的一种语法,是一种代码实现的思路。而里氏替换原则是一种设计原则,是用来指导继承关系中该最累是如何设计的。子类的设计要保证在替换父类的时候,不会改变原有程序的逻辑以及不破坏原有程序的正确性。里氏替换原则还有一个更有指导意义的描述,那就是“designed by contract”,中文翻译为“按照协议设计”。即:子类在设计的时候,要遵守父类的行为约定(或者叫做协议)。父类定义了函数的行为约定,子类可以改变函数的内部实现逻辑,但不能改变函数原有的行为约定。这里的行为约定包括:函数声明要实现的功能;对输入输出、异常的约定,甚至包括注释中所罗列的任何特殊说明。实际上,定义中子类和父类的之间的关系,也可以替换成接口和实现类之间的关系。
12、接口隔离原则:客户端不应该强迫依赖它不需要的接口,此处客户端可以理解为接口的调用者或者使用者。接口可以理解为一组API接口集合、单个API接口或者函数、OOP中的接口概念。将接口理解为一组接口集合时,它可以是某个微服务的接口,也可以是某个类库的接口。在设计微服务或者类库接口的时候,如果部分接口只被部分调用者使用,那就需要将这部分接口隔离出来,单独给对应的调用者使用,而不是强迫其他调用者也依赖这部分不会被用到的接口。如果把接口理解为单个接口或者函数时,那么接口隔离原则就可以理解为:函数的设计要功能单一,不要将多个不同的功能逻辑在同一个函数中实现。接口隔离原则与单一职责原则有点类似,不过还是有稍微区别的。单一职责原则针对的模块、类、接口的设计。而接口隔离原则原则相对于单一职责原则,一方面它更侧重于接口的设计,另一方面它思考的角度不同,接口隔离原则提供了一种判断接口是否职责单一的标准,通过调用者如何使用接口来间接的判定。如果调用者只使用部分接口或者接口的部分功能,那么接口的设计就不够职责单一。
13、框架实现“控制反转”:框架提供了一个可扩展的代码骨架,用于组装对象、管理整个执行流程。程序员利用框架进行开发的时候,只需要往框架预留的扩展点上,添加自己业务相关的代码,就可以利用框架来驱动整个程序流程的执行。此处的控制指定的是对程序执行流程的控制。 而“反转”指的是在没有使用框架之前,程序员自己控制整个整个程序的执行。在使用框架之后,整个程序的执行流程可以通过框架来控制。流程的控制权从程序员反转到了框架。
14、依赖注入:不通过new的方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数传参等方式传递(或注入)给类使用。
15、依赖反转原则:高层模块不应该依赖于低层模块,高层模块和低层模块都应该通过抽象来互相依赖。除此之外,抽象不应该依赖于具体实现细节,具体实现一件依赖于抽象。所谓的高层模块和低层模块,简单的来说就是,在调用链上,调用者属于高层,被调用者属于低层。实际上这条原则主要是用于知道框架层面的设计,在平时的业务代码开发中,高层模块依赖于低层模块是没有问题的。
16、控制反转是一种编程思想,把控制权交给第三方,依赖注入是实现控制反转最典型的方法。低层的实现要符合里氏替换原则。子类的可替换性,使得父类模块或依赖于抽象的高层模块无需更改,实现程序的可扩展性。
17、迪米特法则:不该有直接依赖关系的类之间不要有依赖,有依赖关系的类之间,尽量只依赖必要的接口。也即依赖有限的必要的接口。迪米特法则是希望减少类之间的耦合,让类越独立越好。每个类都应该少了解系统的其他部分。一旦发生变化,需要了解这一变化的类也就会相应越少。
18、高内聚低耦合:一个非常重要的设计思想,能够有效提高代码的可读性和可维护性,缩小功能改动导致的的代码改动范围。高内聚用于指导类本身的设计,松耦合用知道类与类之间依赖关系的设计。所谓高内聚,就是将相近的功能应该放到同一个类中,不相近的功能不要放到同一个类中。相近的功能往往会被同时修改,放到同一个类中修改会比较集中。所谓松耦合,是指代码中类与类之间的依赖关系简单清晰,即使两个类有依赖关系,一个类的代码改动也不会或者很少导致依赖类的代码改动。
19、设计模式的几大原则目的都是希望实现高内聚低耦合,但是出发的角度不一样。单一职责是从自身提供的功能出发,迪米特法则是从关系出发,针对接口而非实现编程是从使用者的角度。殊途同归。
20、系统设计的步骤:1)合理地将功能划分到不同模块中;2)设计模块与模块之间的交互关系;3)设计模块的接口、数据库、业务模型;
21、面向对象设计的本质就是把合适的代码放到合适的类中。合理地划分代码可以实现代码的高内聚、低耦合。类与类之间的交互简单清晰。系统设计就是将合适的功能放到合适的模块中。合理地划分模块可以做到模块层面的高内聚、低耦合、架构整洁清晰。
22、系统之间常见的交互模式有两种:同步的接口调用和利用消息中间件的异步调用。同步调用简单直接,异步调用解耦效果更好。上下层之间的调用倾向于通过同步接口,同层之间的调用倾向于异步消息调用。
23、业务系统的设计与开发工作主要包括:接口设计、数据库设计和业务模型设计(即业务逻辑)。数据库和接口的设计非常重要,一旦设计好并投入使用之后,这两部分都不能轻易改动。改动数据库表结构需要涉及数据的迁移和适配;改动接口需要推动接口的使用者做相应的代码修改。这两种情况,即使是微小的改动,执行起来都会非常麻烦。因此设计接口和数据库的时候,一定要多花点心思和时间切不可随意。相反,业务侧代码侧重内部实现,不涉及被外部依赖的接口,也不包含持久化的数据,所以对改动的容忍性更大。
24、分层的作用:1)分层能起到代码复用的作用;2)分层能起到隔离变化的作用(分层体现了一种抽象和封装的设计思想);3)分层能起到隔离关注点的作用;4)分层能够提高代码的可测试性;5)分层能应对系统的复杂性(当一个类或者函数的代码过多之后,可读性和可维护性就会变差,此时就需要拆分。拆分有垂直方向和水平方向。水平方向基于业务来做拆分,就是模块化;垂直拆分基于流程来做拆分,就是分层)。
25、无论是分层、模块化还是OOP、OOD以及各种设计模式、原则和思想,都是为了应对复杂系统,解决系统的复杂性。对于简单系统而言,其实是发挥不了作用的。
26、为了尽量减少每层之间的耦合,把职责边界划分明确,每层都会维护自己的数据对象,曾与层之间通过接口交互。数据从下一层传递到上一层的时候,将下一层的数据对象转换成上一层的数据对象再继续处理。虽然这样的设计有些繁琐,每层都需要定义各自的数据对象,需要数据对象之间的转化,但是分层清晰。对于大型项目来说,结构清晰是第一位的。
27、针对稍微复杂系统的开发,可以借鉴TDD(测试驱动开发)和ProtoType(最小原型)的思想,先聚焦于一个简单的应用场景,就此设计实现一个简单的原型。尽管这个最小原型系统在功能和非功能特性上都不完善,但是它能够看得见摸得着。比较具体、不抽象,能够很有效地帮助设计者缕清更复杂的设计思路,是迭代的基础。
28、面向对象设计和实现要做的事情,就是把合适的代码放到合适的类中。
29、重构:在保持功能不变的前提下,利用设计思想、原则。模式、编程规范等理论来优化代码,修改设计上的不足,提高代码的质量。大型重构指的是对顶层代码设计的重构,包括系统、模块、代码结构、类与类之间的关系等的重构,重构的手段有分层、模块化、解耦、抽象可复用组件等,使用的重构工具包括设计思想、原则和模式。小型重构指的是对代码细节的重构,主要是针对类、函数、变量等代码级别的重构,如规范命名,规范注释、消除大类或者函数、提取重复代码等。小型冲个更多的是利用编码规范,这种重构修改的地方比较集中,比较简单、可操作性强、耗时短。
30、最可落地执行、最有效的保证重构不出错的手段就是单元测试。当重构完成之后,如果新的代码仍然能够通过单元测试,那就说明代码原有逻辑的正确性未被破坏,原有的外部可见行为未变。
31、代码解耦的手段:1)封装与抽象,封装与抽象可以有效地隐藏实现的复杂性,隔离实现的易变性,给依赖的模块提供稳定且易用的抽象接口;2)中间层,引入中间层能够简化模块或类之间的依赖关系,重构的时候引入中间层可以起到过度的作用,让开发和重构同步进行互不干扰;3)模块化:模块化是构建复杂系统常用的手段,对于一个大型复杂的系统来说,没有人能够掌控所有的细节,因此将系统划分成各个独立的模块,让不同的人负责不同的模块,这样即使在不了解全部细节的情况下,管理者也能协调各个模块,让整个系统有效运转。不同的模块之间通过api来进行通信,每个模块之间耦合很小,每个小的团队聚焦于一个独立的高内聚模块来开发,最终像搭积木一样将各个模块组装起来,构建成一个复杂的系统。在代码层面,合理地划分模块能够有效地解耦代码,提高代码的可读性和可维护性。模块化的思想无处不在,像SOA、微服务、lib库、系统内模块划分、甚至是类、函数的设计都体现了模块化思想。如果追本溯源,模块化更加本质的东西就是分而治之。
32、实现高内聚低耦合的原则:1)单一职责原则:模块或者类的职责设计得单一,而不是大而全,那依赖于它的类和它依赖的类就会比较少,代码耦合也就相应的降低了;2)依赖于接口而非实现编程,基于接口这样一个中间层,隔离变化和具体的实现。一个模块或者类的改动不会影响到另一个模块或类。实际上这就相当于将一种强依赖关系(强耦合)解耦为了弱依赖关系(弱耦合);3)依赖注入:与基于接口而非实现编程的思想类似,依赖注入也是将代码之间的强耦合变为弱耦合。4)多用组合少用继承:继承是一种强依赖关系,父类与子类高度耦合,且这种耦合关系非常脆弱,牵一发而动全身,父类的每一次改动都会影响所有的子类。组合是一种弱依赖关系,这种关系更加灵活。对于继承结构非常复杂的代码,利用组合来替换继承,也是一种解耦的有效手段。5)迪米特法则:不应高有直接依赖关系的类之间不要有依赖,有依赖关系的类之间,尽量只依赖必要的接口。
33、面向对象四大特性总结:
1)封装:封装也叫信息隐藏或者数据访问保护,类通过暴露有限的访问接口,授权外部仅能通过类提供的方法来访问内部信息或者数据。封装一方面保护数据不被随意修改提高代码的可维护性,另一方面是仅暴露有限的必要接口,提高类的易用性。
2)抽象:讲如何隐藏方法的具体实现,让使用者只需要关注方法提供了哪些功能,不需要关注这些功能是如何实现的。抽象存在的意义:一方面是修改实现不需要改变定义;另一方面,它也是处理复杂系统有效的手段,能有效过滤不必要关注的信息。
3)继承:表示类之间的is-a关系,主要用来解决代码服用的问题。
4)多态:指子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现。多态可以提高代码的扩展性和复用性,是很多设计模式、设计原则、编程技巧的代码实现基础。
34、抽象类和接口:抽象类不允许被实例化,只允许被继承,它可以包含属性和方法。方法既可以包含代码实现也可以不包含代码实现。接口不能包含属性,只能声明方法,方法不能包含代码实现。抽象类是对成员变量和方法的抽象,是一种is-a关系,专注于解决代码复用问题;接口仅仅是对方法的抽象,是一种has-a关系,表示具有某一组行为特性,是为了解决解耦问题,隔离接口和具体的实现,提高代码的扩展性。