文章目录
- 三.软件(面向对象)设计原则
- 3.1 开闭原则(OSP)
- 3.1.1 概述
- 3.1.2 案列
- 3.2 里氏代换原则(LSP)
- 3.2.1 概述
- 3.2.2 案例
- 3.3 依赖倒转原则(DIP)
- 3.3.1概述
- 3.3.2 案例
- 3.4 接口隔离原则(ISP)
- 3.4.1 概述
- 3.4.2 案列
- 3.5 迪米特法则(DP)
- 3.5.1 概述
- 3.5.2 案例
- 3.6 合成复用原则(CRP)
- 3.6.1 概述
- 3.6.2 案列
三.软件(面向对象)设计原则
3.1 开闭原则(OSP)
Open-Closed Principle , OCP
3.1.1 概述
对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类。
3.1.2 案列
如下,分析搜狗输入法皮肤设计:
分析:搜狗输入法的皮肤是输入法背景图片、窗口颜色和声音等元素的组合。用户可以根绝自己的喜爱更换不同的输入法皮肤,也可以从网上下载新的皮肤。这些皮肤有共同的特点,可以将这些共同的的特点抽取到一个抽象类(AbstractSkin)中,而每个具体的皮肤(DefaultSkin、MySkin)是其子类。用户可以根据需要选择或者增加新的主题,而不需要修改原代码。
// 抽象类
public abstract class AbstractSkin {
public abstract void displaySkin();
}
=========================================================
//实现类
public class DefaultSkin extends AbstractSkin{
@Override
public void displaySkin() {
System.out.println("这是默认皮肤...");
}
}
=========================================================
//实现类
public class MySkin extends AbstractSkin{
@Override
public void displaySkin() {
System.out.println("这是自己的皮肤...");
}
}
=========================================================
/**
* 实现聚合的类
*/
public class SouGouInput {
//成员变量是AbstractSkin类型,(实现聚合)
private AbstractSkin skin;
public void setSkin(AbstractSkin skin) {
this.skin = skin;
}
//普通方法
public void display(){
//根据设置(setXxx)的成员变量的不同,来调用不同成员变量的皮肤
//这里的成员变量类型是抽象类型,
//所以是根据传递不同实现类的对象而调用不同的实现类里重写后的方法 来显示不同的皮肤
skin.displaySkin();
}
}
=========================================================
//测试类
public class ClinentTest {
public static void main(String[] args) {
//1. 创建搜狗输入法对象,将各种皮肤聚合到一起
SouGouInput sgi = new SouGouInput();
// 2.创建皮肤对象
//2.1 常规
// DefaultSkin ds = new DefaultSkin();
// 2.2多态
AbstractSkin ds = new DefaultSkin();
MySkin ms = new MySkin();
// 3.将皮肤设置到输入法中
sgi.setSkin(ds);
// sgi.setSkin(ms);
// 4.显示皮肤
sgi.display();//这是默认皮肤...
//当有新的输入法皮肤时候,只需重新创建一个皮肤类去继承抽象类,然后重写里面的抽象方法,再在测试类中添加即可。而不用修改之前的代码
}
}
3.2 里氏代换原则(LSP)
Liskov Substitution Principle , 简称:LSP
3.2.1 概述
里氏代换原则是面向对象设计的基本原则之一。
里氏代换原则:任何基类可以出现的地方,子类一定可以出现。通俗理解:子类可以扩展父类的功能,但不能改变父类原有的功能。换句话说,子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。
如果通过重写父类的方法来完成新的功能,写起来虽然简单,但整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的概率会非常大。
3.2.2 案例
下面看一个里氏替换原则中经典的一个反例:
【例】正方形不是长方形。
在数学领域里,正方形毫无疑问是长方形,它是一个长宽相等的长方形。所以,我们开发的一个与几何图形相关的软件系统,就可以顺理成章的让正方形继承自长方形。
代码如下:
//父类 长方形
public class Rectangle {
private double length;
private double width;
public double getLength() {return length; }
public void setLength(double length) {
this.length = length;
}
public double getWidth() { return width;}
public void setWidth(double width) {
this.width = width;
}
}
======================================================
//子类(正方形) 继承父类(长方形)
//由于正方形的长和宽相同,所以在方法setLength和setWidth中,对长度和宽度都需要赋相同值。
public class Square extends Rectangle{
// 重写父类中的方法
@Override
public void setLength(double length) {
super.setLength(length);
super.setWidth(length);
}
// 重写父类中的方法
@Override
public void setWidth(double width) {
super.setWidth(width);
super.setLength(width);
}
}
======================================================
//测试类
public class Test01 {
public static void main(String[] args) {
// 创建长方形对象
Rectangle r = new Rectangle();
// 设置长宽
r.setWidth(6);
r.setLength(8);
// 扩宽方法
resize(r);
// 打印扩宽后的长和宽
printLengthWidth(r);//8.0 , 9.0
//====以下演示 违背里氏代换原则的效果====
// 创建正方形对象
Square s = new Square();
// 设置正方形的长或者宽
s.setLength(8);
//resize()方法中的形参是父类类型,所以可以传递子类的类型
//是多态形式
resize(s);
printLengthWidth(s);//执行到这里会死循环,知道内存溢出才停止
//所以根据里氏代换原则:任何基类可以出现的地方,子类一定可以出现
//但尽量不要重写父类的方法,如果重写会程序会出问题,比如此处的死循环
}
//扩宽方法
public static void resize(Rectangle r){
//判断宽如果比长小,进行扩宽的操作
while (r.getWidth() <= r.getLength()){
r.setWidth(r.getWidth() + 1);
}
}
//打印长和宽
public static void printLengthWidth(Rectangle r){
System.out.println(r.getLength());
System.out.println(r.getWidth());
}
}
运行上述段代码发现,假如把一个普通长方形作为参数传入resize方法,就会看到长方形宽度逐渐增长的效果,当宽度大于长度,代码就会停止,这种行为的结果符合我们的预期;假如再把一个正方形作为参数传入resize方法后,就会看到正方形的宽度和长度都在不断增长,代码会一直运行下去,直至系统产生溢出错误。所以,普通的长方形是适合这段代码的,正方形不适合。
得出结论:在resize方法中,Rectangle类型的参数是不能被Square类型的参数所代替,如果进行了替换就得不到预期结果。因此,Square类和Rectangle类之间的继承关系违反了里氏代换原则(即任何基类可以出现的地方,子类一定可以出现),它们之间的继承关系不成立,正方形不是长方形。
改进上述代码:
//四边形接口类
public interface Quadrilateral {
public abstract double getLength();
public abstract double getWidth();
}
==========================================================
// 长方形类 实现四边形接口
public class Rectangle implements Quadrilateral{
private double length;
private double width;
public void setLength(double length) {
this.length = length;
}
public void setWidth(double width) {
this.width = width;
}
@Override
public double getLength() { return length; }
@Override
public double getWidth() { return width; }
}
============================================================
// 正方形类 实现四边形接口
public class Square implements Quadrilateral {
private double side;
public double getSide() {
return side;
}
public void setSide(double side) {
this.side = side;
}
@Override
public double getLength() {
return side;
}
@Override
public double getWidth() {
return side;
}
}
==========================================================
public class Test {
public static void main(String[] args) {
// 创建长方形对象
Rectangle r = new Rectangle();
r.setLength(20);
r.setWidth(19);
resize(r);
printLengthAndWidth(r);
// 创建正方形对象
Square s = new Square();
// resize(s);此行编译错误
//因为正方形和长方形已经没有直接关系
printLengthAndWidth(s);
}
//扩宽方法
public static void resize(Rectangle r){
//判断宽如果比长小,进行扩宽的操作
while (r.getWidth() <= r.getLength()){
r.setWidth(r.getWidth() + 1);
}
}
//打印长和宽 接口多态
public static void printLengthAndWidth(Quadrilateral q) {
System.out.println(q.getLength());
System.out.println(q.getWidth());
}
}
3.3 依赖倒转原则(DIP)
Dependency Inversion Principle,DIP
3.3.1概述
高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。
3.3.2 案例
下面看一个例子来理解依赖倒转原则
【例】组装电脑
现要组装一台电脑,需要配件cpu,硬盘,内存条。只有这些配置都有了,计算机才能正常的运行。选择cpu有很多选择,如Intel,AMD等,硬盘可以选择希捷,西数等,内存条可以选择金士顿,海盗船等。
public class InterCpu {
public void runCpu(){
System.out.println("Inter的cpu正在运行...");
}
}
====================================================
public class KingstonMemory {
public void saveMemory(){
System.out.println("使用金士顿作为内存条...");
}
}
====================================================
public class XiJieHardDisk {
public void saveDisk(String data){
System.out.println("从硬盘获取的数据是:"+data);
}
public String getData(){
System.out.println("获取硬盘数据操作...");
return "获取硬盘数据完成。";
}
}
====================================================
public class Computer {
private XiJieHardDisk hardDisk;//硬盘
private InterCpu cpu;// cpu
private KingstonMemory memory;//内存
public Computer() {}
public Computer(XiJieHardDisk hardDisk, InterCpu cpu, KingstonMemory memory) {
this.hardDisk = hardDisk;
this.cpu = cpu;
this.memory = memory;
}
public XiJieHardDisk getHardDisk() {return hardDisk;}
public void setHardDisk(XiJieHardDisk hardDisk) {
this.hardDisk = hardDisk;
}
public InterCpu getCpu() {return cpu;}
public void setCpu(InterCpu cpu) {this.cpu = cpu;}
public KingstonMemory getMemory() {return memory;}
public void setMemory(KingstonMemory memory) {
this.memory = memory;
}
public void runComputer(){
System.out.println("计算机正在工作...");
cpu.runCpu();
memory.saveMemory();
hardDisk.saveDisk("你好Java。");
String data = hardDisk.getData();
System.out.println(data);
}
}
=========================================================
//测试类
public class ComputerTest {
public static void main(String[] args) {
// 创建计算机组件内存、硬盘、cpu
KingstonMemory k = new KingstonMemory();//表示内存
InterCpu i = new InterCpu();// 表示 cpu
XiJieHardDisk x = new XiJieHardDisk();// 表示硬盘
// 创建计算机对象,并组装(即,传入组件参数)
Computer computer = new Computer(x,i,k);
// 运行计算机
computer.runComputer();
}
}
上面代码可以看到已经组装了一台电脑,但是似乎组装的电脑的cpu只能是Intel的,内存条只能是金士顿的,硬盘只能是希捷的,这对用户肯定是不友好的,用户有了机箱肯定是想按照自己的喜好,选择自己喜欢的配件。如果想换成不是Intel的cpu需要修改Computer类,这就违背了开闭原则。
根据依赖倒转原则进行改进:
// Cpu接口
public interface Cpu {
public abstract void runCpu();
}
===========================================================
// 硬盘接口
public interface HardDisk {
public abstract void saveDate(String date);
public abstract String getDate();
}
==========================================================
// 内存接口
public interface Memory {
public abstract void saveMemory();
}
==========================================================
//IntelCpu类实现Cpu接口
public class IntelCpu implements Cpu{
@Override
public void runCpu() {
System.out.println("InterCpu正在运行...");
}
}
==========================================================
// 新增Amd类型的cpu
public class AmdCpu implements Cpu{
@Override
public void runCpu() {
System.out.println("AMD Cpu正在运行...");
}
}
==========================================================
// KingstonMemory类实现Memory接口
public class KingstonMemory implements Memory{
@Override
public void saveMemory() {
System.out.println("使用金士顿作为内存条...");
}
}
==========================================================
// XiJieHardDisk类实现HardDisk接口
public class XiJieHardDisk implements HardDisk{
@Override
public void saveDate(String date) {
System.out.println("从硬盘获取的数据是:"+date);
}
@Override
public String getDate() {
System.out.println("获取硬盘数据操作...");
return "获取硬盘数据完成。";
}
}
============================================================
//聚合各种组件
public class Computer {
private HardDisk hardDisk;
private Cpu cpu;
private Memory memory;
public Computer() {}
public Computer(HardDisk hardDisk, Cpu cpu, Memory memory) {
this.hardDisk = hardDisk;
this.cpu = cpu;
this.memory = memory;
}
public HardDisk getHardDisk() {return hardDisk;}
public void setHardDisk(HardDisk hardDisk) {
this.hardDisk = hardDisk;
}
public Cpu getCpu() { return cpu;}
public void setCpu(Cpu cpu) { this.cpu = cpu;}
public Memory getMemory() { return memory;}
public void setMemory(Memory memory) {
this.memory = memory;
}
//运行计算机方法
public void runComputer(){
System.out.println("计算机正在工作...");
cpu.runCpu();
memory.saveMemory();
hardDisk.saveDate("你好Java。");
String data = hardDisk.getDate();
System.out.println(data);
}
}
=======================================================
//测试类
public class ComputerTest {
public static void main(String[] args) {
// 创建计算机的组件:内存、cpu、硬盘
// Cpu cpu = new IntelCpu();
Cpu cpu = new AmdCpu();
HardDisk hardDisk = new XiJieHardDisk();
Memory memory = new KingstonMemory();
// 创建计算机
Computer computer = new Computer(hardDisk,cpu,memory);
// 运行计算机
computer.runComputer();
}
}
上述代码根据依赖倒转原则改进后扩展性比较好,如想换AMD类型的Cpu,只需子新增一个AmdCpu类去实现Cpu接口,重写Cpu里的抽象方法,再在测试类中去用Cpu接口去接AmdCpu的对象即可,这样就不用修改Computer类里面的代码了。
3.4 接口隔离原则(ISP)
Interface Segregation Principle,简称ISP
3.4.1 概述
客户端测试类不应该被迫依赖于它不使用的方法;一个类对另一个类的依赖应该建立在最小的接口上。
3.4.2 案列
面看一个例子来理解接口隔离原则:
【例】安全门案例
需求:创建一个学校品牌的安全门,该安全门具有防火、防水、防盗的功能。可以将防火,防水,防盗功能提取成一个接口,形成一套规范。类图如下:
// 防盗门
public interface SafetyDoor {
//防盗功能
public abstract void antiTheft();
// 防火功能
public abstract void fireproof();
// 防水功能
public abstract void waterproof();
}
==========================================================
//学校防盗门类 实现防盗门接口
public class SchoolDoor implements SafetyDoor {
@Override
public void antiTheft() {
System.out.println("防盗");
}
@Override
public void fireproof() {
System.out.println("防火");
}
@Override
public void waterproof() {
System.out.println("防水");
}
}
============================================================
public class ClientTest {
public static void main(String[] args) {
// 创建学校防盗门对象
SchoolDoor schoolDoor = new SchoolDoor();
// 调用方法实现防盗门的功能
schoolDoor.antiTheft();
schoolDoor.waterproof();
schoolDoor.fireproof();
}
}
上述代码看似实现了需求的功能,但是如果加入新增一个家庭品牌的安全门,有防盗功能和防火功能,此时如果再定义一个家庭安全门类去实现安全门的接口会造成家庭安全门被迫去实现防水功能,这就违背了接口隔离原则。
根据接口隔离原则,改进如下:
// 防盗接口
public interface AntiTheft {
public abstract void antiTheft();
}
===========================================================
// 防火接口
public interface Fireproof {
public abstract void fireproof();
}
===========================================================
// 防水接口
public interface Waterproof {
public abstract void waterproof();
}
==========================================================
//创建学校防盗门,实现该有功能的接口
nmpublic class SchoolDoor implements AntiTheft,Fireproof,Waterproof{
@Override
public void antiTheft() {
System.out.println("防盗");
}
@Override
public void fireproof() {
System.out.println("防火");
}
@Override
public void waterproof() {
System.out.println("防水");
}
}
=========================================================
//新增家庭品牌安全门 实现该有功能的接口
public class HomeDoor implements AntiTheft,Fireproof{
@Override
public void antiTheft() {
System.out.println("防盗");
}
@Override
public void fireproof() {
System.out.println("防火");
}
}
===========================================================
public class ClientTest {
public static void main(String[] args) {
// 创建学校防盗门
SchoolDoor s = new SchoolDoor();
// 实现学校防盗门功能
s.fireproof();
s.waterproof();
s.antiTheft();
// 创建家庭品牌安全门
HomeDoor homeDoor = new HomeDoor();
//实现家庭品牌安全门的功能
homeDoor.antiTheft();
homeDoor.fireproof();
}
}
3.5 迪米特法则(DP)
Demeter Principle,简称DP
3.5.1 概述
只和你的直接朋友交谈,不跟“陌生人”说话(Talk only to your immediate friends and not to strangers)。
其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。(如,学生通过中介租房,而不是直接联系房东)
迪米特法则中的“朋友”是指:当前对象本身、当前对象的成员对象、当前对象所创建的对象(即 在当前对象的方法中创建其他对象)、当前对象的方法参数(即 当前对象方法的形参是一个对象类型,调用改方法需要传入一个实际的对象)等,这些对象同当前对象存在关联、依赖、聚合或组合关系,可以直接访问这些对象的方法。
3.5.2 案例
下面看一个例子来理解迪米特法则
【例】明星与经纪人的关系实例
明星由于全身心投入艺术,所以许多日常事务由经纪人负责处理,如和粉丝的见面会,和媒体公司的业务洽淡等。这里的经纪人是明星的朋友,而粉丝和媒体公司是明星的陌生人,降低了明星和粉丝以及明星和公司的耦合度,所以适合使用迪米特法则。
public class Fans {
private String name;
// 有参构造
public Fans(String name) {this.name = name;}
public String getName() {return name;}
}
=========================================================
public class Star {
private String name;
// 带参构造
public Star(String name) {this.name = name;}
public String getName() { return name;}
}
==========================================================
public class Company {
private String name;
// 有参构造
public Company(String name) {this.name = name;}
public String getName() {return name;}
}
===========================================================
// 经纪人类,相当于第三方
public class Agent {
//将粉丝、明星、公司聚合起来
private Star star;
private Fans fans;
private Company company;
public void setStar(Star star) {this.star = star; }
public void setFans(Fans fans) { this.fans = fans;}
public void setCompany(Company company) {
this.company = company;
}
public void meeting(){
System.out.println(fans.getName()+"与明星"
+star.getName()+"见面了");
}
public void business(){
System.out.println(company.getName()+"与明星"
+star.getName()+"洽谈业务");
}
}
===================================================
public class ClientTest {
public static void main(String[] args) {
// 创建经纪人类
Agent agent = new Agent();
// 创建明星类
Star star = new Star("詹姆斯");
agent.setStar(star);
// 创建粉丝类
Fans fans = new Fans("球迷");
agent.setFans(fans);
//创建公司类
Company company = new Company("李宁公司");
agent.setCompany(company);
//和粉丝见面
agent.meeting();
//和公司洽谈业务
agent.business();
}
}
3.6 合成复用原则(CRP)
Composite Reuse Principle,CRP
又叫:
组合/聚合复用原则(Composition/Aggregate Reuse Principle,CARP)
3.6.1 概述
合成复用原则是指:尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。
通常类的复用分为继承复用和合成复用两种。
继承复用虽然有简单和易实现的优点,但它也存在以下缺点:
-
继承复用破坏了类的封装性。
因为封装会将父类的实现细节暴露给子类,父类相对于子类是透明的,所以这种复用又称之为“白箱”复用。
-
子类与父类的耦合度高。
父类的实现的任何改变都会导致子类的实现发生变化,不利于类的扩展与维护。
-
限制了复用的灵活性。
从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。
采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点:
-
它维持了类的封装性。
因为成员对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
-
对象间的耦合度低。
可以在类的成员位置声明抽象(即 声明抽象父类或者抽象父接口,此时就可以传递该抽象父类的子类或者抽象父接口的实现类)。
-
复用的灵活性高。
这种复用可以在运行时动态进行,新对象可以动态地引用与成员对象类型相同的对象。
3.6.2 案列
汽车分类管理程序
汽车按“动力源”划分可分为汽油汽车、电动汽车等;按“颜色”划分可分为白色汽车、黑色汽车和红色汽车等。如果同时考虑这两种分类,其组合就很多。继承复用的类图如下:
从上面类图我们可以看到使用继承复用产生了很多子类,如果现在又有新的动力源或者新的颜色的话,就需要再定义新的类,就产生了很多类,如下图。
改进,试着将继承复用改为聚合复用,如下:
改为复用聚用后如果再有新的动力源或者新的颜色的话,直接创建一个类即可,如下图所示: