目录
前言
设计模式和原则作用
面向对象的设计原则
开闭原则OCP: Open-Closed Principle
题目举例
里氏代换原则LSP: Liskov Subtitution
依赖倒置原则DIP: Dependency Inversion Principle
接口隔离原则ISP: Interface Segregation Principle
组合优先原则CRP: Composition Reuse Principle
迪米特法则 LOD
单一职责原则SRP
面向对象的设计模式
单例模式
工厂模式
简单工厂
工厂方法
抽象工厂
总结
前言
写在前面:本文是主要针对山东大学软件学院面向对象技术课程的设计模式和设计原则部分的总结,包括老师课上讲解的主要设计模式,类图和往年回忆考题中的设计类题目和自己写的代码(都是正确可运行的)
设计模式和原则作用
目标:提高面向对象设计复用性的设计原则。满足可拓展性,灵活性和可插入性
复用:尽量从具体模块中,抽象出抽象模块;抽象模块独立于具体模块设计
面向对象的设计原则
- 开闭原则OCP: Open-Closed Principle
- 里氏代换原则LSP: Liskov Subtitution
- 依赖倒置原则DIP: Dependency Inversion Principle
- 接口隔离原则ISP: Interface Segregation Principle
- 组合复用原则CRP: Composition Reuse Principle
- 迪米特法则LoD: Law of Demeter
- 单一职责原则SRP
开闭原则OCP: Open-Closed Principle
定义:软件实体对拓展是开发的,但是对修改是关闭的。试图设计永远不需要改变的模块(关键在于抽象层次结构的设计,抽象模块是不需要改变的,只要不停在抽象模块下面增加具体模块即可)
目标:尝试在软件系统中减少不满足OCP原则的模块数量(尽量增加抽象层次,让具体层次和抽象层次绑定)。同时做到解耦(解耦指的是减少模块或类之间的依赖,使得一个模块的变更不会影响到其他模块。通过解耦,可以实现更高的模块化和灵活性,使得系统更容易扩展和维护)
题目举例
题目一:
- 给定某几种商品,要求商品可以拓展,商品要求可以设置价格,并且可能设置商品折扣
class Part{protected double price;public void setPrice(double price){this.price=price;}public double getPrice(){return price;}
}
class Memory extends Part{}
class Disk extends Part{}
public class Test {public static void main(String[] args){Part p1=new Memory();p1.setPrice(599);Part p2=new Disk();p1.setPrice(499);Part[] part={p1,p2};System.out.println(sumPrice(part));}public static double sumPrice(Part[] parts){double total = 0.0;for (int i=0;i<parts.length;i++){total += parts[i].getPrice();}return total;}
}
分析代码:
如果此时要给商品促销,那么我们就需要重新修改函数sumPrice,这就不符合OCP原则。因此,要提取抽象层次,抽象出一个类pricePolicy表示促销策略(让具体促销价格计算放在每个商品内部解决)。然后在sumPrice中对所有商品调用同样的getPrice即可
修改代码如下:
class Part{protected double price;private pricePolicy pricePolicy;public void setPrice(double price){this.price=price;}public double getPrice(){if(pricePolicy == null)return price;elsereturn pricePolicy.getPrice(price);}
}
class pricePolicy{public double getPrice(double basePrice){return basePrice;}
}
class sale extends pricePolicy{private double discount;public void setDiscount(double discount){this.discount = discount;}public double getPrice(double basePrice){return basePrice * discount;}
}
class Memory extends Part{}
class Disk extends Part{}
public class Test {public static void main(String[] args){Part p1=new Memory();p1.setPrice(599);Part p2=new Disk();p1.setPrice(499);Part[] part={p1,p2};System.out.println(sumPrice(part));}public static double sumPrice(Part[] parts){double total = 0.0;for (int i=0;i<parts.length;i++){total += parts[i].getPrice();}return total;}
}
抽象出了pricePolicy类,然后在每个商品类中都加入pricePolicy属性这样每个商品都有独立的价格政策。每个商品具体的价格政策sale又是独立的类继承自pricePolicy,如此,新增价格政策时不是修改pricePolicy而是新增一个具体类,放到这个抽象类中,抽象类用来返回具体类中计算的最终价格
方法:
不符合OCP原则,则想办法抽象出新的层次,在抽象层次去调用,从而提高复用率
题目二:
- 绘制图形,并提供展示方法,要求每增加一个新的图形,都要尽量满足OCP原则
class Shape{}
class Circle extends Shape {void drawCircle(){System.out.println("I am drawing a circle in switch!"); }
}
class Square extends Shape{void drawSquare(){System.out.println("I am drawing a square in switch!"); }
}import java.util.ArrayList;
import java.util.List;
public class Test{ public static void main(String[] args){ List<Shape> shapeList=new ArrayList<Shape>(); Circle c=new Circle(); Square s=new Square(); shapeList.add(c); shapeList.add(s); switchDraw(shapeList); } static void switchDraw(List shapeList){ for(int i=0;i<shapeList.size();i++){ if(shapeList.get(i) instanceof Circle){ Circle circle= (Circle)shapeList.get(i); circle.drawCircle(); }else if(shapeList.get(i) instanceof Square){ Square square= (Square)shapeList.get(i); square.drawSquare(); } } }
}
// 这种实现如果在图形形状多的情况下循环分支数量会很大,不推荐
分析代码:
如果现在要增加一个图形形状,那么我们需要修改switchDraw画图函数,一旦图形形状增多后,这个分支将非常长,并且修改函数不符合OCP原则。所以要提取抽象层次,把画图提取为draw函数,从而使得switchDraw函数转化为draw函数。
因此具体如何draw就不由main决定而是由每一个类自己内部决定。将每个图形继承自shape类,shape类要有一个draw抽象函数,这个抽象函数由每个具体类去具体实现。
修改代码如下:
import java.util.ArrayList;
abstract class shape {abstract void draw();
}
class circle extends shape{void draw(){System.out.println("a circle is drawing");}
}
class square extends shape{void draw(){System.out.println("a square is drawing");}
}
public class Test{public static void main(String[] args) {ArrayList<shape> arrayList = new ArrayList<shape>();shape circle = new circle();shape square = new square();arrayList.add(circle);arrayList.add(square);for(int i=0;i<arrayList.size();i++){arrayList.get(i).draw();}}
}
里氏代换原则LSP: Liskov Subtitution
定义:如果对每一个类型为的对象,都有类型为的对象,使得定义的所有程序P在所有的对象都换成时,程序P的行为没有变化,那么类型是类型的子类型 (可替换原则)
简单来说:父类适用的地方替换成子类一样适用
里氏替换原则是实现开闭原则的重要方法之一。如果一个程序不符合LSP,那么程序用子类替换父类,我们就需要修改程序
实现LSP的方法:
在程序中尽量用基类类型定义对象,在具体运行实现的时候再用子类类型,并用子类类型去替换父类对象
在语法上,在父类类型中使用子类对象肯定是没有错误的,那么为什么有的程序不满足LSP原则呢?既然不是语法错误,那么一定是程序逻辑上的问题,也就是说有的子类在逻辑上就不能代替父类使用(逻辑上并不继承)
例如:鸟一般都会飞行,如燕子的飞行速度大概是每小时 120 千米。但是新西兰的几维鸟由于翅膀退化无法飞行。假如要设计一个实例,计算这两种鸟飞行 300 千米要花费的时间。显然,拿燕子来测试这段代码,结果正确,能计算出所需要的时间;但拿几维鸟来测试,结果会发生“除零异常”或是“无穷大”,明显不符合预期
生活中存在一类事物在认知上是存在继承关系,但是在行为上并不存在继承关系。这一类事物用继承来实现便不符合LSP(行为上不存在继承关系——>子类父类行为上不可复用——>子类父类函数不可复用——>复用时函数需要修改——>不符合OCP和LSP)
例如:
class Bird { double flySpeed; public void setSpeed(double speed) { flySpeed = speed; } public double getFlyTime(double distance) { return (distance / flySpeed); }
}
//几维鸟类
class BrownKiwi extends Bird { public void setSpeed(double speed) { flySpeed = 0; }
}
public class Test { public static void main(String[] args) { Bird bird1 = new Swallow(); Bird bird2 = new BrownKiwi(); bird1.setSpeed(120); bird2.setSpeed(120); System.out.println("如果飞行300公里:"); try { System.out.println("燕子将飞行" + bird1.getFlyTime(300) + "小时.");System.out.println("几维鸟将飞行" + bird2.getFlyTime(300) + "小时。"); } catch (Exception err) { System.out.println("发生错误了!"); } }
}
// 这显然是不满足里氏代换原则的
遵守的方法:
一旦两个事物在行为上不严格存在继承关系,那么便定义为两个类,不使用继承关系
依赖倒置原则DIP: Dependency Inversion Principle
定义:
高层模块不应该依赖底层模块,二者都应该依赖其抽象;抽象不应该依赖细节;而细节应该依赖抽象(所有细节模块都依赖抽象模块,类之间的联系全靠抽象模块)
必要性:
假如现在有一个司机类又有一个汽车类,司机要开汽车那么汽车类和司机类一定要产生依赖关系。假如我们将一个具体的司机和一个具体的车联系起来,如果我们增加了一辆新的车便要重新添加联系,这将造成麻烦,并且模块之间的耦合性太强
实现核心:
利用抽象类/接口来提取抽象层次,将模块之间的联系转化为抽象层次的联系
两种实现方式:
a.接口声明实现依赖对象
public interface ICar{public void run();
}
public interface IDriver {public void drive(ICar car);
}
public class Benz implements ICar {public void run(){System.out.println("奔驰汽车开始运行...");}
}
public class Driver implements IDriver {public void drive(ICar car){car.run();}
}
public class Test {public static void main(String[] args){IDriver driver1=new Driver();ICar benz=new Benz();driver1.drive(benz);}
}
b. 构造函数注入实现依赖对象
public interface ICar{public void run();
}
public interface IDriver {public void drive();
}
public class Benz implements ICar {public void run(){System.out.println("奔驰汽车开始运行...");}
}
public class Driver implements IDriver {private ICar car;public Driver(ICar benz) {this.car=car;}public void drive(){this.car.run();}
}
public class Test {public static void main(String[] args){ICar benz=new Benz();IDriver driver1=new Driver(benz);driver1.drive();}
}
c.Setter方法传递依赖对象:
public interface ICar{public void run();
}
public interface IDriver {public void drive();
}
public class Benz implements ICar {public void run(){System.out.println("奔驰汽车开始运行...");}
}
public class Driver implements IDriver {private ICar car;public void setCar(ICar benz) {this.car=car;}public void drive(){this.car.run();}
}
public class Test {public static void main(String[] args){ICar benz=new Benz();IDriver driver1=new Driver();driver1.setCar(benz);driver1.drive();}
}
本人最喜欢抽象接口实现依赖 ,使用这个更符合上图中的类关系图,真正的依赖绑定是在接口声明中进行
理解关键点:
1、“正置”:依赖正置就是类间的依赖是实实在在地实现类间的依赖,也就是面向实现编程,这也是正常人的思维方式,我要开奔驰车就依赖奔驰车,我要使用笔记本电脑就直接依赖笔记本电脑
2、而编写程序需要的是对现实世界的事物进行抽象,抽象的结果就是有了抽象类和接口,然后我们根据系统设计的需要产生了抽象间的依赖,代替了人们传统思维中的事物间的依赖,“倒置”就是从这里产生的。
接口隔离原则ISP: Interface Segregation Principle
定义:使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口
目标:接口功能明确;一个接口表示一个角色;接口尽量少且专一
接口分工明确;接口职能明确;接口功能交集少
组合优先原则CRP: Composition Reuse Principle
思想:将代码易变化的部分封装起来,将其和代码不变的部分独立开来(将一个物品中多变化的部分独立变为一个模块,利用组合复用来搭建原本的物品);这个分开要求二者本身是可独立的模块
目标:实现可变性和不可变性的分离,将可变因素映射为同一个抽象类的不同子类。(将可独立模块分隔开,利用组合来复用而不是继承来实现)
示例:蜡笔和毛笔的区别。蜡笔的型号和颜色在出厂时就已经确定,二者不是可独立模块,但是毛笔和型号和颜色是可分离和可变化的,故可以采用桥梁模式。
同时在毛笔中按照组合复用原则,封装可变部分,利用桥梁模型进行设计,可以避免继承带来的缺点(破坏封装、子父类耦合、继承是静态的不能改变)又保留继承的优点(子类能够拓展父类的实现,而不用修改父类,符合开闭原则)
核心部分:在于独立的两个模块(Abstraction和Implementor依靠关联和聚合达成继承的效果,共同组成一个物体)
迪米特法则 LOD
定义:
一个对象应该对其他对象保持最少的了解。
如果两个类不必彼此通信,那么这两个类就不应当发生直接的相互作用。如果其中的一个类需要调用另一个类的某一个方法的话,可通过第三者实现调用(朋友圈)
原则出现的原因:
类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。因此想要尽量减少各个类之间的关系
单一职责原则SRP
定义:一个类只负责一项职责。此原则的核心就是解耦和增强内聚性
原则出现的原因:
类T负责两个不同的职责:职责P1,职责P2。当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障。因此,遵循单一职责原则。分别建立两个类T1、T2,使T1完成职责P1功能,T2完成职责P2功能。这样,当修改类T1时,不会使职责P2发生故障风险;同理,当修改T2时,也不会使职责P1发生故障风险。
举例:
假如存在一个类描述动物的呼吸:
class Animal{public void breathe(String animal){System.out.println(animal+"呼吸空气");}
}
public class Client{public static void main(String[] args){Animal animal = new Animal();animal.breathe("牛");animal.breathe("羊");animal.breathe("猪");}
}
此时要增加一个呼吸水的功能,按照单一职责原则修改如下:
class Terrestrial{public void breathe(String animal){System.out.println(animal+"呼吸空气");}
}
class Aquatic{public void breathe(String animal){System.out.println(animal+"呼吸水");}
}public class Client{public static void main(String[] args){Terrestrial terrestrial = new Terrestrial();terrestrial.breathe("牛");terrestrial.breathe("羊");terrestrial.breathe("猪");Aquatic aquatic = new Aquatic();aquatic.breathe("鱼");
}}
如果不遵循单一职责原则,代码如下:
class Animal{public void breathe(String animal){if("鱼".equals(animal)){System.out.println(animal+"呼吸水");}else{System.out.println(animal+"呼吸空气");}}
}public class Client{public static void main(String[] args){Animal animal = new Animal();animal.breathe("牛");animal.breathe("羊");animal.breathe("猪");animal.breathe("鱼");}
}
可以看到,这种修改方式要简单的多。但是却存在着隐患:有一天需要将鱼分为呼吸淡水的鱼和呼吸海水的鱼,则又需要修改Animal类的breathe方法,而对原有代码的修改会对调用“猪”“牛”“羊”等相关功能带来风险,也许某一天你会发现程序运行的结果变为“牛呼吸水”了。这种修改方式直接在代码级别上违背了单一职责原则,虽然修改起来最简单,但隐患却是最大的。
面向对象的设计模式
设计模式的分类
- 创建型:工厂模式、单例模式
- 结构型:桥梁模式、适配器模式、装饰者模式
- 行为型:观察者、责任链、策略模式
单例模式
定义:一个类只有一个实例,且该类能够自行创建这个实例的(不用手动new)
特点:
- 单例类只有一个实例对象
- 该单例对象必须由单例类自行创建
- 单例类对外提供一个访问该单例对象的全局访问点
优点:
- 单例模式可以保证每个单例类内存中只有一个实例,减少内存开销。避免资源重复浪费
- 单例对象设置全局访问点,优化对其的访问
缺点:
- 单例模式没有接口,拓展困难
- 单例模式的功能只能写在一个类中,如果设计的不合理,将违背单一职责原则(要求一个类/方法/接口只实现一个功能)
类图:
单例模式根据实例化对象时机的不同分为两种:一种是饿汉式单例,一种是懒汉式单例。饿汉式单例在单例类被加载时候,就实例化一个对象交给自己的引用;而懒汉式在调用取得实例方法的时候才会实例化对象。
代码如下:
饿汉式单例
public class Singleton {private Singleton(){}//private保证外界不能获取,控制实例唯一性private static Singleton singleton=new Singleton();//static保证延迟实例化(在需要时才创建)public static Singleton getInstance(){return singleton;}//全局可访问性public static void main(String[] args) {for(int i = 0; i <2; i++){Singleton obj = Singleton.getInstance();System.out.println(obj); //获得对象,打印}}
}
懒汉式单例
public class Singleton {private Singleton(){}//private保证外界不能获取,控制实例唯一性private static Singleton singleton;//懒汉式并不直接创建实例对象,在调用getInstance才生成public static Singleton getInstance(){if(singleton==null){singleton = new Singleton();}return singleton;}//全局可访问性public static void main(String[] args) {for(int i = 0; i <2; i++){Singleton obj = Singleton.getInstance();System.out.println(obj); //获得对象,打印}}
}
处理多线程
public class Singleton {private static Singleton uniqueInstance;// other useful instance variables hereprivate Singleton() {}public static synchronized Singleton getInstance() {if (uniqueInstance == null) {uniqueInstance = new Singleton();}return uniqueInstance;}// other useful methods here
}
工厂模式
- 简单工厂:一个工厂完成所有产品的生产
- 工厂方法:将不同产品分给不同工厂去生产(分工)
- 抽象工厂:对分工后的工厂提取一个抽象层次
简单工厂
定义:产品实现接口;工厂生产产品
类图:
角色对象:
工厂类(Creator)角色:该角色是工厂方法模式的核心,含有与应用紧密相关的商业逻辑。工厂类在客户端的直接调用下创建产品对象,它往往由一个具体类实现。
抽象产品(Product)角色:担任这个角色的类是工厂方法模式所创建的对象的父类,或它们共同拥有的接口。抽象产品角色可以用接口或者抽象类实现。
具体产品(Concrete Product)角色:工厂方法模式所创建的任何对象都是这个角色的实例,具体产品角色由一个具体类实现。
代码如下:
抽象产品(水果接口)
public interface FruitIF {
void grow();
void harvest();
void plant();
}
具体产品(具体水果)
public class Apple implements FruitIF {private int treeAge;@Overridepublic void grow() {System.out.println("Apple is growing...");}@Overridepublic void harvest() {System.out.println("Apple has been harvested.");}@Overridepublic void plant() {System.out.println("Apple has been planted.");}public int getTreeAge() {return treeAge;}public void setTreeAge(int treeAge) {this.treeAge = treeAge;}
}
public class Strawberry implements FruitIF {@Overridepublic void grow() {System.out.println("Strawberry is growing...");}@Overridepublic void harvest() {System.out.println("Strawberry has been harvested.");}@Overridepublic void plant() {System.out.println("Strawberry has been planted.");}
}
public class Grape implements FruitIF {@Overridepublic void grow() {System.out.println("Grape is growing...");}@Overridepublic void harvest() {System.out.println("Grape has been harvested.");}@Overridepublic void plant() {System.out.println("Grape has been planted.");}
}
工厂(水果代理商)
public class FruitGardener {public FruitIF factory(String which) throws BadFruitException {if (which.equalsIgnoreCase("apple")) {return new Apple();} else if (which.equalsIgnoreCase("strawberry")) {return new Strawberry();} else if (which.equalsIgnoreCase("grape")) {return new Grape();} else {throw new BadFruitException("Bad fruit request");}}public static void main(String[] args) {FruitGardener gardener = new FruitGardener();try {FruitIF apple = gardener.factory("apple");apple.grow();apple.harvest();apple.plant();FruitIF strawberry = gardener.factory("strawberry");strawberry.grow();strawberry.harvest();strawberry.plant();FruitIF grape = gardener.factory("grape");grape.grow();grape.harvest();grape.plant();} catch (BadFruitException e) {System.out.println(e.getMessage());}}
}
优缺点:
工厂方法
定义:在工厂方法模式中,我们不再提供一个统一的工厂类来创建所有的产品对象,而是针对不同的产品提供不同的工厂,系统提供一个与产品等级结构对应的工厂等级结构
本质就是分解+抽象层次抽取:为不同的产品分配上不同的工厂,并为这些工厂抽取抽象工厂
角色:
定义工厂方法所创建的对象的接口,也就是实际需要使用的对象的接口。
ConcreteProduct :
具体的 Product 接口的实现对象。
Facotry :
创建器,声明工厂方法,工厂方法通常会返回一个 Product 类型的实例对象,而且多是抽象方法。也可以在 Factory 里面提供工厂方法的默认实现,让工厂方法返回一个缺省的 Product 类型的实例对象。
ConcreteFacotry :
具体的创建器对象,(覆盖)实现 AbstractFacotry 定义的工厂方法,返回具体的 Product 实例。
类图:
实现:
抽象产品(Logger)
//日志记录器接口:
//抽象产品 interface Logger { public void writeLog();
} //数据库日志记录器:具体产品
具体产品
class DatabaseLogger implements Logger { public void writeLog() { System.out.println("数据库日志记录"); } } //文件日志记录器:具体产品 class FileLogger implements Logger {public void writeLog() { System.out.println("文件日志记录。"); } }
抽象工厂
interface LoggerFactory { public Logger createLogger(); } //数据库日志记录器工厂类:具体工厂
具体工厂
class DatabaseLoggerFactory implements LoggerFactory { public Logger createLogger() { //连接数据库,代码省略 ;创建数据库日志记录器对象 Logger logger = new DatabaseLogger(); //初始化数据库日志记录器代码省略 return logger; } } //文件日志记录器工厂类:具体工厂
class FileLoggerFactory implements LoggerFactory { public Logger createLogger() { //创建文件日志记录器对象 Logger logger = new FileLogger(); //创建文件,代码省略 return logger; } }
客户端
class Client { public static void main(String args[]) { LoggerFactory factory; Logger logger;
//可引入配置文件实现 factory = new FileLoggerFactory(); logger = factory.createLogger(); logger.writeLog(); }
}
优缺点:
关键点:1、保证工厂方法内对修改关闭;2、如果要更换另一种产品,仍然需要修改实例化的具体工厂类;3、使得客户端和具体实现端分离,实现面向接口编程
例子:
抽象工厂
定义:
选择产品簇的实现。
联系:
类图:
代码:
抽象部分(抽象工厂、抽象产品)
/** * 抽象工厂的接口,声明创建抽象产品对象的操作 */
public interface Factory { /** * 示例方法,创建抽象产品A的对象 @return 抽象产品A的对象 */ public ProductA createProductA(); /** * 示例方法,创建抽象产品B的对象 @return 抽象产品B的对象 */ public ProductB createProductB();
}
/** * 抽象产品A的接口 */
public interface ProductA { //定义抽象产品A相关的操作
}
/** * 抽象产品B的接口 */
public interface ProductB { //定义抽象产品B相关的操作
}
具体部分(具体产品+具体工厂)
/**
/ * 产品A的具体实现 */
public class ProductA1 implements ProductA { //实现产品A的接口中定义的操作
}
public class ProductA2 implements ProductA { //实现产品A的接口中定义的操作
}
//ProductB系列是类似的代码
/** * 具体的工厂实现对象,实现创建具体的产品对象的操作 */
public class Factory1 implements Factory { public ProductA createProductA() { return new ProductA1(); } public ProductB createProductB() { return new ProductB1(); }
}
/** * 具体的工厂实现对象,实现创建具体的产品对象的操作 */
public class Factory2 implements Factory { public ProductA createProductA() { return new ProductA2(); } public ProductB createProductB() { return new ProductB2(); }
}
客户端
public class Client { public static void main(String[] args) { //创建抽象工厂对象 Factory af = new Factory1(); //通过抽象工厂来获取一系列的对象,如产品A和产品B af.createProductA(); af.createProductB(); }
}
总结
简单工厂:一个工厂生产所有的商品。耦合性太强,不符合开闭等原则。但是实现客户端和具体商品创建的解耦,也一定程度上实现了具体实现的封装
工厂方法:对工厂按照生产不同商品进行分离,不同工厂生产不同的商品。进一步对程序进行解耦,更易达成开闭原则等(假如总共只有一种商品,那么工厂方法就退化为简单工厂)
抽象工厂:存在一个超级大厂生产产品簇,能够生产具体的工厂,不同工厂协作生产出一个商品(本质:一个工厂能够生产多个产品)(假如产品簇只有一个产品,那么一个工厂就能生产一个产品,因此抽象工厂退化为工厂方法)
总结
如果觉得写的还不错,可以点个赞收藏一下呀~~
祝大家学业、事业、爱情顺利!
天天开心,没有Bug每一天