1. 程式人生 > >《Head First 設計模式》筆記10

《Head First 設計模式》筆記10

代理模式(Proxy)

為另一個物件提供一個替身或佔位符以控制對這個物件的訪問。

栗子

還記得上一個筆記中的糖果機吧,現在產品經理想要一份寫著糖果機位置、庫存和當前的狀態報告。

是不是挺簡單的?趕緊寫程式碼。

糖果機加上位置資訊:

class GumballMachine {
    // ...
    private String location;

    public GumballMachine(String location, int count) {
        this.location = location;
        // ...
    }

    public
String getLocation() { return location; } // ... }

一個監控糖果機的監視器:

class GumballMonitor {
    private GumballMachine gumballMachine;

    public GumballMonitor(GumballMachine gumballMachine) {
        this.gumballMachine = gumballMachine;
    }

    public void report() {
        System.out.println("糖果機:"
+ gumballMachine.getLocation()); System.out.println("當前庫存:" + gumballMachine.getCount()); System.out.println("當前狀態:" + gumballMachine.getState()); } }

測試

public static void main(String[] args) {
    GumballMachine gumballMachine = new GumballMachine("廣州", 600);
    GumballMonitor monitor = new
GumballMonitor(gumballMachine); monitor.report(); }

完美通過測試,收拾東西回家洗澡。

滿足了假的需求

產品經理的需求並沒有完全表達清楚,我們就開始寫了,最後白費了時間和精力,而且沒完成任務。(記得問清楚需求再去實現)

需求是要一個能遠端的監控器,而按我們上面的監視器和糖果機程式碼,它們就是在同一個 JVM 上執行的,就相當於一個本地監控器,什麼意思呢?相當於在教室裡裝了一個攝像頭,而且是實時監控,沒聯網的,那麼只能在教室看,對於坐在辦公室的老師來說這個攝像頭沒起作用。

前置知識

RMI:遠端方法呼叫(Remote Method Invocation),用於不同虛擬機器之間的通訊,這些虛擬機器可以在不同的主機上、也可以在同一個主機上;一個虛擬機器中的物件呼叫另一個虛擬機器中的物件的方法,而允許被遠端呼叫的物件需要通過一些標誌加以標識。

詳情的可以看看這篇文章

製作遠端服務

將一個普通的物件變成可以被遠端客戶呼叫的遠端物件,簡要步驟:
1. 製作遠端介面:遠端介面定義出可以讓客戶遠端呼叫的方法。客戶將用它作為服務的類型別。Stub 和實際的服務都實現此介面。
2. 製作遠端實現:做實際工作的類,為遠端介面中定義的遠端方法提供了真正的實現,是客戶真正想要呼叫方法的物件(比如我們的GumballMachine)。
3. 利用 rmic 產生的 stub 和 skeleton:客戶和服務的輔助類。不需要建立,因為執行 rmic 工具時就會自動處理。
4. 啟動 RMI registry:rmireistry 就像電話簿,客戶可以從中查到代理的位置(就是客戶的 stub helper 物件)。
5. 開始遠端服務:讓服務物件開始執行。服務實現類會去例項化一個服務的例項,並將這個服務註冊到 RMI registry。註冊之後,這個服務就可以供客戶呼叫了。

製作遠端介面

擴充套件 java.rmi.Remote

Remote 不具有方法,只是作為一個“記號”介面。對 RMI 來說,Remote 介面具有特別的意義。

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface MyRemote extends Remote {
    public String sayHello() throws RemoteException;
}

注意:

所有的方法都要宣告丟擲 RemoteException,因為客戶會呼叫實現遠端介面的 Stub 上的方法,而 Stub 底層用到了網路和 I/O,所以各種意外都可能發生。
方法上的變數和返回值都必須屬於原語(primitive)或可序列化(Serializable)型別(遠端方法的變數必須被打包並通過網路運送,這需要序列化)。原語型別、字串和許多 API 中內定的型別都不會有問題,但如果是自己定義的類,必須保證它實現了 Serializable。

製作遠端實現

你的服務必須實現遠端介面,它是客戶將要呼叫的方法的介面。

import java.rmi.server.UnicastRemoteObject;

public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote {
    public MyRemoteImpl() throws RemoteException { }

    public String sayHello() {
        return "伺服器:你好";
    }

    // 為了方便後面的啟動服務
    public static void main(String[] args) {
        try {
            MyRemote service = new MyRemoteImpl();
            Naming.rebind("RemoteHello", service);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

注意:

擴充套件 UnicastRemoteObject:為了成為遠端服務物件,你的物件需要某些“遠端的”功能。最簡單的方式就是擴充套件 UnicastRemoteObject,讓超類幫你做這些工作。
不帶變數的構造器要宣告 RemoteException,這樣當類被例項化的時候,超類的構造器總是會被呼叫。如果超類的構造器丟擲異常,那麼你只能什麼子類的構造器也丟擲異常。

產生 Stub 和 Skeleton

在遠端實現類上執行 rmic 命令:

rmic MyRemoteImpl

執行 rmireistry

執行命令啟動 rmireistry

rmireistry

啟動服務

在這個簡單的例子中,我們從實現類中的 main 方法啟動:

java MyRemoteImpl

客戶取得 Stub 物件

客戶總是使用遠端介面做為服務型別,事實上客戶不需要知道遠端服務的真正類名什麼。

import java.rmi.*;

class MyRemoteClient {
    public static void main(String[] args) {
        try {
            // 通過查詢 RemoteHello 註冊名,找到遠端服務
            MyRemote service = (MyRemote) Naming.lookup("rmi://127.0.0.1/RemoteHello");
            System.out.println(service.sayHello());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

注意:

要先啟動 rmireistry 註冊,再啟動遠端服務。
遠端方法的變數和返回值型別必須為可序列化的型別。
必須給客戶提供 Stub 類。

客戶機內有:Client.class、MyRemoteImpl_Stub.class、MyRemote.class
遠端服務機內有:MyRemoteImpl.class、MyRemoteImpl_Stub.class、MyRemoteImpl_Skel.class、MyRemote.class

繼續完成需求

把糖果機變成一個遠端服務

同樣,按照上面的步驟進行。

1.建立遠端介面:

import java.rmi.*;

public interface GumballMachineRemote extends Remote {
    public int getCount() throws RemoteException;
    public String getLocation() throws RemoteException;
    public State getState() throws RemoteException;
}

2.State 這個返回型別不是序列化的,要修改:

import java.io.Serializable;

public interface State extends Serializable {
    public void insertQuarter();
    public void ejectQuarter();
    public void turnCrank();
    public void dispense();
}

3.每個實體狀態都維護著一個糖果機的引用,而我們不希望整個糖果機都被序列化並隨著 State 物件一起傳送:

加上 transient 關鍵字,告訴 JVM 不要序列化這個欄位。

public class SoldState implements State {
    transient GumballMachine gumballMachine;

    // ...
}

4.糖果機要實現遠端介面:

import java.rmi.*;
import java.rmi.server.UnicastRemoteObject;

public class GumballMachine extends UnicastRemoteObject implements GumballMachineRemote {
    // 構造方法會拋 RemoteException 異常,因為超類
    public GumballMachine(String location, int count) throws RemoteException {
        // ...
    }

    // ...
}

5.修改監控器:

import java.rmi.*;

public class GumballMonitor {
    // 改成遠端介面上的糖果機
    private GumballMachineRemote gumballMachine;

    public GumballMonitor(GumballMachineRemote gumballMachine) {
        this.gumballMachine = gumballMachine;
    }

    public void report() {
        try {
            System.out.println("糖果機:" + gumballMachine.getLocation());
            System.out.println("當前庫存" + gumballMachine.getCount());
            System.out.println("當前狀態" + gumballMachine.getState());
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }
}

在 RMI registry 中註冊

糖果機服務已經完成了,現在要把它裝上,好開始接收請求。

首先要確保將它註冊到 RMI registry 中,好讓客戶可以找到它:

public class GumballMachineTestDrive {
    public static void main(String[] args) {
        if (args.length < 2) {
            System.out.println("GumballMachine <location> <count>");
            System.exit(1);
        }

        try {
            String location = args[0];
            int count = Integer.parseInt(args[1]);
            GumballMachineRemote gumballMachine = new GumballMachine(location, count);
            // 用糖果機的位置釋出糖果機的 stub
            Naming.rebind("//" + location + "/gumballmachine", gumballMachine);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

在命令列註冊:

rmiregistry

啟動糖果機服務:

java GumballMachineTestDrive GuangZhou 100

監控器測試程式

現在一切就緒,可以嘗試監控更多糖果機:

public class GumballMonitorTestDrive {
    public static void main(String[] args) {
        List<String> locations = Arrays.asList( "rmi://GuangZhou/gumballmachine",
                                                "rmi://ShangHai/gumballmachine",
                                                "rmi://BeiJing/gumballmachine");
        List<GumballMonitor> monitors = new ArrayList<>();

        locations.forEach(location -> {
            try {
                // 為每個遠端機器建立一個代理
                GumballMachineRemote machine = (GumballMachineRemote) Naming.lookup(location);
                monitors.add(new GumballMonitor(machine));
            } catch (Exception e) {
                e.printStackTrace();
            }
        });

        monitors.forEach(monitor -> monitor.report());
    }
}

虛擬代理(Virtual Proxy)

遠端代理

遠端代理可以作為另一個 JVM 上物件的本地代表。呼叫代理方法,會被代理利用網路轉發到遠端執行,並且結果會通過網路返回給代理,再由代理將結果轉給客戶。

本地客戶 Client 發出請求 -> 本地的遠端代理 proxy 轉發該請求 -> 遠端物件 RealSubject

虛擬代理

虛擬代理作為建立開銷大的物件的代表。虛擬代理經常直到我們真正需要一個物件的時候才建立它。當物件在建立前和建立中時,由虛擬代理來扮演物件的替身。物件建立後,代理就會將請求委託給物件。

本地客戶 Client 發出請求 -> 本地的虛擬代理 proxy 處理請求 -> 如果 RealSubject(開銷大的物件)已經建立,proxy 就把請求委託給 RealSubject;否則 proxy 建立該 RealSubject。