面向对象编程(Object-oriented Programming,缩写:OOP)是软件工程中一种具有对象
概念的编程范式(Programming Paradigm),同时也是一种程序开发的抽象方针,与之对应的编程范式还有:函数式编程(Functional Programming)、过程式编程(Procedural Programming)、响应式编程(Reactive Programming)等。
在面向对象编程世界里,一切皆为对象,对象是程序的基本单元,对象把程序与数据封装起来提供对外访问的能力,提高软件的重用性,灵活性和扩展性。例如,Java中的java.lang.Object
对象,可以表示Java中的一切对象(注意区分8种基本数据类型)。
在面向对象编程中,通常把对象的数据(字段)称为属性
,把对象的行为称为方法
。
在面向对象编程中,最常见的表现就是基于类(Class)
来表现的,每一个对象实例都有具体的类,即对象的类型。使用类的面向对象编程也称为基于类的编程(Class-based programming)
,如常见的Java,C++;而与之类似的有基于原型的编程(Prototype-based programming)
,如JavaScript。
如,Java中Object obj = new Object();
,其中Object
就是类,而obj
就是具体对象实例。
面向对象的三大特征分别是:封装、继承、多态,这三者是面向对象编程的基本要素
通过对象隐藏程序的具体实现细节,将数据与操作包装在一起,对象与对象之间通过消息传递机制实现互相通信(方法调用),具体的表现就是通过提供访问接口实现消息的传入传出。
封装常常会通过控制访问权限来控制对象之间的互访权限,常见的访问权限:公有(public
),私有(private
),保护(protected
)。某些语言可能还会提供更加具体的访问控制,如,Java的package
。
封装的意义:由于封装隐藏了具体的实现,如果实现的改变或升级对于使用方而言是无感知的,提高程序的可维护性;而且封装鼓励程序员把特定数据与对数据操作的功能打包在一起,有利于应用程序的去耦
。
支持类的语言基本都支持继承,继承即类之间可以继承,通过继承得到的类称为子类,被继承的类为父类,子类相对于父类更加具体化。
子类具有自己特有的属性和方法,并且子类使用父类的方法也可以覆盖(重写)父类方法,在某些语言中还支持多继承,但是也带来了覆盖的复杂性。
继承的意义:继承是代码复用的基础机制
多态发生在运行期间,即子类型多态,指的是子类型是一种多态的形式,不同类型的对象实体有统一接口,相同的消息给予不同的对象会引发不同的动作。
多态的意义:提供了编程的灵活性,简化了类层次结构外部的代码,使编程更加注重关注点分离
(Separation of concerns,SoC)
能够把复杂问题通过抽象简单化,可以为具体问题找到最恰当的类定义,并且可以在最恰当的继承级别解释问题。
对象可以在其实例变量中包含其他对象
随着计算机科学的发展,面向对象也一直在扩展,其实面向对象只是一种编程范例,或者是一种编程思路,只是编码解决问题的一种通用思路,不同语言对于面向对象的支持与实现其实也是大同小异,了解面向对象的思想更为重要。无需纠结概念上的却别,例如,Golang认为组合优于继承,但是从大体来看其实组合和继承最终的结果都是为了复用。
面向对象思想早在20世纪50年代末和60年代初就已经被提出,第一个真正实现面向对象的语言Smalltalk,也就在20世纪70年代出现;面向对象的提出就是为了提高软件的重用性、灵活性和扩展性。
早期的编程范式就是过程式编程,因为计算机运行的时候就是一行一行指令执行,所以传统的编程方式就是把程序看成一系列函数的集合,或者直接向机器发出指令(如,汇编语言),这就是面向过程的编程。而随着计算机的发展,以及过程式编程暴露出来的问题,如无法复用,不灵活,不符合人类的思维方式等等,这就是面向对象思想产生的原因,人们希望编程是更加灵活更加符合人类思维方式的,面向对象编程本质可以看成是由各种独立而互相调用的对象组成的程序,而且事实证明,面向对象确实比过程式更加灵活,更加容易维护。
由于面向对象的各种特点,使得面向对象编程更加容易学习,是复杂的问题简单化,是程序更加便于分析、设计和理解。
那么,既然面向对象如此灵活易用,那么我们还需要其他的编程范式吗?
其实不然,使用面向对象解决问题的时候需要明确抽象的层次,也就是说不同的问题其对应的抽象层次是不同的,比如,起床
这件事情:
class People {
public void getUp(){
}
}
People.getUp();//起床
//起床开始
openEye;//睁眼,开始到结束
dressed;//穿衣服,开始到结束
getOutOfBed;//下床,开始到结束
//起床结束
这样看来,从起床
这件事情来看,面向对象更加简洁明了,但是面向过程在这过程中就没用了么,其实不是,例如,People.getUp
方式可以这么实现:
class People {
public void getUp(){
Eye.open();//睁眼
Body.dressed();//穿衣服
Body.getOutOfBed();//下床
}
}
那么,从起床
这个问题来看,起床的内部实现其实还是面向过程式的,即使用的还是面向对象编程去实现,所以,我个人觉得面向对象是相对的,需要站在解决问题的角度来看待面向对象的抽象层次,与之对于的过程式编程是在不同的层次解决不同的问题,其他编程范式也一样,它们之间可以并存,这并不矛盾。
谈起面向对象编程,就不得不得说设计模式,设计模式
最初来源于建筑设计领域,后由GoF(Gang of Four,四人帮,Erich Gamma,Richard Helm,Ralph Johnson,John Vlissides)引入到计算机科学,在他们合作出版的《设计模式:可复用面向对象软件的基础》(Design Patterns - Elements of Reusable Object-Oriented Software)一书中介绍了23种设计模式,而随着计算机科学的发展,设计模式也越来越多,应用也越来越广泛。
设计模式是一种用于在某个范围内普遍发生的问题的通用解决方法,设计模式不是代码,可以说是一种解决方案或者最佳实践,它是描述在不同情况下要怎么解决问题的一种方案或一种模板。
常见的设计模式:
常见设计模式一个类或则一个模块应当只有一种职责,其提供的服务应该与其责任保持一致,如果存在多种责任则应考虑对其拆分。
软件中的对象(类,模块,函数等等)应该对于扩展是开放的,但是对于修改是封闭的
开闭原则主要思想就是对于扩展的包含,对于修改的限制,新增功能的同时避免修改已有的实现,尽量做到对外提供的功能不变
程序中的对象应该是可以在不改变程序正确性的前提下被它的子类所替换的
里氏替换原则认为子类的功能应该可以完全替换父类并且不会影响程序的正确性,简单理解就是子类在继承父类的同时不能改变父类已有的功能,加上开闭原则子类只能对父类进行扩展而不能对父类的功能进行修改。
多个特定功能接口要好于一个宽泛用途的接口
接口隔离强调将大而全的接口拆分成小而精的接口,使用方只需关系自己需要的接口,通过接口隔离有利于系统的解耦,增加程序的易用性和拓展性。
1.高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口
2.抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口
依赖反转原则是指一种特定的解耦(传统的依赖关系创建在高层次上,而具体的策略设置则应用在低层次的模块上)形式,使得高层次的模块不依赖于低层次的模块的实现细节,依赖关系被颠倒(反转),从而使得低层次模块依赖于高层次模块的需求抽象。
通用职责分配软件模式(General Responsibility Assignment Software Patterns,GRASP),面向对象的设计原则,与SOLID设计原则无关,与GoF设计模式也不太相同,GRASP更像是一个设计思想,是在面向对象设计过程中起指导的作用,是长期面向对象编程过程中经过验证且标准的最佳实践,可以说我们通常所说的设计模式是基于GRASP的。
GRASP告诉我们怎样设计问题空间中的类与分配它们的行为职责,以及明确类之间的相互关系等,下面简单介绍下GRASP的九个原则:
信息专家原则主要用于表明何处委派职责,职责的委派可以是一个方法,字段等。
分配职责的原则:查看给定的职责,确定履行职责所需要的信息,然后确定信息的位置并将其职责分配给它。也就是,将职责分配给拥有履行一个职责所必需所有信息的类。
创建对象是面向对象系统中最常见的活动之一。创建者原则表明对象的创建应该由哪个类负责创建的原则,如果A和B之间符合下面的规则,则表明A的创建可以分配给B: - 实例B包含实例A或者实例B聚合实例A - 实例B记录实例A - 实例B频繁使用实例A - 实例B拥有实例A初始化的全部信息并且在创建的过程中把这些信息传递给A
耦合是衡量一个元素与其他元素的连接,或依赖其他元素的强弱程度。低耦合是一种评估模式,决定了如何将责任的分配
低耦合设计: - 类与类之间的依赖尽可能要降到最小 - 修改一个类对其他类的影响应该是无影响或者要把影响降到最小 - 提高系统的复用性
高内聚是衡量对象保持适当的集中,可管理和可理解的程度,低耦合通常需要高内聚的支持。
高内聚意味着特定元素的职责是强相关且高度集中的,为了实现高内聚通常做法就是类的划分和子系统的划分,若是划分的元素低内聚也就是职责不明确,那么使用者将会难以理解,程序也难以复用,难以维护。
控制器模式是通过控制器(Controller)将系统事件或者一类用例分配给对应职责的对象,这个对象可以是类或模块或子系统,控制器不与UI进行交互,它只负责系统事件的调配。
基于用例的控制器应该负责处理该类别的所有用例,并且是支持多用例的(如,用户相关的用例,新增用户和修改用户等应该统一交给用户控制器处理)
虽然控制器不与UI交互,但控制器通常用于UI层之外的第一层,也就是我们经常使用的MVC软件架构种的C
层(即控制器层),控制器层起组织协调的作用,负责事件的分发委派并返回处理结果。
多态性原则即面向对象的三大特征之一,指的是不同的类型实现统一的接口,使在系统运行期间相同消息发送给不同类型的实例而会有不同行为。
在具有多态性的场景下应该使用多态性操作,而不应该使用具体某个类型(如表现在Java中就是使用接口编程,即IOP)
纯虚构是指一个不代表处理某个问题领域的类,专门用于实现高内聚低耦合
,提高复用性,这总类在领域驱动设计中被称为服务(Service)。
为了实现高内聚
类通常根据功能被划分称为功能集中的类,而这种划分导致使用方需要更多的类从而提高了耦合度,这与低耦合相矛盾,而通过纯虚构可以构造出"虚构类",这种类不是针对某个问题,而是某些能力/功能的抽象划分。(实际使用中例如我们通常使用的分层系统,数据库访问层就属于纯虚构的一种实现)
中介模式是指通过一个中介
来实现两个对象之间的交互实现低耦合。其目的是为了避免两个对象之间产生直接耦合,降低对象之间的耦合度。
同样的,在MVC设计模式中,控制器(Controller)起到的作用就是作为中介连接其数据模型(Model)与视图(View)
与开闭原则类似,通过使用接口封装系统中存在的不稳定点,并且使用多态操作来使用此接口,从而避免不稳定点影响其他对象(类,模块,子系统等)
面向对象编程其实是一个非常系统非常抽象的话题,这里只对主要概念的介绍以及一些个人的看法。要理解并使用面向对象编程需要通过不断的实践和理解,并且需要一定的抽象能力,通过阅读以后的优秀源码也可以增强自己的面向对象编程意识。如果在面向对象抽象过程中一团雾水的话可以通过画图来整理思路,通过面向对象的设计原则来检查自己的设计与实现,循序渐进不断迭代来提高自己的面向对象编程能力和思维方式。