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 模式。


<<:  Re: 新手让网页 act 起来: Day17 - 探索 useEffect

>>:  视觉化当日趋势图(1)-client端架设&&工具篇

ios app 上架流程

由於目前的 app 还在实作阶段,先参考前辈们的上架经验! 目前查到比较完整的是这一篇,提供给大家参...

<Day13> Ticks — 取得股票(Stock)逐笔成交资料

● 接下来这几章会示范如何取得想要的Ticks资料 什麽是Ticks? "Tick&quo...

Day 29 | 状态管理-从官方范例来看如何使用BLoC (2)

今天就来实作UI的部分,以及来小小的比较一下BLoC与MobX的差异 我们把这个页面分成三个档案po...

使用appnode存储管理创建LVM

首先我是一名合格的菜鸟本文仅仅是记录自己遇到的问题及找到的解决方法! 最关键是我没搞懂appnode...

Day 0xB UVa948 Fibonaccimal Base

Virtual Judge ZeroJudge 题意 输入十进位的数字,输出对应的费氏进位表示法 ...