iT邦幫忙

2021 iThome 鐵人賽

DAY 17
0
Software Development

也該是時候學學 Design Pattern 了系列 第 17

Day 17: Structural patterns - Proxy

目的

將實際執行的服務遮蔽,取而代之的,建立一個代理人負責對外窗口的身份,以及對內與該服務溝通。

說明

我們可能會依照情況的需求,必須將實際運作服務的物件進行遮蔽,這些情況可能是:

  • 服務需要較長的時間啟動,等候期間沒有回應。
  • 服務本身接觸到機密資料,不能開放給外部任意存取。
  • 服務本身在遠端主機上,當需要使用時則在本地端建立代理人,由代理人負責連線。
  • 快取機制,減少服務的運作負擔。

為了實現以上的需求,需要建立一個代理人,負責對外窗口的一切,任何需求都會先經過代理人這一關,通過這關後才會與實際服務接觸、運作等等。

實踐的作法是:

  1. 觀察服務本身的主要功能能不能抽離出虛擬層,讓服務與代理人都繼承,方便代理人不論在對外或是對內的使用都能簡單化。如果無法抽離,則讓代理人繼承服務。
  2. 建立代理人,除了實作虛擬層之外,本身要儲存與服務本體的 reference,方便對內的呼叫。
  3. 實作虛擬層定義的方法,這邊重點在「需求」本身,依照需求量身打照,只要滿足需求的情況下才能呼叫實際服務。

以下範例以需求「服務本身接觸到機密資料,不能開放給外部任意存取」為核心製作。

UML 圖

Proxy Pattern UML Diagram

使用 Java 實作

使用者物件:User

public class User {
    private String id;
    private String name;
    private String level;

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

    public String getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getLevel() {
        return level;
    }
}

定義共同方法的虛擬層:AccessDataLibrary

public interface AccessDataLibrary {
    HashMap<String, User> operatorUsers(String token);

    HashMap<String, User> auditUsers(String toke);

    HashMap<String, User> adminUsers(String token);
}

實際服務的物件:AccessData

public class AccessData implements AccessDataLibrary {
    private HashMap<String, User> adminData = new HashMap<>();
    private HashMap<String, User> auditData = new HashMap<>();
    private HashMap<String, User> operatorData = new HashMap<>();

    public AccessData() throws IOException, ParseException {
        for (int i = 0; i < 3; i++) {
            String id = RandomID.exec(5);
            String name = NameSelector.exec();
            adminData.put(name, new User(id, name, "admin"));
        }

        for (int i = 0; i < 5; i++) {
            String id = RandomID.exec(7);
            String name = NameSelector.exec();
            auditData.put(name, new User(id, name, "audit"));
        }

        for (int i = 0; i < 10; i++) {
            String id = RandomID.exec(10);
            String name = NameSelector.exec();
            operatorData.put(name, new User(id, name, "operator"));
        }
    }

    @Override
    public HashMap<String, User> operatorUsers(String token) {
        return operatorData;
    }

    @Override
    public HashMap<String, User> auditUsers(String token) {
        return auditData;
    }

    @Override
    public HashMap<String, User> adminUsers(String token) {
        return adminData;
    }
}

代理人物件:AccessDataProxy(Proxy 物件)

public class AccessDataProxy implements AccessDataLibrary {
    private AccessData accessData;

    public AccessDataProxy() throws IOException, ParseException {
        this.accessData = new AccessData();
    }

    @Override
    public HashMap<String, User> operatorUsers(String token) {
        if (token.contentEquals("operator") || token.contentEquals("audit") || token.contentEquals("admin")) {
            return accessData.operatorUsers("PASS");
        } else {
            return new HashMap<>();
        }
    }

    @Override
    public HashMap<String, User> auditUsers(String token) {
        if (token.contentEquals("audit") || token.contentEquals("admin")) {
            return accessData.auditUsers("PASS");
        } else {
            return new HashMap<>();
        }
    }

    @Override
    public HashMap<String, User> adminUsers(String token) {
        if (token.contentEquals("admin")) {
            return accessData.adminUsers("PASS");
        } else {
            return new HashMap<>();
        }
    }
}

使用介面:AccessDataApp

public class AccessDataApp {
    private AccessDataLibrary middleware;

    public AccessDataApp(AccessDataLibrary middleware) {
        this.middleware = middleware;
    }

    public void pushLaunchButton(User user, String target) {
        System.out.println("身為 " + user.getLevel() + ",嘗試取得 " + target + " 等級的資料");
        HashMap<String, User> targetData = null;

        if (target.contentEquals("operator")) {
            targetData = middleware.operatorUsers(user.getLevel());
        } else if (target.contentEquals("audit")) {
            targetData = middleware.auditUsers(user.getLevel());
        } else if (target.contentEquals("admin")) {
            targetData = middleware.adminUsers(user.getLevel());
        }
        System.err.println("\n---系統顯示---");

        if (targetData == null) {
            System.err.println("權限不符合");
        } else {
            System.out.println("人員姓名: " + user.getName() + " ,身份: " + user.getLevel() + " ,順利取得 " + target + " 等級的資料");
            System.out.println("人員清單:");

            for (User listUser : targetData.values()) {
                System.out.println(listUser.getId() + " " + listUser.getName() + " " + listUser.getLevel());
            }
        }

        System.err.println("---系統關閉---\n");
    }
}

測試,建立不同權限的使用者取得不同等級的資料:AccessDataProxySample

public class AccessDataProxySample {
    public static void main(String[] args) throws IOException, ParseException {
        AccessDataLibrary accessDataProxy = new AccessDataProxy();
        AccessDataApp app = new AccessDataApp(accessDataProxy);

        System.out.println("模擬不同權限的人員,取得資料的情境");
        System.out.println("情境一:Operator");
        User operator = new User(RandomID.exec(5), NameSelector.exec(), "operator");
        app.pushLaunchButton(operator, "admin");
        app.pushLaunchButton(operator, "audit");
        app.pushLaunchButton(operator, "operator");

        System.out.println("情境二:Audit");
        User audit = new User(RandomID.exec(5), NameSelector.exec(), "audit");
        app.pushLaunchButton(audit, "admin");
        app.pushLaunchButton(audit, "audit");
        app.pushLaunchButton(audit, "operator");

        System.out.println("情境三:Admin");
        User admin = new User(RandomID.exec(5), NameSelector.exec(), "admin");
        app.pushLaunchButton(admin, "admin");
        app.pushLaunchButton(admin, "audit");
        app.pushLaunchButton(admin, "operator");
    }
}

Utils:NameSelectorRandomID

public class NameSelector {
    private static Random random = new Random();

    public static String exec() throws IOException, ParseException {
        JSONParser parser = new JSONParser();
        Object obj = parser.parse(new FileReader("./src/utils/boyNameList.json")); // 讀取 JSON Array,內容是字串陣列
        JSONArray jsonArray = (JSONArray) obj;

        // 0 - 149
        int option = random.nextInt(149 + 0) + 0;
        return (String) jsonArray.get(option);
    }
}

public class RandomID {
    private static final Random RANDOM = new SecureRandom();
    private static final String ALPHABET = "0123456789QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm";

    private RandomID() {
        throw new IllegalStateException("Utility class");
    }

    public static String exec(int length) {
        StringBuilder buffer = new StringBuilder(length);

        for (int i = 0; i < length; i++) {
            buffer.append(ALPHABET.charAt(RANDOM.nextInt(ALPHABET.length())));
        }

        return new String(buffer);
    }
}

使用 JavaScript 實作

使用者物件:User

class User {
  /**
   * @param {string} id
   * @param {string} name
   * @param {string} level
   */
  constructor(id, name, level) {
    this.id = id;
    this.name = name;
    this.level = level;
  }

  getId() {
    return this.id;
  }

  getName() {
    return this.name;
  }

  getLevel() {
    return this.level;
  }
}

定義共同方法的虛擬層:AccessDataLibrary

/** @interface */
class AccessDataLibrary {
  /**
   * @param {string} token
   * @returns {Map<string, User>}
   */
  operatorUsers(token) {
    return new Map();
  }

  /**
   * @param {string} token
   * @returns {Map<string, User>}
   */
  auditUsers(token) {
    return new Map();
  }

  /**
   * @param {string} token
   * @returns {Map<string, User>}
   */
  adminUsers(token) {
    return new Map();
  }
}

實際服務的物件:AccessData

class AccessData extends AccessDataLibrary {
  constructor() {
    super();
    /** @type {Map<string, User>} */
    this.adminData = new Map();
    /** @type {Map<string, User>} */
    this.auditData = new Map();
    /** @type {Map<string, User>} */
    this.operatorData = new Map();

    for (let i = 0; i < 3; i++) {
      const id = randomID(5);
      const name = nameSelector();
      this.adminData.set(name, new User(id, name, "admin"));
    }

    for (let i = 0; i < 5; i++) {
      const id = randomID(7);
      const name = nameSelector();
      this.auditData.set(name, new User(id, name, "audit"));
    }

    for (let i = 0; i < 10; i++) {
      const id = randomID(10);
      const name = nameSelector();
      this.operatorData.set(name, new User(id, name, "operator"));
    }
  }

  /**
   * @override
   * @param {string} token
   * @returns {Map<string, User>}
   */
  operatorUsers(token) {
    return this.operatorData;
  }

  /**
   * @override
   * @param {string} token
   * @returns {Map<string, User>}
   */
  auditUsers(token) {
    return this.operatorData;
  }

  /**
   * @override
   * @param {string} token
   * @returns {Map<string, User>}
   */
  adminUsers(token) {
    return this.operatorData;
  }
}

代理人物件:AccessDataProxy(Proxy 物件)

class AccessDataProxy extends AccessDataLibrary {
  constructor() {
    super();
    this.accessData = new AccessData();
  }

  /**
   * @override
   * @param {string} token
   * @returns {Map<string, User>}
   */
  operatorUsers(token) {
    if (token === "operator" || token === "audit" || token === "admin") {
      return this.accessData.operatorUsers("PASS");
    } else {
      return new Map();
    }
  }

  /**
   * @override
   * @param {string} token
   * @returns {Map<string, User>}
   */
  auditUsers(token) {
    if (token === "audit" || token === "admin") {
      return this.accessData.auditUsers("PASS");
    } else {
      return new Map();
    }
  }

  /**
   * @override
   * @param {string} token
   * @returns {Map<string, User>}
   */
  adminUsers(token) {
    if (token === "admin") {
      return this.accessData.adminUsers("PASS");
    } else {
      return new Map();
    }
  }
}

使用介面:AccessDataApp

class AccessDataApp {
  /**
   * @param {AccessDataLibrary} middleware
   */
  constructor(middleware) {
    this.middleware = middleware;
  }

  /**
   * @param {User} user
   * @param {string} target
   */
  pushLaunchButton(user, target) {
    console.log("身為 " + user.getLevel() + ",嘗試取得 " + target + " 等級的資料");
    /** @type {Map<string, User>} */
    let targetData = null;

    if (target === "operator") {
      targetData = this.middleware.operatorUsers(user.getLevel());
    } else if (target === "audit") {
      targetData = this.middleware.auditUsers(user.getLevel());
    } else if (target === "admin") {
      targetData = this.middleware.adminUsers(user.getLevel());
    }
    console.error("\n---系統顯示---");

    if (targetData == null) {
      console.error("權限不符合");
    } else {
      console.log("人員姓名: " + user.getName() + " ,身份: " + user.getLevel() + " ,順利取得 " + target + " 等級的資料");
      console.log("人員清單:");

      for (const listUser of targetData.values()) {
        console.log(listUser.getId() + " " + listUser.getName() + " " + listUser.getLevel());
      }
    }

    console.error("---系統關閉---\n");
  }
}

測試,建立不同權限的使用者取得不同等級的資料:AccessDataProxySample

const accessDataProxySample = () => {
  const accessDataProxy = new AccessDataProxy();
  const app = new AccessDataApp(accessDataProxy);

  console.log("模擬不同權限的人員,取得資料的情境");
  console.log("情境一:Operator");
  const operator = new User(randomID(5), nameSelector(), "operator");
  app.pushLaunchButton(operator, "admin");
  app.pushLaunchButton(operator, "audit");
  app.pushLaunchButton(operator, "operator");

  console.log("情境二:Audit");
  const audit = new User(randomID(5), nameSelector(), "audit");
  app.pushLaunchButton(audit, "admin");
  app.pushLaunchButton(audit, "audit");
  app.pushLaunchButton(audit, "operator");

  console.log("情境三:Admin");
  const admin = new User(randomID(5), nameSelector(), "admin");
  app.pushLaunchButton(admin, "admin");
  app.pushLaunchButton(admin, "audit");
  app.pushLaunchButton(admin, "operator");
}

accessDataProxySample();

Utils:NameSelectorRandomID

/**
 * @param {number} digits
 */
const randomID = (digits) => {
  const ALPHABET = "0123456789QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm";
  let id = "";

  for (let i = 0; i < digits; i++) {
    const randomIndex = Math.floor(Math.random() * (ALPHABET.length - 1 - 0 + 1)) + 0;
    id += ALPHABET[randomIndex];
  }

  return id;
}

const nameSelector = () => {
  const boyNameList = require('./src/utils/boyNameList.json');  // 讀取 JSON Array,內容是字串陣列
  const option = Math.floor(Math.random() * (149 - 0 + 1)) + 0;
  return boyNameList[option];
}

總結

Proxy 模式好玩在於,閱讀到常見情境時,才恍然大悟,不少情境自己早已體驗過。

  • 遊戲需要較長的時間啟動? -> 那建立一個假的,負責在啟動時跟玩家互動,避免長時間的等待造成玩家的厭煩。
  • 機密資料不能任意存取? -> 在影集上看過權限不足者存取資料遭到拒絕的畫面,或是權限最高者可以任意存取。
  • 服務本身在遠端主機? -> 常見的 VPN 啊!
  • 快取機制? -> 在系統設計101—大型系統的演進(下)文章內初步介紹系統設計可以架設快取伺服器,將常見的 Request 資源放入快取伺服器內,好減少實際服務的負擔。

真的很有趣,在於我從來沒想過這些情境原來都可以稱作 Proxy 模式!

我才是 Proxy 模式

這是最後一篇 Structural patterns,明天將進入下個類別:Behavioural patterns 的第一個模式:Chain of Responsibility 模式。


上一篇
Day 16: Structural patterns - Flyweight
下一篇
Day 18: Behavioral patterns - Chain of Responsibility
系列文
也該是時候學學 Design Pattern 了31

尚未有邦友留言

立即登入留言