【软件构造】Java设计模式



创建模式:关注对象的创建过程


简单工厂模式

实现流程:

  1. 将需要创建的各种不同的产品对象封装到不同的类中,成为具体产品类
  2. 将具体产品类的公共代码进行提取封装到抽象产品类中,具体产品类继承抽象产品类
  3. 提供一个工厂类用于创建各种产品,在工厂类中提供一个静态工厂方法,根据传入的参数的不同创建不同的产品对象
  4. 客户只需要使用静态工厂方法,传入相应的参数获取不同的产品对象

类图:

代码实践:

需求:开发具有多种不同外观的图表库,通过设置不同的参数即可得到不同的图表,并且可以方便的对图表库进行扩展,增加新的图表

具体工厂:

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 products;
    private int c1;
    private int c2;

    public ProductIterator(ProductList products) {
        this.products = products.getObjects();
        this.c1 = 0;
        this.c2 = this.products.size()-1;
    }

    @Override
    public void next() {
        if(c1-1){
            c2--;
        }

    }

    @Override
    public boolean isFirst() {
        return (c2==-1);
    }

    @Override
    public Object getNextItem() {
        return products.get(c1);
    }

    @Override
    public Object getPreviousItem() {
        return products.get(c2);
    }

}

客户端:

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 products=new ArrayList<>();
        products.add("-1-");
        products.add("-2-");
        products.add("-3-");
        products.add("-4-");
        products.add("-5-");
        ProductList list=new ProductList(products);

        // 不可声明为ProductIterator:静态检测时,list.createIterator()返回值AbstractIterator
        // 用范围更大的类去接收范围更小的类
        AbstractIterator iterator=list.createIterator();

        System.out.println("正向遍历");
        while(!iterator.isLast()){
            System.out.print(iterator.getNextItem()+" ");
            iterator.next();
        }
        System.out.println();
        System.out.println("-------------------------------------------------------");
        System.out.println("反向遍历");
        while(!iterator.isFirst()){
            System.out.print(iterator.getPreviousItem()+" ");
            iterator.previous();
        }
    }
}

使用环境:

若需要增加一个新的具体聚合类,只需要增加一个新的聚合子类和一个相应的具体迭代器即可,原有代码无需修改。

若需要为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~??