【软件构造】Java设计模式
创建模式:关注对象的创建过程
简单工厂模式
实现流程:
- 将需要创建的各种不同的产品对象封装到不同的类中,成为具体产品类
- 将具体产品类的公共代码进行提取封装到抽象产品类中,具体产品类继承抽象产品类
- 提供一个工厂类用于创建各种产品,在工厂类中提供一个静态工厂方法,根据传入的参数的不同创建不同的产品对象
- 客户只需要使用静态工厂方法,传入相应的参数获取不同的产品对象
类图:
代码实践:
需求:开发具有多种不同外观的图表库,通过设置不同的参数即可得到不同的图表,并且可以方便的对图表库进行扩展,增加新的图表
具体工厂:
package PatternChart;
/*
@author HIT_Why 120L021418
*/
public class ChartFactory {
public static Chart getChart(String str){
Chart chart=null;
if(str.equalsIgnoreCase("line")){
chart=new LineChart();
System.out.println("初始化设置折线图!");
}
else if(str.equalsIgnoreCase("pie")){
chart=new PieChart();
System.out.println("初始化设置饼状图!");
}
return chart;
}
}
客户端调用:
package PatternChart;
/*
@author HIT_Why 120L021418
*/
public class Client {
public static void main(String[] args) {
Chart chart=ChartFactory.getChart("LiNE");
chart.display();
}
}
使用环境:
无需知道具体产品类的类名,只需知道具体产品对应的参数即可
一旦添加新的产品不得不修改静态工厂方法的逻辑
工厂方法模式
实现概述:
在简单工厂模式的基础上进行修改,将具体产品的创建过程交给具体的工厂子类去完成,所有工厂子类继承/实现同一个抽象工厂类。出现新的具体产品类型时,只需要添加一个新的具体工厂类,无需对原有代码进行修改。
即针对不同的产品,提供不同的工厂,使得产品等级结构与工厂等级结构一一对应。
工厂父类负责定义创建产品对象的公共接口,工厂子类负责具体产品对象的创建。
类图:
代码实践:
需求:设计一个具有多种日志记录方式的日志记录器,更好地封装记录器的初始化过程并保证多种记录方式的灵活性。
具体工厂:
package FactoryMethod;
/*
@author HIT_Why 120L021418
*/
public class DatabaseLoggerFactory implements LoggerFactory{
@Override
public Logger createLogger() {
System.out.println("连接数据库!");
Logger logger=new DatabaseLogger();
System.out.println("初始化数据库日志记录器!");
return logger;
}
}
客户端:
package FactoryMethod;
/*
@author HIT_Why 120L021418
*/
public class Client {
public static void main(String[] args) {
// 通过工厂创建产品,并非直接new一个所需的产品
Logger logger=new DatabaseLoggerFactory().createLogger();
// 使用产品
logger.writeLogger();
/*
如果需要增加一个新的产品,只需要创建一个新的具体产品类以及相对应的具体工厂,并分别实现write和create即可
*/
}
}
使用环境:
用户只关心所需产品对应的工厂,无需知道具体产品类的类名。
抽象工厂类只需要提供一个创建产品的接口,由子类来决定具体要创建的对象,在程序运行时,子类对象将覆盖父类对象,使得系统更容易扩展。
结构模式:将现有类/对象组织在一起形成强大的结构
适配器模式
实现概述:
适配器的作用在于将客户类的请求对适配者相应接口的调用。例如,需要完成任务A,对此设计了ADT adt,而adt中的功能已经在某个类或多个类中的到了代码实现(看成已有的源代码功能库),但由于接口不兼容,无法直接调用已经实现的代码,这时就需要一个适配器,将两个类的接口匹配起来,且无需修改原来的适配者接口(已有的代码库)和抽象目标类接口(新的ADT)。
其中,适配器Adaptor通过实现/继承Target(新的ADT,定义了客户所需接口),并关联(委托)一个Adaptee(已有代码库)对象,即在Adaptor代码中维持一个对适配者对象的引用并在实现Target接口时进行转发调用,使二者产生联系。
类图:
代码实践:
需求:开发一个具有灯光闪烁和声音提示功能的玩具车,使用以往的产品中已经实现了控制灯光闪烁和声音提示的代码(接口不兼容),并使汽车控制软件具有更好的灵活性与可扩展性。
Adaptor实现:
package Adaptor;
/*
@author HIT_Why 120L021418
*/
public class PoliceCarAdaptor implements CarInter {
// 维持两个对适配者对象的引用
private PoliceSound psound;
private PoliceLamp plamp;
// 要么set 要么constructor
public PoliceCarAdaptor() {
this.psound = new PoliceSound();
this.plamp = new PoliceLamp();
}
@Override
public void soundInter() {
System.out.print("装饰sound功能!----");
psound.alarmSound();
}
@Override
public void blingInter() {
System.out.print("装饰bling功能!----");
plamp.alarmLamp();
}
}
客户端:
package Adaptor;
/*
@author HIT_Why 120L021418
*/
public class Client {
public static void main(String[] args) {
CarInter car=new PoliceCarAdaptor();
car.move();
car.blingInter();
car.soundInter();
}
}
使用环境:
将现有接口转化为客户类所希望的接口,实现了对类的复用,无需修改原有结构(不想/不能修改原有code)
将具体业务实现过程封装到适配者类中,对于客户类而言是透明的,提高适配者的复用性
可以在适配器中对适配者类的修改进行装饰
不需要知道适配者类的具体代码
装饰模式
实现概述:
装饰模式通过一种无需定义子类的方式给对象动态的增加职责,使用对象之间的关联关系(委托)代替继承关系。
装饰模式包括抽象构件类,具体构件类,抽象装饰类,具体装饰类。
所谓委托代替继承,即需求为对某个具体构件类的对象进行功能扩充,使用委托来代替对该对象的继承。
如何使用委托呢?
定义一个抽象装饰类,该类代码中维护一个对抽象构件类对象的引用,在Set方法或构造方法注入一个抽象构件类的对象,同时定义方法op,在op中调用原有业务方法。
定义多个具体装饰类,继承上述抽象装饰类,每个具体装饰类可以完成多个装饰功能,对op进行重写,在op代码内部实现装饰。
客户调用时,使用AbstractDecorator a=new ConcreteDecratorA(ConcreteComponentA);根据Liskov替换原则,ConcreteDecratorA可以实现对ConcreteComponentA类的装饰(继承/实现共同父类AbstractComponent)。
个人理解:
本质上为使用横向委托代替纵向继承,对需要被功能扩充的类C进行横向扩展,添加AbstractDecorator、ConcreteDecrator,通过层级嵌套进行功能扩充,代替对C的纵向继承,避免类之间的关系过深,同时,使得不同的装饰代码可以很好的复用。
需求:想要扩充某个具体构件类的功能,但又不想去继承该类。
首先设计一个抽象装饰类实现抽象构件类,并设计多种具体装饰类继承抽象装饰类
抽象装饰类中维护一个对抽象构件类对象的引用,通过client传参,在具体装饰类中使用已经实现的功能(待扩充)
在具体装饰类中添加多种装饰功能
客户端传入想用使用并扩充的某具体构件类,层层嵌套具体装饰类。
UML图:
代码实践:
需求:某图形界面构件库包含了大量的基本构件,由于用户经常要求定制一些具有特殊效果的构件,要求对该基本构件库进行扩展以增强其功能。
具体装饰类实现
package Decorator;
/*
@author HIT_Why 120L021418
*/
public class BoarderDecorator extends ComponentDecorator{
public BoarderDecorator(Component component) {
super(component);
}
public void display(){
// 调用本类中的装饰功能1:增加滚动条
this.setBoarder();
// 调用父类中的display()方法,本质上某具体构件类中待装饰的方法 调用父类中的公共方法
super.display();
}
public void setBoarder(){
System.out.println("增加黑色边框!");
}
}
客户端
package Decorator;
/*
@author HIT_Why 120L021418
*/
public class Client {
public static void main(String[] args) {
Component component=new Window();
System.out.println("装饰前:");
component.display();
System.out.println();
// 对列表框(具体构件类)进行装饰,首先添加滚动条,再添加边框
Component cp=new BoarderDecorator(new BarDecorator(component));
System.out.println("装饰后:");
cp.display();
}
}
使用环境:
引入抽象构件类,使得客户端以一致的方式处理未被装饰的对象以及装饰之后的对象,实现客户端的透明操作。
动态增删对象的职责,大大减少子类的个数(装饰功能的复用)
根据需要增加具体构件类和具体装饰类,原有类库代码无需改变
不能采取继承:系统中存在大量独立扩展;具体构件类被final修饰
行为模式:研究系统在运行时对象之间的交互与责任分配
策略模式
实现算法的自由切换和扩展,以及算法定义与算法使用的分离。
策略模式包括环境类,抽象算法类,具体算法类三部分。
环境类是算法的使用者,其内部维护一个对抽象算法类对象的引用实例,并设置set与algorithm方法,set用于动态的改变算法的具体实现类,在algorithm中调用具体算法类对象的方法,一个系统可以有多个环境类。
抽象算法类是一个接口类,用于声明抽象算法,保证策略在使用时的一致性。
具体算法类封装了一个具体算法,是算法的实现者。
UML图:
代码实践:
需求:某售票系统为不同的用户提供不同的打折方案,并且可能引入新的打折方式,设计该影院的打折方案。
MovieTicket作为环境类(算法的使用者),不同打折方案作为具体策略类。
环境类:
package Strategy;
/*
@author HIT_Why 120L021418
*/
public class MovieTicket {
private double price;
// 维持一个对抽象算法类对象的引用,由客户端通过set方法动态决定使用何种算法 (-------------------------关键-------------------------)
private Discount discount;
// 由客户端通过set方法动态决定使用何种算法
public void setPrice(double price){
this.price=price;
}
public void setDiscount(Discount discount){
this.discount=discount;
}
// 将计算票价的具体算法实现委托给具体算法类对象
public double getPrice(){
// 调用具体算法
return this.discount.calculate(this.price);
}
}
客户端
package Strategy;
/*
@author HIT_Why 120L021418
*/
public class Client {
public static void main(String[] args) {
MovieTicket ticket=new MovieTicket();
double srcPrice=60.0;
ticket.setPrice(srcPrice);
System.out.println("原始票价:"+srcPrice);
System.out.println();
double nowPrice;
ticket.setDiscount(new StudentDiscount());
nowPrice=ticket.getPrice();
System.out.println("折后价格:"+nowPrice);
System.out.println();
ticket.setDiscount(new ChildrenDiscount());
nowPrice=ticket.getPrice();
System.out.println("折后价格:"+nowPrice);
System.out.println();
ticket.setDiscount(new VIPDiscount());
nowPrice=ticket.getPrice();
System.out.println("折后价格:"+nowPrice);
System.out.println();
}
}
使用环境:
涉及到算法的封装、复用和切换都可以考虑使用策略模式
模板方法模式
模板方法模式包含抽象类与具体子类两个类,只存在父类与子类的继承关系,父类(模板方法)定义次序,子类选择性覆盖某些步骤(某些步骤不固定,存在可变性)。本质上是定义了一个算法框架,将算法的一些步骤延迟到子类中实现。
为了保证模板方法在子类中不被修改,需将模板方法定义为final方法。故模板方法的抽象层只能是抽象类,不能是接口。
理论依据:子类对象在运行时将覆盖父类对象,子类中定义的方法也将覆盖父类中定义的方法。因此子类的钩子方法可以覆盖父类的钩子方法,实现子类对父类行为的反向控制。
UML图:
代码实践:
需求:设计某利息计算模块,共包括A、B、C 三个流程(B:根据用户类型的不同使用不同的计算公式计算利息)。
抽象模板类
package Template;
/*
@author HIT_Why 120L021418
*/
public abstract class Account {
public boolean validate(String account,String password){
System.out.println("账号:"+account);
System.out.println("密码:"+password);
return account.equalsIgnoreCase("哈工大") && password.equalsIgnoreCase("1920-2022");
}
public abstract void calculate();
public void display(){
System.out.println("显示利息!");
}
public final void handle(String account,String password){
if(!this.validate(account,password)){
System.out.println("账号或密码错误!");
return;
}
calculate();
display();
}
}
客户端
package Template;
/*
@author HIT_Why 120L021418
*/
public class Client {
public static void main(String[] args) {
// 编译时父类寻找确定,运行时子类覆盖执行
Account ac=new CurrentAccount();
ac.handle("哈工大","1920-2022");
System.out.println();
ac.handle("HIT","2022");
System.out.println();
Account alwaysRight=new SavingAccount();
alwaysRight.handle("HIT","2022");
}
}
使用环境:
继承+子类反向控制
固定不变的部分设计为模板方法和父类具体方法
迭代器模式
在不暴露对象内部表示的情况下,采用不同方式遍历聚合对象。
本质上为工厂模式:
抽象迭代器类作为抽象产品类,声明产品功能接口,包括first(),hasNext(),next(),currentItem()等。
具体迭代器作为具体产品类,实现上述接口,并设置游标记录在聚合对象中所处的当前位置。
抽象聚合类作为抽象工厂类,声明产品创建接口createIterator()。
具体聚合类作为具体工厂类,实现上述接口,返回与之相对应的具体产品类的对象(一一对应)。
与上述工厂方法模式不同的是,具体产品类,也就时具体迭代器类中,维护了一个对具体聚合类对象(一一对应,若非此,可由传参实现一对多)的引用,以便访问存储在该具体聚合类对象中的数据。
UML图:
代码实践:
某商场数据管理系统维护了两大数据:客户数据,商品数据,要求将遍历数据的方法封装到专门的类中,实现数据存储与数据遍历的分离,并给不同的具体数据集合类分配不同的遍历方式。
迭代器实现:
package Iterator;
import java.util.List;
/*
@author HIT_Why 120L021418
*/
public class ProductIterator implements AbstractIterator{
// 维护一个对抽象聚合类对象的引用 与普通工厂方法模式的区别
private List
客户端:
package Iterator;
import java.util.ArrayList;
import java.util.List;
/*
@author HIT_Why 120L021418
*/
public class Client {
public static void main(String[] args) {
// 注意:Object不可修改为String : 泛型类不满足Liskov替换原则
List
使用环境:
若需要增加一个新的具体聚合类,只需要增加一个新的聚合子类和一个相应的具体迭代器即可,原有代码无需修改。
若需要为ProductList类更换一个迭代器,只需要增加一个新的具体迭代器,重新实现所需的遍历方法,原有代码无需修改。
支持以不同的方式遍历一个聚合对象,并简化了聚合类。
访问者模式
某集合对象中存储了多种不同类型的对象信息,不同的访问者可以访问多个或同一个对象结构,且对同一个对象结构的操作方式不唯一,并可能增加新的处理方式。访问者模式是一个对象行为模式,为操作存储不同类型的对象结构提供了一种解决方案,用户可以对不同类型的元素施加不同的操作。
访问者模式包含五个角色:
对象结构:具体元素的集合,提供遍历方法,可采用简单集合对象,如Collections等
抽象访问者:为每个具体元素声明一个访问操作接口,通过参数类型/操作名称确定访问的具体元素类型,即每个操作用于访问对象结构中一种类型的元素。
具体访问者:实现上述接口。
抽象元素:以一个抽象访问者作为参数,声明accept接口用于接收访问者的访问,使得只需要一个统一的accept接口,在运行时决定具体访问者的类型。
具体元素:实现accept方法,在该方法中调用访问者的对该元素的访问方法(tips:委托的两种方式,一种是在类内部维护一个A类型的对象,一种是将A类型的对象作为参数传入,此外在使用A类型对象的方法时,可以将本类型的对象作为参数传入,实现一一对应,这种机制也称为双重分派),这种机制使得新增加的访问者无需修改现有代码,只需要将新访问者对象作为参数传入具体元素对象的accept方法。
本质:是一种多对多的关系,相同的访问者可以用不同的方式访问不同的元素,相同的元素可以接受不同访问者以不同方式进行访问。
UML图:
代码实践:
某公司包含正式工和临时工,每周人力资源部和财务部需要对员工数据进行汇总,包括员工工作时间,员工工资等,设计一个员工信息管理子系统完成上述任务。
客户端:
package Vistor;
/*
@author HIT_Why 120L021418
@create 2022-06-09 22:58
*/
public class Client {
public static void main(String[] args) {
EmployeeList list=new EmployeeList();
Employee f1=new FulltimeEmployee("正式1",3200.00,45);
Employee p1=new ParttimeEmployee("临时1",80.00,20);
Employee f2=new FulltimeEmployee("正式2",2000.00,40);
Employee p2=new ParttimeEmployee("临时2",60.00,18);
list.addEmployee(f1);
list.addEmployee(p1);
list.addEmployee(f2);
list.addEmployee(p2);
Department dep1=new FADepartment();
Department dep2=new HRDepartment();
System.out.println();
list.accept(dep1);
System.out.println();
list.accept(dep2);
}
}
使用环境:
访问者模式中增加新的访问操作容易,但想要增加新的元素类很困难
一个对象结构包含多个多个类型的对象(正式工,临时工),希望对这些对象实施一些依赖其具体类型的操作:每个类型(正式工,临时工)都有一个访问操作,同一类型对象由不同访问操作(计算工资,计算工时)
详细代码见github:https://github.com/120L021418/JavaDesignPatterns.git
Ending~??