浅谈面向对象中的一些主要思想
何为OOP
OOP是一种思想,即为面向对象编程,将数据和行为进行封装并看作对象进行操作,这一点很多资料书籍都提过,OOP的核心是一种思想,是解决实际问题时需要的一种思考方式,在这里,我想以一个例子切入,来谈一谈的对与OOP的理解。
人作为现实生活中的一个实体,我们可以很直观的看到,人都会有姓名,年龄,体重,身高等等的一些公共属性,除此之外,人还会说话,会吃饭,会睡觉等等一系列的行为,于是,我们进行总结,人是一种具有姓名、年龄、体重……且会说话、睡觉……的物类,而这个总结,几乎适用于所有的人,于是,人类的概念被概括出来,而我们每一个人,即是人类这个概念中的具体实体,即为一个对象。然后,每个对象(实际生活中的你我他)在一个环境里生活,交流,工作。
抽象:
以上这个例子,我们会现,我们首先是对一个实体,抽取这个实体群中所共有的属性、动作,换言之,就是抽取了公共的部分,进行一个整合,然后进行一个类别的定义,这是一个很平常的思维方式,经过以上的一个过程,就得到了一个有描述的定义。对于编程中,OOP中需要对我们程序中的一些主体进行特征的抽取、然后定义,这就是OOP的第一步,抽象,即将实际存在或需求中的事物进行泛化,提取公共部分,进行类型的定义。
对象:
生活中的你我他,是作为人类这个定义中的具体,即一个抽象类型的具体,我们将一个定义抽象出来之后,可以根据这个定义,任意的产生一个具体的实例,这就是编程中的Class与具体的new Object,对象是根据抽象出的类型的实例化,我们定义了人类的特征和行为(即编写了一个Class),便可以根据这个Class,产出一个具体的个体来(new 出一个对象),就像我们每个人生活在地球这个环境中交流,工作一样,程序中的也是每个不同类型的具体对象,进行交流(通信)、工作,在OOP设计的程序中,程序就是一个个不同对象的集合。
面向对象要明确的:
- 一切皆是对象:在程序中,任何事务都是对象,可以把对象看作一个奇特的变量,它可以存储,可以通信,可以从自身来进行各自操作,你总是可以从要解决的问题身上抽象出概念性的组件,然后在程序中将其表示为一个对象。
- 程序是对象的集合,它通过发送消息来告知彼此需要做什么:程序就像是个自然环境,一个人,一头猪,一颗树,一个斧头,都是这个环境中的具体对象,对象之间相互的通信,操作来完成一件事,这便是程序中的一个流程,要请求调用一个对象的方法,你需要向该对象发送消息。
- 每个对象都有自己的存储空间,可容纳其他对象:人会有手机,一个人是一个对象,一个手机也是一个对象,而手机可以是人对象中的一部分,或者说,通过封装现有对象,可制作出新型对象。所以,尽管对象的概念非常简单,但在程序中却可达到任意高的复杂程度。
- 每个对象都拥有其类型:按照通用的说法,任何一个对象,都是某个“类(Class)”的实例,每个对象都必须有其依赖的抽象。
- 同一类所有对象都能接收相同的消息:这实际是别有含义的一种说法,大家不久便能理解。由于类型为“圆”(Circle)的一个对象也属于类型为“形状”(Shape)的一个对象,所以一个圆完全能接收发送给"形状”的消息。这意味着可让程序代码统一指挥“形状”,令其自动控制所有符合“形状”描述的对象,其中自然包括“圆”。这一特性称为对象的“可替换性”,是OOP最重要的概念之一。
针对OOP的一些设计原则
这部分开始之前,这里推荐先熟悉一下,耦合与内聚:高内聚与低耦合
开闭原则
定义:软件实体应当对扩展开放,对修改关闭
开闭原则的含义是:当应用的需求改变时,在不修改软件实体的源代码或者二进制代码的前提下,可以扩展模块的功能,使其满足新的需求。这里的软件实体可以是项目中划分出的模块,类与接口,方法。开闭原则是面向对象程序设计的终极目标,它使软件实体拥有一定的适应性和灵活性的同时具备稳定性和延续性。实现了该原则,对于测试时,我们只需要测试新增的功能即可,原有代码仍能正常运行,同时提高了代码的复用性,粒度越小,可复用性就越大。
下面通过一个具体的例子来阐述一下开闭原则:
windows的桌面主题,我们应该比较熟悉,Windows 的主题是桌面背景图片、窗口颜色和声音等元素的组合。用户可以根据自己的喜爱更换自己的桌面主题,也可以从网上下载新的主题。这些主题有共同的特点,可以为其定义一个抽象类(Abstract Subject),而每个具体的主题(Specific Subject)是其子类。用户窗体可以根据需要选择或者增加新的主题,而不需要修改原代码,所以它是满足开闭原则的经典例子。
里氏替换原则
定义:继承必须确保超类所拥有的性质在子类中仍然成立
里氏替换原则主要阐述了有关继承的一些原则,也就是什么时候应该使用继承,什么时候不应该使用继承,以及其中蕴含的原理。里氏替换原是继承复用的基础,它反映了基类与子类之间的关系,是对开闭原则的补充,是对实现抽象化的具体步骤的规范。通俗来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。
具体到实现其实就是:
-
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
- 子类中可以增加自己特有的方法
- 当子类的方法重载父类的方法时,方法的前置条件(即方法的输入参数)要比父类的方法更宽松
- 当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的的输出/返回值)要比父类的方法更严格或相等
依赖倒置原则
定义:高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象
上面的话太过于官方,换成民间的话来说就是——要面向接口编程,不要面向实现编程。
由于在软件设计中,细节具有多变性,而抽象层则相对稳定,因此以抽象为基础搭建起来的架构要比以细节为基础搭建起来的架构要稳定得多。这里的抽象指的是接口或者抽象类,而细节是指具体的实现类。使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给它们的实现类去完成。依赖倒置原则是实现开闭原则的重要途径之一,它降低了客户与实现模块之间的耦合。
具体到实现就是:
-
- 每个类尽量提供接口或抽象类,或者两者都具备
- 变量的声明类型尽量是接口或者是抽象类
- 任何类都不应该从具体类派生
- 使用继承时尽量遵循里氏替换原则
举一个简单的例子:
以下是一个墨西哥风味披萨店的类,它拥有获取披萨的方法,客户可以根据这个类的方法来获取披萨吃。
public class MXGPizzaStore{ void getPizza(){ System.out.println("得到一个墨西哥风味的披萨"); } } public class Customer{ void eat(MXGPizzaStore pizza){ pizza.getPizza(); } }
此时如果客户想吃别的口味的披萨,例如纽约风味的,我们必须要修改Customer的eat方法的入参,这就带来了代码修改的麻烦,同时也违背了开闭原则,相反,如果我们一开始定义一个披萨店的接口,所有的披萨店都是实现这一接口,客户只使用接口,这样想要别的口味披萨时也不需要更该代码。
public interface PizzaStore{ void getPizza(); } public class MXGPizza implements PizzaStore{ void getPizza(){ System.out.println("墨西哥风味"); } } public class NyPizza implements PizzaStore{ void getPizza(){ System.out.println("纽约风味"); } } public class Customer{ void eat(PizzaStore pizza){ pizza.getPizza(); } }
单一职责原则
定义:单一职责原则规定一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分
对象不应该承担太多职责,如果一个对象承担了太多的职责,至少存在以下两个缺点:
1.一个职责的变化可能会削弱或者抑制这个类实现其他职责的能力;
2.当客户端需要该对象的某一个职责时,不得不将其他不需要的职责全都包含进来,从而造成冗余代码或代码的浪费。
单一职责原则是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,再封装到不同的类或模块中。单一职责同样也适用于方法。一个方法应该尽可能做好一件事情。如果一个方法处理的事情太多,其颗粒度会变得很粗,不利于重用。
具体例子:
大学学生工作主要包括学生生活辅导和学生学业指导两个方面的工作,其中生活辅导主要包括班委建设、出勤统计、心理辅导、费用催缴、班级管理等工作,学业指导主要包括专业引导、学习辅导、科研指导、学习总结等工作。如果将这些工作交给一位老师负责显然不合理,正确的做 法是生活辅导由辅导员负责,学业指导由学业导师负责。
接口隔离原则
定义:客户端不应该被迫依赖于它不使用的方法,一个类对另一个类的依赖应该建立在最小的接口上
该原则要求程序员尽量将臃肿庞大的接口拆分成更小的和更具体的接口,让接口中只包含客户感兴趣的方法;要为各个类建立它们需要的专用接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用,这一点上,它和单一职责原则很像,都是为了提高类的内聚性、降低它们之间的耦合性,但两者有些许不同,单一职责注重的是职责,而接口隔离注重的是对接口依赖的隔离,单一职责主要约束的是类,它针对程序中的细节,接口隔离原则主要约束接口,主要针对抽象和程序整体框架的构建。
具体实现准则:
-
- 接口尽量小,但是要有限度。一个接口只服务于一个子模块或业务逻辑。
- 为依赖接口的类定制服务。只提供调用者需要的方法,屏蔽不需要的方法。
- 了解环境,拒绝盲从。每个项目或产品都有选定的环境因素,环境不同,接口拆分的标准就不同深入了解业务逻辑。
- 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。
以下为一个具体的实例:
学生成绩管理程序一般包含插入成绩、删除成绩、修改成绩、计算总分、计算均分、打印成绩信息、査询成绩信息等功能,如果将这些功能全部放到一个接口中显然不太合理,正确的做法是将它们分别放在输入模块、统计模块和打印模块等 3 个模块中。
具体实现样例:
public class ISPtest { public static void main(String[] args) { InputModule input = StuScoreList.getInputModule(); CountModule count = StuScoreList.getCountModule(); PrintModule print = StuScoreList.getPrintModule(); input.insert(); count.countTotalScore(); print.printStuInfo(); //print.delete(); } } //输入模块接口 interface InputModule { void insert(); void delete(); void modify(); } //统计模块接口 interface CountModule { void countTotalScore(); void countAverage(); } //打印模块接口 interface PrintModule { void printStuInfo(); void queryStuInfo(); } //实现类 class StuScoreList implements InputModule, CountModule, PrintModule { private StuScoreList() { } public static InputModule getInputModule() { return (InputModule) new StuScoreList(); } public static CountModule getCountModule() { return (CountModule) new StuScoreList(); } public static PrintModule getPrintModule() { return (PrintModule) new StuScoreList(); } public void insert() { System.out.println("输入模块的insert()方法被调用!"); } public void delete() { System.out.println("输入模块的delete()方法被调用!"); } public void modify() { System.out.println("输入模块的modify()方法被调用!"); } public void countTotalScore() { System.out.println("统计模块的countTotalScore()方法被调用!"); } public void countAverage() { System.out.println("统计模块的countAverage()方法被调用!"); } public void printStuInfo() { System.out.println("打印模块的printStuInfo()方法被调用!"); } public void queryStuInfo() { System.out.println("打印模块的queryStuInfo()方法被调用!"); } }