設計模式——行為型設計模式

行為型設計模式

針對對象之間的交互

解釋器模式

java中用的很。JVM編譯的時候就是對我們寫的代碼進行了解釋操作;數據庫SQL語句亦是如此

解釋器:對語言進行解釋,根據不同語義來做不同的事情。

舉例:雙棧計算器

public class 雙棧實現計算器 {
    //設置兩棧
    private static Deque<Character> opr = new LinkedList<>();
    private static Deque<Double> number = new LinkedList<>();

    public static void main(String[] args) {
        //接收一串字符串並轉字符數組
        Scanner scanner = new Scanner(System.in);
        String str = scanner.nextLine();
        char [] arrC = str.toCharArray();

        for (int i = 0; i < arrC.length;) {
            char c = arrC[i];
            //是+-/*字符時
            if (isChar(c)){
                ///如果棧內有字符,則需要判斷優先級,入棧字符小於等於棧內字符則需要先計算棧內字符
                Character peek = opr.peek();    //細節!!! 這裡必須先取
                while (peek!=null && isLowIn(c,peek)){
                    cal();
                    peek=opr.peek();       //細節!!這裡也必須更新peek
                }
                //入棧
                opr.push(c);
                i++;
            }
            //字符是數字時
            else {
                double sum=0;   //接收整數和
                double sum2=0;  //接收小數和
                int times = 1;  //記錄當前小數位數
                boolean flag=false; //是否開啟小數檢測模式

                //判斷下一個是不是+-*/,不是的話就繼續判斷直到非數字
                while (i<=arrC.length-1 && !isChar(arrC[i])){       //細節:括號內兩者順序不能改變
                    //遇到小數的情況
                    if (arrC[i]=='.'){
                        flag=true;
                    }else {
                        //小數情況
                        if (flag){
                            double val=arrC[i]-'0';
                            for (int j = 0; j < times; j++) {   //細節!用times縮小值
                                val/=10.0;
                            }
                            times++;
                            sum2+=val;
                        }
                        //正數情況
                        else {
                            sum=sum*10+arrC[i]-'0';     //獲取多位數字的關鍵!!!
                        }
                    }
                    i++;
                }
                number.push(sum+sum2);
            }
        }
        //字符都獲取完了後棧內還有數字和字符的話,就計算完棧內的數據並輸出最終結果
        while (!opr.isEmpty()) cal();
        System.out.println(number.peek());
    }

    //判斷是否為字符
    public static boolean isChar(char c) {
        return c=='+'||c=='-'||c=='*'||c=='/';
    }

    //判斷優先級是否是棧外字符小於等於棧內字符
    public static boolean isLowIn(char out,char in){
        return (out=='+'||out=='-')||(in=='*'||in=='/');
    }

    public static void cal(){
        //從棧內取出兩個數組和一個字符
        double a = number.pop();
        double b = number.pop();
        char c = opr.poll();

        //根據字符c進行不同的運算
        switch (c){
            case '+':
                number.push(a+b);
                break;
            case '-':
                number.push(b-a);
                break;
            case '*':
                number.push(b*a);
                break;
            case '/':
                number.push(b/a);
                break;
            default:
                System.out.println("字符輸入有誤");
        }
    }

}

模板方法模式

在執行一類業務時前面有很多步驟都是相同時,就可以寫一個模板抽象類,留出一個方法去給子類定義業務最後的操作。

該模式在源碼中大量的被應用。這樣寫會給後期維護提供非常清晰的思路

舉例:去醫院看病,挂號和看醫生是固定模式,但後面要不要開處方葯和拿葯是不一定的

//模板抽象類
/**
 * 抽象診斷方法,因為現在只知道挂號和看醫生是固定模式,剩下的開處方和拿葯都是不確定的
 */
public abstract class AbstractDiagnosis {

    public void test(){
        System.out.println("今天頭好暈,不想起床,開擺,先跟公司請個假");
        System.out.println("去醫院看病了~");
        System.out.println("1 >> 先挂號");
        System.out.println("2 >> 等待叫號");
        //由於現在不知道該開什麼處方,所以只能先定義一下行為,然後具體由子類實現
      	//大致的流程先定義好就行
        this.prescribe();
        this.medicine();  //開藥同理
    }

    public abstract void prescribe();   //開處方操作根據具體病症決定了

    public abstract void medicine();   //拿葯也是根據具體的處方去拿
}

//實現具體業務的子類
/**
 * 感冒相關的具體實現子類
 */
public class ColdDiagnosis extends AbstractDiagnosis{
    @Override
    public void prescribe() {
        System.out.println("3 >> 一眼丁真,鑒定為假,你這不是感冒,純粹是想擺爛");
    }

    @Override
    public void medicine() {
        System.out.println("4 >> 開點頭孢回去吃吧");
    }
}

//主方法
public static void main(String[] args) {
    AbstractDiagnosis diagnosis = new ColdDiagnosis();
    diagnosis.test();
}

責任鏈模式

就像闖關,一層接一層的往下進行。可以理解為報銷的時候需要一層一層審批

比如JavaWeb中學習的Filter過濾器,正是採用的責任鏈模式,通過將請求一級一級不斷向下傳遞,來對我們所需要的請求進行過濾和處理。

image

舉例:這裡就使用責任鏈模式來模擬一個簡單的面試過程,面試也是一面二面三面這樣走的流程,這裡先設計一下責任鏈上的各個處理器

//設計模板抽象方法,並在此基礎上寫層層往下的責任鏈
public abstract class Handler {

    protected Handler successor;    //這裡我們就設計責任鏈以單鏈表形式存在,這裡存放後繼節點

    public Handler connect(Handler successor){     //拼接後續節點
        this.successor = successor;
        return successor;  //這裡返回後繼節點,方便我們一會鏈式調用
    }

    public void handle(){
        this.doHandle();   //由不同的子類實現具體處理過程
        Optional
                .ofNullable(successor)	//設置可以為null
                .ifPresent(Handler::handle);    //責任鏈上如果還有後繼節點,就繼續向下傳遞
    }

    public abstract void doHandle();   //結合上節課學習的模板方法,交給子類實現
}

//一面子類
public class FirstHandler extends Handler{   //用於一面的處理器
    @Override
    public void doHandle() {
        System.out.println("============= 白馬程序員一面 ==========");
        System.out.println("1. 談談你對static關鍵字的理解?");
        System.out.println("2. 內部類可以調用外部的數據嗎?如果是靜態的呢?");
        System.out.println("3. hashCode()方法是所有的類都有嗎?默認返回的是什麼呢?");
        System.out.println("以上問題會的,可以依次打在評論區");
    }
}

//二面子類
public class SecondHandler extends Handler{  //二面
    @Override
    public void doHandle() {
        System.out.println("============= 白馬程序員二面 ==========");
        System.out.println("1. 如果我們自己創建一個java.lang包並且編寫一個String類,能否實現覆蓋JDK默認的?");
        System.out.println("2. HashMap的負載因子有什麼作用?變化規律是什麼?");
        System.out.println("3. 線程池的運作機制是什麼?");
        System.out.println("4. ReentrantLock公平鎖和非公平鎖的區別是什麼?");
        System.out.println("以上問題會的,可以依次打在評論區");
    }
}

//三面子類
public class ThirdHandler extends Handler{
    @Override
    public void doHandle() {
        System.out.println("============= 白馬程序員三面 ==========");
        System.out.println("1. synchronized關鍵字了解嗎?如何使用?底層是如何實現的?");
        System.out.println("2. IO和NIO的區別在哪裡?NIO三大核心組件?");
        System.out.println("3. TCP握手和揮手流程?少一次握手可以嗎?為什麼?");
        System.out.println("4. 操作系統中PCB是做什麼的?運行機制是什麼?");
        System.out.println("以上問題會的,可以依次打在評論區");
    }
}

//主方法
public static void main(String[] args) {
    Handler handler = new FirstHandler();  //一面首當其衝
    handler
            .connect(new SecondHandler())   //繼續連接二面和三面
            .connect(new ThirdHandler());
    handler.handle();   //開始面試
} 

命令模式

命令模式,此時會有三個頂層行為:遙控器、命令、接收器。

話不多說,直接搬例。小米傢具就是典型的命令模式。只需要在手機(遙控器)上通過紅外線、藍牙等按下一些命令,家中的傢具(接收器)就會執行命令。

舉例:設置三個頂層的 接口/抽象類 ,遙控器、命令、接收器

//遙控器
public class Controller {   //遙控器只需要把我們的指令發出去就行了
    public static void call(Command command){
        command.execute();
    }
}

//命令
public abstract class Command {   //指令抽象,不同的電器有指令

    private final Receiver receiver;

    protected Command(Receiver receiver){   //指定此命令對應的電器(接受者)
        this.receiver = receiver;
    }

    public void execute() {
        receiver.action();   //執行命令,實際上就是讓接收者開始幹活
    }
}

//接收器
public interface Receiver {
    void action();   //具體行為,這裡就寫一個算了
}

//具體接收器
public class AirConditioner implements Receiver{
    @Override
    public void action() {
        System.out.println("空調已開啟,呼呼呼");
    }
}

//具體命令
public class OpenCommand extends Command {
    public OpenCommand(AirConditioner airConditioner) {
        super(airConditioner);
    }
}

//可以創建具體控制器(手機),也可以不創建直接遙控,因為控制器一般只有一個。所以這裡就不創建了

//主方法
public static void main(String[] args) {
    AirConditioner airConditioner = new AirConditioner();   //先創建一個空調
    Controller.call(new OpenCommand(airConditioner));   //直接通過遙控器來發送空調開啟命令
}

迭代器模式

每個集合類都有相應的迭代器。很少自定義,大都是用jdk定義好的迭代器

迭代器最直接的例子就是foreach語法糖。

public static void main(String[] args) {
    List<String> list = Arrays.asList("AAA", "BBB", "CCC");
    for (String s : list) {   //使用foreach語法糖進行迭代,依次獲取每一個元素
        System.out.println(s);   //打印一下
    }
}

上述代碼編譯後的樣子:

public static void main(String[] args) {
    List<String> list = Arrays.asList("AAA", "BBB", "CCC");
    Iterator var2 = list.iterator();   //實際上這裡本質是通過List生成的迭代器來遍歷我們每個元素的

    while(var2.hasNext()) {   //判斷是否還有元素可以迭代,沒有就false
        String s = (String)var2.next();   //通過next方法得到下一個元素,每調用一次,迭代器會向後移動一位
        System.out.println(s);    //打印一下
    }
}

由此可知迭代器原理:使用迭代器對List進行遍歷時,實際上就像一個指向列表頭部的指針,我們通過不斷向後移動指針來依次獲取所指向的元素:

image

image

拓展:自定義迭代器。

(由於每個迭代器需要根據不同的集合類特點來設計,所以自定義迭代器前需要自定義一個集合類)

//自定義集合類
public class ArrayCollection<T> {    //首先設計一個簡單的數組集合,一會我們就迭代此集合內的元素

    private final T[] array;   //底層使用一個數組來存放數據

    private ArrayCollection(T[] array){   //private掉,自己用
        this.array = array;
    }

    public static <T> ArrayCollection<T> of(T[] array){   //開個靜態方法直接吧數組轉換成ArrayCollection,其實和直接new一樣,但是這樣寫好看一點
        return new ArrayCollection<>(array);
    }
}

//自定義迭代器
public class ArrayCollection<T> implements Iterable<T>{   //實現Iterable接口表示此類是支持迭代的

    ...

    @Override
    public Iterator<T> iterator() {    //需要實現iterator方法,此方法會返回一個迭代器,用於迭代我們集合中的元素
        return new ArrayIterator();
    }

    public class ArrayIterator implements Iterator<T> {   //這裡實現一個,注意別用靜態,需要使用對象中存放的數組
        private int cur = 0;   //這裡我們通過一個指針表示當前的迭代位置

        @Override
        public boolean hasNext() {     //判斷是否還有下一個元素
            return cur < array.length;   //如果指針大於或等於數組最大長度,就不能再繼續了
        }

        @Override
        public T next() {   //返回當前指針位置的元素並向後移動一位
            return array[cur++];   //正常返回對應位置的元素,並將指針自增
        }
    }
}

//主方法
public static void main(String[] args) {
    String[] arr = new String[]{"AAA", "BBB", "CCC", "DDD"};
    ArrayCollection<String> collection = ArrayCollection.of(arr);
    for (String s : collection) {    //可以直接使用foreach語法糖,當然最後還是會變成迭代器調用
        System.out.println(s);
    }
}

//編譯主方法後的樣子
public static void main(String[] args) {
    String[] arr = new String[]{"AAA", "BBB", "CCC", "DDD"};
    ArrayCollection<String> collection = ArrayCollection.of(arr);
    Iterator var3 = collection.iterator();   //首先獲取迭代器,實際上就是調用我們實現的iterator方法

    while(var3.hasNext()) {
        String s = (String)var3.next();   //直接使用next()方法不斷向下獲取
        System.out.println(s);
    }
}

中介者模式

將多對多的複雜關係,變成一對多的簡單明了關係

話不多說,直接上例子。中介,第一個想到的就是房子的中介。當一堆人需要出租房屋、另一堆人又需要租房,如果沒有中介,那再好的房子很難遇上租客也租不出去,此時就需要一個中介將雙方的需要進行匹配從而實現房子出租的目的。

不找中介:很亂,還不一定能夠遇上需求相同的對方

image

找中介:中介將雙方整理好,進行需求匹配

image

舉例:中介、出租者、租房者

//中介
public class Mediator {   //房產中介
    private final Map<String, User> userMap = new HashMap<>();   //在出售的房子需要存儲一下

    public void register(String address, User user){   //出售房屋的人,需要告訴中介他的房屋在哪裡
        userMap.put(address, user);
    }

    public User find(String address){   //通過此方法來看看有沒有對應的房源
        return userMap.get(address);
    }
}

//出租者和租房者(這裡偷懶就寫在一起了)
public class User {   //用戶可以是出售房屋的一方,也可以是尋找房屋的一方
    String name;
    String tel;

    public User(String name, String tel) {
        this.name = name;
        this.tel = tel;
    }
  
    public User find(String address, Mediator mediator){   //找房子的話,需要一個中介和你具體想找的地方
        return mediator.find(address);
    }

    @Override
    public String toString() {
        return name+" (電話:"+tel+")";
    }
}

//主方法
public static void main(String[] args) {
    User user0 = new User("劉女士", "10086");   //出租者
    User user1 = new User("李先生", "10010");   //租房者
    Mediator mediator = new Mediator();   //我是中介

    mediator.register("廣州市天河區白馬程序員", user0);   //出租人先把房子給中介掛上去

    User user = user1.find("廣州市天河區非馬程序員", mediator);  //租房者向指定中介找房子
    if(user == null) System.out.println("沒有找到對應的房源");
    System.out.println(user);   //成功找到對應房源
}

備忘錄模式

我也稱其為時光回溯模式。比較少用,大都是底層代碼才用

這個備忘錄不是我們平時用於記錄容易忘記的ddl,而是保存曾經某個時刻的狀態,後面有需要就恢復到該時刻的狀態

舉例:保存對象的狀態

//對象實體
public class Student {
    private String currentWork;   //當前正在做的事情
    private int percentage;   //當前的工作完成百分比

    public void work(String currentWork) {
        this.currentWork = currentWork;
        this.percentage = new Random().nextInt(100);
    }

    @Override
    public String toString() {
        return "我現在正在做:"+currentWork+" (進度:"+percentage+"%)";
    }
    
    public State save(){
        return new State(currentWork, percentage);
    }

    public void restore(State state){
        this.currentWork = state.currentWork;
        this.percentage = state.percentage;
    }
}

//狀態保存類
public class State {
    final String currentWork;
    final int percentage;

    State(String currentWork, int percentage) {   //僅開放給同一個包下的Student類使用
        this.currentWork = currentWork;
        this.percentage = percentage;
    }
}

//主方法
public static void main(String[] args) {
    Student student = new Student();
    student.work("學Java");   //開始學Java
    System.out.println(student);

    State savedState = student.save();   //保存一下當前的狀態

    student.work("打電動");   //剛打開B站播放視頻,學一半開始擺爛了
    System.out.println(student);

    student.restore(savedState);   //後悔浪費時間,回到上一個保存的狀態
    System.out.println(student);   //回到學Java的狀態
}

觀察者模式

觀察者模式可以實現監聽器機制,當對象發生改變時,觀察者能夠立即察覺到並進行一些聯動操作。很少用,大都是直接用監聽器

舉例:自定義觀察者

//觀察者接口
public interface Observer {   //觀察者接口
    void update();   //當對象有更新時,會回調此方法
}

//支持觀察者的實體
public class Subject {
    private final Set<Observer> observerSet = new HashSet<>();

    public void observe(Observer observer) {   //添加觀察者
        observerSet.add(observer);
    }

    public void modify() {   //模擬對象進行修改
        observerSet.forEach(Observer::update);   //當對象發生修改時,會通知所有的觀察者,並進行方法回調
    }
}

//主方法
public static void main(String[] args) {
    Subject subject = new Subject();
    subject.observe(() -> System.out.println("我是一號觀察者!"));
    subject.observe(() -> System.out.println("我是二號觀察者!"));
    subject.modify();
}

JDK也提供了是實現觀察者模式的相關接口:

//繼承接口表示支持觀察者
import java.util.Observable;    //java.util包下提供的觀察者抽象類

public class Subject extends Observable {   //繼承此抽象類表示支持觀察者

    public void modify(){
        System.out.println("對對象進行修改!");
        this.setChanged();    //當對對象修改後,需要setChanged來設定為已修改狀態
        this.notifyObservers(new Date());   //使用notifyObservers方法來通知所有的觀察者
      	//注意只有已修改狀態下通知觀察者才會有效,並且可以給觀察者傳遞參數,這裡傳遞了一個時間對象
    }
}

//主方法
public static void main(String[] args) {
    Subject subject = new Subject();
    subject.addObserver((o, arg) -> System.out.println("監聽到變化,並得到參數:"+arg));  
  	//注意這裡的Observer是java.util包下提供的
    subject.modify();   //進行修改操作
}

狀態模式

根據不同的狀態執行不同的行為

水在不同的溫度狀態會隨之改變,程序也可以達到某種狀態後就執行不同的行為

//枚舉狀態
public enum State {   //狀態直接使用枚舉定義
    NORMAL, LAZY
}

//實體類
public class Student {

    private State state;   //使用一個成員來存儲狀態

    public void setState(State state) {
        this.state = state;
    }

    public void study(){  
        switch (state) {   //根據不同的狀態,學習方法會有不同的結果
            case LAZY:
                System.out.println("只要我不努力,老闆就別想過上想要的生活,開擺!");
                break;
            case NORMAL:
                System.out.println("拼搏百天,我要上清華大學!");
                break;
        }
	}
}

//主方法
public static void main(String[] args) {
    Student student = new Student();
    student.setState(State.NORMAL);   //先正常模式
    student.study();

    student.setState(State.LAZY);   //開啟擺爛模式
    student.study();
}

策略模式

和狀態模式代碼一樣。但狀態模式思想是:狀態是先天的設定,就像水不同溫度狀態不同。而策略模式思想是:策略需要根據不同情況制定的。

舉例:簡單的數組排列

//策略接口(模板方法模式)
public interface Strategy {   //策略接口,不同的策略實現也不同

    Strategy SINGLE = Arrays::sort;   //單線程排序方案
    Strategy PARALLEL = Arrays::parallelSort;   //並行排序方案
    
    void sort(int[] array);
}

//排序類
public class Sorter {

    private Strategy strategy;   //策略

    public void setStrategy(Strategy strategy) {
        this.strategy = strategy;
    }

    public void sort(int[] array){
        strategy.sort(array);
    }
}

//主方法
public static void main(String[] args) {
    Sorter sorter = new Sorter();
    sorter.setStrategy(Strategy.PARALLEL);    //指定為並行排序方案
    
    sorter.sort(new int[]{9, 2, 4, 5, 1, 0, 3, 7});
}

訪問者模式

一件事情,一個訪問接口。訪問者實現該接口,但不同訪問者關心的事情着重點不同

舉例:假如你獲獎了還是國家級一等獎

//獎實體類
public class Prize {   //獎
    String name;   //比賽名稱
    String level;    //等級

    public Prize(String name, String level) {
        this.name = name;
        this.level = level;
    }

    public String getName() {
        return name;
    }

    public String getLevel() {
        return level;
    }
}

//訪問者接口
public interface Visitor {
    void visit(Prize prize);   //visit方法來訪問我們的獎項
}

//不同訪問者
public class Teacher implements Visitor {   //指導老師作為一個訪問者
    @Override
    public void visit(Prize prize) {   //它只關心你得了什麼獎以及是幾等獎,這也關乎老師的榮譽
        System.out.println("你得獎是什麼獎?"+prize.name);
        System.out.println("你得了幾等獎?"+prize.level);
    }
}

public class Boss implements Visitor{    //你的公司老闆作為一個訪問者
    @Override
    public void visit(Prize prize) {   //你的老闆只關心這些能不能為公司帶來什麼效益,獎本身並不重要
        System.out.println("你的獎項大么,能夠為公司帶來什麼效益么?");
        System.out.println("還不如老老實實加班給我多乾乾,別去搞這些沒用的");
    }
}

//主方法
public static void main(String[] args) {
    Prize prize = new Prize("ACM國際大學生程序設計競賽","一等價");
	Teacher teacher = new Teacher();
    Boss boss = new Boss();
    teacher.visit(prize);
    boss.visit(prize);

}