极客时间学习笔记
一、面向对象编程 OOP(Object Oriented Programming)
什么是面向对象编程?
面向对象编程是一种编程范式或编码风格。 它以类和对象为组织代码的基本单元, 并将封装、抽象、集成、多态四个特性,作为代码设计和实现的基石。
什么是面向对象编程语言?
面向对象编程语言是支持类和对象的语法机制, 并有现成的语法机制方便的实现面向对象编程四大特性的编程语言。
面向对象编程和面向对象编程语言之间有何关系?
面向对象编程一般使用面向对象编程语言来进行,但是不用面向对象编程语言,我们一样可以进行面向对象编程。 反之,使用面向对象编程语言编写的代码,也不一定是面向对象编程风格,也有可能是面向过程编程风格的。 放宽要求的说,编程语言具备类和对象的语法机制就算得上是面向对象编程语言,不一定要具备所以四大特性。
什么是面向对象分析和面向对象设计?
面向对象分析是要分析清楚做什么。 面向对象设计是要设计清楚怎么做。 两者的产物是类的设计,属性、方法、类之间的关系、交互等。
对于UML你怎么看?
UML(Unified Model Language)统一建模语言。 是对面向对象软件的统一描述规范。 不仅包含类图,还包括用例图、顺序图、活动图、状态图等 有较高的学习成本,虽然是相互沟通的标准。 但如果双方没有对UML有一致对认知,反而会带来理解上对偏差、降低沟通效率。
二、面向对象特性
封装(Encapsulation)
定义:封装也叫作信息隐藏或者数据访问保护。 类通过暴露有限的访问接口,授权外部仅能通过类提供的方式来访问内部信息或者数据。
意义:一方面是保护数据不被随意修改,提高代码的可维护性; 另一方面是仅暴露有限的必要接口,提高类的易用性。
应用:它需要编程语言提供权限访问控制语法来支持。
例如 Java 中的 private、protected、public 关键字;
Objective-C虽没有Public、Private语法。可以从形式上遵从封装的思想。
在.h文件中暴露必要的属性方法,以及合理运用readonly关键字。
TODO:+代码
抽象(Abstraction)
定义:抽象讲的是如何隐藏方法的具体实现,让调用者只需要关心方法提供了哪些功能,并不需要知道这些功能是如何实现的
意义:一方面是提高代码的可扩展性、维护性,修改实现不需要改变定义,减少代码的改动范围; 另一方面,它也是处理复杂系统的有效手段,能有效地过滤掉不必要关注的信息。
应用:抽象可以通过接口类或者抽象类来实现,但也并不需要特殊的语法机制来支持。
只需要提供“函数”这一非常基础的语法机制,就可以实现抽象特性、所以,它没有很强的“特异性”,有时候并不被看作面向对象编程的特性之一。
Java中利用Interface接口、abstract抽象类语法来实现抽象特性
Objective-C中protocal协议与JavaInterface类似
Objective-C没有抽象类的直接语法,可以采取下面的方式间接实现:
1、在类的init方法中增加NSAssert,避免抽象类被实例化;
2、在方法中抛出异常,避免该方法有具体实现。
TODO:+代码
// Objective-C实现抽象类
@implementation AbsBase
- (instancetype)init
{
NSAssert(![self isMemberOfClass:[AbsBase class]], @"AbsBase is an abstract class, you should not instantiate it directly.");
self = [super init];
if (self) {
}
return self;
}
- (void)read
{
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:[NSString stringWithFormat:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)]
userInfo:nil];
}
继承(Inheritance)
定义:继承是用来表示类之间的 is-a 关系。分为两种模式:单继承和多继承。
意义:继承主要是用来解决代码复用的问题。
应用:编程语言需要提供特殊的语法机制来支持。 例如 Java 中的extends 关键字; Objective-C通过冒号“:”语法实现单继承。 Objective-C不支持多继承语法,可以通过组合、协议的方式实现类似的效果。
TODO:+代码
多态(Polymorphism)
定义:多态是指子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现。
意义:多态可以提高代码的扩展性和复用性,是很多设计模式、设计原则、编程技巧的代码实现基础。
应用:多态这种特性也需要编程语言提供特殊的语法机制来实现,比如继承、接口类。
TODO:+代码
三、面向对象vs面向过程
面向过程编程
相较于面向对象编程以类为组织代码的基本单元,面向过程编程则是以过程(或方法)作为组织代码的基本单元。它最主要的特点就是数据和方法相分离。
相较于面向对象编程语言,面向过程编程语言最大的特点就是不支持丰富的面向对象编程特性,比如继承、多态、封装。
面向对象编程优势
- 对于大规模复杂程序的开发,程序的处理流程并非单一的一条主线,而是错综复杂的网状结构。面向对象编程比起面向过程编程,更能应对这种复杂类型的程序开发。
- 面向对象编程相比面向过程编程,具有更加丰富的特性(封装、抽象、继承、多态)。利用这些特性编写出来的代码,更加易扩展、易复用、易维护。
- 从编程语言跟机器打交道的方式的演进规律中,我们可以总结出:面向对象编程语言比起面向过程编程语言,更加人性化、更加高级、更加智能。
面向对象的错误用法
看似是面向对象风格,实际上是面向过程风格的
-
滥用 getter、setter 方法
例如:不必要getter、setter方法暴露
它违反了面向对象编程的封装特性,相当于将面向对象编程风格退化成了面向过程编程风格
在设计实现类的时候,除非真的需要,否则尽量不要给属性定义 setter 方法。
Objective-C中,要慎重将属性暴露在.h中,以及注意readwrite关键字。
-
滥用全局变量
例如:定义一个大而全的 Constants 类,把程序中所有用到的常量放进去。
-
影响代码的可维护性
文件很大,查找修改某个常量会变得比较费时,而且还会增加提交代码冲突的概率。
-
增加代码的编译时间
依赖它的类文件重新编译,影响到开发效率
-
影响代码的复用性
如果我们要复用项目的某个类,而这个类又依赖 Constants 类,即便这个类只依赖 Constants 类中的一小部分常量,我们仍然需要把整个 Constants 类也一并引入。
-
-
滥用全局方法
例如:只包含静态方法不包含任何属性的 Utils 类,是彻彻底底的面向过程的编程风格。但这并不是说,我们就要杜绝使用 Utils 类
解决方法:
1、你要问一下自己,你真的需要单独定义这样一个 Utils 类吗?是否可以把 Utils 类中的某些方法定义到其他类中呢?
2、设计不同的 Utils 类,比如 FileUtils、IOUtils、StringUtils、UrlUtils 等,不要设计一个过于大而全的 Utils 类。
-
定义数据和方法分离的类
为什么容易写出面向过程风格的代码?
1、面向过程编程风格恰恰符合人的这种流程化思维方式。而面向对象编程风格正好相反。它是一种自底向上的思考方式。
它不是先去按照执行流程来分解任务,而是将任务翻译成一个一个的小的模块(也就是类),设计类之间的交互,最后按照流程将类组装起来,完成整个任务。
这样的思考路径比较适合复杂程序的开发,但并不是特别符合人类的思考习惯。
2、面向对象编程要比面向过程编程难一些。在面向对象编程中,类的设计还是挺需要技巧,挺需要一定设计经验的。你要去思考如何封装合适的数据和方法到一个类里,如何设计类之间的关系,如何设计类之间的交互等等诸多设计问题。
心得
1、之所以Unix、Linux 这些复杂的系统,是基于C语言实现。 一是时间上面向过程是继机器指令、汇编之后顺势产生的面向机器形式。 当时面向对象还没有产生或普及。 二是面向过程确实适用于与机器沟通,同时面向过程语言也可以实现面向对象编程。 从而为操作系统上层提供易用的应用接口。
2、面向对象和面向过程两种编程风格,也并不是非黑即白、完全对立的。
项目中合理运用各自特性,以写出易维护、易读、易复用、易扩展的高质量代码为目的。
四、接口vs抽象类
抽象类更多的是为了代码复用,而接口就更侧重于解耦。
从类的继承层次上来看,抽象类是一种自下而上的设计思路,先有子类的代码重复,然后再抽象成上层的父类(也就是抽象类)。
而接口正好相反,它是一种自上而下的设计思路。我们在编程的时候,一般都是先设计接口,再去考虑具体的实现。
语法特性
抽象类
-
抽象类不允许被实例化,只能被继承。
-
可以包含属性和方法。方法既可以包含代码实现,也可以不包含代码实现。不包含代码实现的方法叫作抽象方法。
-
子类继承抽象类,必须实现抽象类中的所有抽象方法。
接口
- 不能包含属性
- 只能声明方法,方法不能包含代码实现。
- 类实现接口的时候,必须实现接口中声明的所有方法。
意义及应用场景
抽象类是对成员变量和方法的抽象,是一种 is-a 关系,是为了解决代码复用问题。
接口仅仅是对方法的抽象,是一种 has-a 关系,表示具有某一组行为特性,是为了解决解耦问题,隔离接口和具体的实现,提高代码的扩展性。
五、基于接口而非实现编程
解读原则中的“接口”
“接口”就是一组“协议”或者“约定”,是功能提供者提供给使用者的一个“功能列表”。
可以将接口和实现相分离,封装不稳定的实现,暴露稳定的接口。上游系统面向接口而非实现编程,不依赖不稳定的实现细节,这样当实现发生变化的时候,上游系统的代码基本上不需要做改动,以此来降低耦合性,提高扩展性。
“基于接口而非实现编程”,这条原则的另一个表述方式,是“基于抽象而非实现编程”。
后者的表述方式其实更能体现这条原则的设计初衷。我们在做软件开发的时候,一定要有抽象意识、封装意识、接口意识。越抽象、越顶层、越脱离具体某一实现的设计,越能提高代码的灵活性、扩展性、可维护性。
应用场景
如果在我们的业务场景中,某个功能只有一种实现方式,未来也不可能被其他实现方式替换,那我们就没有必要为其设计接口,也没有必要基于接口编程,直接使用实现类就可以了。
越是不稳定的系统,我们越是要在代码的扩展性、维护性上下功夫。相反,如果某个系统特别稳定,在开发完之后,基本上不需要做维护,那我们就没有必要为其扩展性,投入不必要的开发时间。
六、多用组合、少用继承
继承的问题
例如鸟类的例子:是否会飞?是否会下蛋?
继承最大的问题就在于:继承层次过深、继承关系过于复杂会影响到代码的可读性和可维护性。
可读性:因为我们要搞清楚某个类具有哪些方法、属性,必须阅读父类的代码、父类的父类的代码……一直追溯到最顶层父类的代码。
可维护性:继承将父类的实现细节暴露给了子类。子类的实现依赖父类的实现,两者高度耦合,一旦父类代码修改,就会影响所有子类的逻辑。
组合相比继承有哪些优势?
继承主要有三个作用:表示 is-a 关系,支持多态特性,代码复用。
我们可以利用组合、接口、委托三个技术手段达成继承的效果。
- is-a 关系,我们可以通过组合和接口的 has-a 关系来替代;
- 多态特性,我们可以利用接口来实现;
- 代码复用,我们可以通过组合和委托来实现;
通过这些形式解决层次过深、过复杂的继承关系影响代码可维护性的问题。
TODO:+代码
应用
尽管我们鼓励多用组合少用继承,但组合也并不是完美的,继承也并非一无是处。在实际的项目开发中,我们还是要根据具体的情况,来选择该用继承还是组合。
如果类之间的继承结构稳定,层次比较浅,关系不复杂,我们就可以大胆地使用继承。反之,我们就尽量使用组合来替代继承。除此之外,还有一些设计模式、特殊的应用场景,会固定使用继承或者组合。
未完待续