Skilled design pattern

Posted by turdferguson on Fri, 04 Mar 2022 11:29:30 +0100

Chain mode (OperateOfResponsibility)

core

  • Definition: returns the current object in the object method to realize the chain call to multiple methods of one object
  • Key points: simplify interface calls (return this)
  • Application: jQuery method chain call

Example 1: jQuery get element function

  • Requirements: simply simulate the basic functions of jQuery to obtain elements
  • js
// index.js
var A = function (selector, context) {
  return new A.fn.init(selector, context);
};
A.fn = A.prototype = {
  /**
   * // init Method, new. This method will report an error
   * init(){
   *
   * }
   * All method definitions are not constructors, and TypeError will be thrown if you try to instantiate them.
   */
  /**
   *
   * @param {string} selector selector
   * @param {dom|undefined} context context
   * @returns
   */
  init: function (selector, context = document) {
    this.length = 0;
    if (~selector.indexOf("#")) {
      this[0] = document.getElementById(selector.slice(1));
      this.length = 1;
    } else {
      var doms = context.getElementsByTagName(selector),
        i = 0,
        len = doms.length;
      for (; i < len; i++) {
        this[i] = doms[i];
      }
      this.length = len;
    }
    this.context = context;
    this.selector = selector;
    return this;
  },
  size() {
    return this.length;
  },
  splice: [].splice, // Enhanced array properties
};
A.fn.init.prototype = A.fn; // Prototype chain inheritance

console.log(A("#d1"));
console.log(A("#d1").size()); // 1
console.log(A("div", A("#d1")[0]).size()); // 3

if (typeof module !== "undefined") {
  module.exports = A;
}
  • html
 <div id="d1">
    <div>11</div>
    <div></div>
    <div></div>
  </div>
  • effect

Example: jQuery method 2

  • Requirement: implement the extend method of jQuery (internal / external object extension)
  • js
// extend.js
// nodejs environment
if (typeof require === "function") {
  A = require("./index");
}

/**
 * Extended object member method
 * Pass 2 or more parameters, similar to object Assign method (external object extension)
 * Pass a parameter to expand this(A or A.extend) (internal expansion)
 */
A.extend = A.fn.extend = function () {
  // The extension object is calculated from the second parameter
  let i = 1,
    len = arguments.length,
    //Source object
    target = arguments[0],
    // Traverse the pointer of the extension object
    j;
  // Only 1 parameter
  if (len === 1) {
    target = this;
    // The first parameter is the extension object
    i--;
  }
  // Traversal extension object
  for (; i < len; i++) {
    for (j in arguments[i]) {
      // Expand source object
      target[j] = arguments[i][j];
    }
  }
  return target;
};

// External expansion (similar to Object.assign)
var obj = {};
console.log(A.extend(obj, { a: 1, b: 2 }, { a: 2, c: 3 }) === obj); // true
console.log(obj); // { a: 2, b: 2, c: 3 }
A.fn.extend(obj, { d: 4, b: 0 });
console.log(obj); // { a: 2, b: 0, c: 3, d: 4 }

// Internal extension (jQuery framework extends classes and prototype methods)
// Browser environment test
if (typeof window === "object") {
  // Expand A.fn (prototype expansion, instance access)
  A.extend(A.fn, { version: "1.0" });
  const d1 = A("#d1");
  console.log(d1); // init [div#d1, context: document, selector: "#d1"]
  console.log(d1.version); // 1.0
  A.fn.extend({
    getVersion() {
      return this.version;
    },
  });
  console.log(d1.getVersion()); // 1.0

  // Extension A (class / constructor extension)
  A.extend({ a: 11, b: 22 });
  A.extend({ c: 33 });
  const { a, b, c } = A;
  console.log(a, b, c); // 11 22 33
}

Example 3: jQuery operation DOM method

  • Requirements: simulate jQuery, realize event binding, html / css / attr query and assignment
  • js
A.extend({
  // Convert - to hump
  camelCase(str) {
    return str.replace(/\-(\w)/g, function (all, letter) {
      console.log(all, letter);
      return letter.toUpperCase();
    });
  },
});
// Add method (event, attribute, class, html)
A.fn.extend({
  // Event, create functions in different environments, and reduce the verification when calling
  on: (function () {
    // Flag browser DOM2 level events
    if (document.addEventListener) {
      return function (type, fn) {
        for (var i = 0; i < this.length; i++) {
          this[i].addEventListener(type, fn, false);
        }
        return this;
      };
    }
    // IEDOM2 level events
    else if (document.attachEvent) {
      return function (type, fn) {
        for (var i = 0; i < this.length; i++) {
          this[i].attachEvent(`on${type}`, fn);
        }
        return this;
      };
    }
    // DOM2 level events are not supported
    else {
      return function (type, fn) {
        for (var i = 0; i < this.length; i++) {
          this[i][`on${type}`] = fn;
        }
        return this;
      };
    }
  })(),
  /**
   *
   * @returns this
   */
  css() {
    const args = arguments,
      len = args.length;
    if (this.length < 1) return this;
    // Only one parameter
    if (len === 1) {
      // Get style $ css('width)
      if (typeof args[0] === "string") {
        return getComputedStyle(this[0])[A.camelCase(args[0])];
      }
      // Set $ css({width: '20px','background-color':'red'})
      else if (typeof args[0] === "object" && args[0] !== null) {
        for (var i in args[0]) {
          for (var j = 0; j < this.length; j++) {
            this[j].style[A.camelCase(i)] = args[0][i];
          }
        }
      }
    }
    // Two parameters $ css('width','30px')
    else if (len === 2) {
      for (var j = 0; j < this.length; j++) {
        this[j].style[args[0]] = args[1];
      }
    }
    return this;
  },
  attr() {
    const args = arguments,
      len = args.length;
    if (this.length < 1) return this;
    // Only one parameter
    if (len === 1) {
      // Get style $ attr('class')
      if (typeof args[0] === "string") {
        return getAttribute(this[0])[name];
      }
      // Set $ attr({name: 'xx','id':'xx'})
      else if (typeof args[0] === "object" && args[0] !== null) {
        for (var i in args[0]) {
          for (var j = 0; j < this.length; j++) {
            this[j].setAttribute(i, args[0][i]);
          }
        }
      }
    }
    // Two parameters $ attr('id','xx')
    else if (len === 2) {
      for (var j = 0; j < this.length; j++) {
        this[j].setAttribute(args[0], args[1]);
      }
    }
    return this;
  },
  html() {
    const args = arguments,
      len = args.length;
    if (len === 0) {
      return this[0] && this[0].innerHTML;
    } else {
      // One parameter
      for (var i = 0; i < this.length; i++) {
        this[i].innerHTML = args[0];
      }
    }
    return this;
  },
});
const input = A("#input1");
input
  .css({
    border: "1px solid #ddd",
    "background-color": "red",
    width: "100px",
  })
  .attr({
    name: "input",
    class: "xx",
  });
console.log(input.css("background-color"));
A("#div2")
  .html("<p>I am xxx</p>")
  .on("click", function (e) {
    console.log(e);
  });
  • html
<body>
  <div id="d1">
    <div>11</div>
    <div></div>
    <div></div>
  </div>
  <div>
    <input id="input1" />
    <div id="div2"></div>
  </div>
  <script src="./index.js"></script>
  <script src="./extend.js"></script>
</body>
  • effect

Delegate mode (Entrust)

core

  • Definition: multiple objects accept and process the same request and delegate to another object to process the request uniformly
  • Key points: centralized processing of multiple event operations to optimize performance
  • Application: DOM event delegation

Example 1: event delegation

  • Requirement: delegate child element events to parent elements
  • advantage
    • Avoid performance problems caused by a large number of child element binding events
    • Elements and their descendants have strong correlation business logic and can be handled in a unified manner
  • js
// Event delegation
var div = document.getElementById("d1");
let btn3;
div.onclick = function (e) {
  const target = e.target;
  var type = target.dataset.type;
  if (type === "btn1") {
    target.style.backgroundColor =
      target.style.backgroundColor === "red" ? "" : "red";
  } else if (type === "btn2" && !btn3) {
    btn3 = document.createElement("button");
    btn3.innerText = "Button 3";
    btn3.setAttribute("data-type", "btn3");
    target.parentElement.appendChild(btn3);
  } else if (type === "btn3") {
    target.parentElement.removeChild(btn3);
    btn3 = null;
  }
};
  • html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    #d1 {
      position: relative;
      border: 1px solid #ddd;
      width: 500px;
      padding: 30px;
    }

    div {
      margin-bottom: 10px;
    }

    span {
      position: absolute;
      right: 10px;
      top: 10px;
    }

    #div {
      margin: 20px;
    }
  </style>
</head>

<body>
  <div id="d1" data-type="main">
    <span data-type="close">xx Icon</span>
    <p>Content content content content content content content content content content content content content content content content content content content content</p>
    <div>
      <input data-type="kw" />
    </div>
    <div>
      <button data-type="btn1">Button 1</button>
      <button data-type="btn2">Button 2</button>
    </div>
  </div>
  <script src="./index.js"></script>
</body>

</html>
  • effect

Data access object schema (dataaccessobject Dao)

core

  • Definition: abstract and encapsulate the access and storage of data sources
  • Application: cache read / write encapsulation, database read / write encapsulation

Example 1: html5 localStorage access encapsulation

  • Requirement: encapsulate the addition, deletion, modification and query operations of localStorage API and return relevant data
  • js
// index.js

// Requirements: local storage encapsulation

/**
 * Local storage class
 * @param {string} preId Prefix id
 * @param {string} timeSign Splice connector between timestamp and data
 */
var BaseLocalStorage = function (preId, timeSign) {
  this.preId = preId;
  this.timeSign = timeSign || "|-|";
};

BaseLocalStorage.prototype = {
  // Operation status
  status: {
    SUCCESS: 0,
    FAIL: 1,
    OVERFLOW: 2,
    TIMEOUT: 3,
  },
  storage: localStorage,
  getKey(key) {
    return this.preId + key;
  },
  // Add / modify data
  set(key, value, callback, time) {
    // Default state
    var status = this.status.SUCCESS,
      key = this.getKey(key);
    try {
      // time is the date object or timestamp
      time = new Date(time).getTime() || time.getTime();
    } catch (e) {
      // The default time is 30 days
      time = new Date().getTime() + 1000 * 3600 * 24 * 30;
    }
    try {
      this.storage.setItem(key, time + this.timeSign + value);
    } catch (e) {
      // Overflow failed
      status = this.status.OVERFLOW;
    }
    // Callback
    callback && callback.call(this, status, key, value);
  },
  // get data
  get(key, callback) {
    var status = this.status.SUCCESS,
      key = this.getKey(key),
      // Default value
      value = null,
      timeSignLen = this.timeSign.length,
      // The starting position of the splice in the data
      index,
      // time stamp
      time,
      // Final data
      result;
    try {
      value = this.storage.getItem(key);
    } catch (e) {
      console.log(this);
      result = {
        status: this.status.FAIL,
        value: null,
      };
      callback && callback.call(this, result.status, result.value);
      return result;
    }
    if (value) {
      index = value.indexOf(this.timeSign);
      time = +value.slice(0, index);
      if (time > new Date().getTime()) {
        value = value.slice(index + timeSignLen);
      } else {
        value = null;
        // be overdue
        status = this.status.TIMEOUT;
        this.remove(key);
      }
    } else {
      status = this.status.FAIL;
    }
    result = {
      status,
      value,
    };
    callback && callback.call(this, result.status, result.value);
    return result;
  },
  // Delete data
  remove(key, callback) {
    var status = this.status.FAIL,
      key = this.getKey(key),
      value = null;
    try {
      value = this.storage.getItem(key);
    } catch (e) {}
    if (value) {
      try {
        this.storage.removeItem(key);
        status = this.status.SUCCESS;
      } catch (e) {}
    }
    // The operation is successful and the real data is returned
    callback &&
      callback.call(
        this,
        status,
        status > 0
          ? null
          : value.slice(value.indexOf(this.timeSign) + this.timeSign.length)
      );
  },
};

var ls = new BaseLocalStorage("ls_", "---");
ls.set("a", "a de value", (...list) => {
  console.log(list); // [0, "ls_a", "a de value"]
});
ls.get("a", function () {
  console.log(arguments); // Arguments(2) [0, "a de value", callee: ƒ, Symbol(Symbol.iterator): ƒ]
});
ls.remove("a", function () {
  console.log(arguments); // Arguments(2) [0, "a de value", callee: ƒ, Symbol(Symbol.iterator): ƒ]
});
ls.remove("a", function () {
  console.log(arguments); // Arguments(2) [1, null, callee: ƒ, Symbol(Symbol.iterator): ƒ]
});
ls.get("a", function () {
  console.log(arguments); // Arguments(2) [1, null, callee: ƒ, Symbol(Symbol.iterator): ƒ]
});
ls.set(
  "b",
  "b value",
  (...list) => {
    console.log(list); // [0, "ls_b", "b value"]
  },
  Date.now() + 100
);
ls.get("b", (...list) => {
  console.log(list); // [0, "b value"]
});
setTimeout(() => {
  ls.get("b", (...list) => {
    console.log(list); // [3, null]
  });
}, 200);

Throttle mode

core

  • Definition: performs throttling control on duplicate business logic, performs the last operation and cancels other operations to improve performance

  • Application: page scrolling, mouse click / slide, image loading, data batch upload

  • Restrictor

// Throttler: clear the function to be executed and delay the execution of the latest function
var throttle = function () {
  // Callback function
  let fn;
  // The first argument is a Boolean value (, the second argument is a function)
  if (typeof arguments[0] === "boolean") {
    fn = arguments[1];
    fn._timer && clearTimeout(fn._timer);
  }
  // The first parameter is the function, and the second parameter is the function execution parameter
  else {
    fn = arguments[0];
    var p = Object.assign(
      {
        context: null, // Function execution scope
        args: [],
        time: 300,
      },
      arguments[1]
    );
    // Self executing function, clearing the previous timer
    arguments.callee(true, fn);
    // Delay execution with timer
    fn._timer = setTimeout(() => {
      fn.apply(p.context, p.args);
    }, p.time);
  }
};

Example 1: return to the top

  • Requirement: when page scrolling stops, add animation to the back to top button
  • js
function moveScroll() {
  var top = $(document).scrollTop();
  console.log(top);
  $("#back").animate({ top: top + 100 }, 400, "easeOutCubic");
}

$(window).on("scroll", function () {
  // After the slide is completed, call moveScroll (within a certain time).
  throttle(moveScroll);
});
  • html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="https://code.jquery.com/jquery-3.6.0.js" integrity="sha256-H+K7U5CnXl1h5ywQfKtSj8PCmoN9aaq30gDh27Xc0jk="
    crossorigin="anonymous"></script>
  <script src="https://cdn.bootcdn.net/ajax/libs/jquery-easing/1.4.1/jquery.easing.min.js"></script>
  <style>
    .content {
      margin: 10px;
      height: 1500px;
      border: 1px solid #ddd;
    }

    #back {
      position: fixed;
      right: 40px;
      top: 40px;
      width: 40px;
      height: 40px;
      border: 1px solid #ddd;
    }
  </style>
</head>

<body>
  <div class="content"></div>
  <span id="back"></span>
  <script src="./index.js"></script>
</body>

</html>
  • effect

Example 2: floating layer optimization

  • Demand: optimize the floating layer. When the mouse moves to the container, the layer is displayed, and when it moves to li (icon), the displayed picture is switched
  • js
// Demand: optimize the floating layer. When the mouse moves to the container, the layer is displayed, and when it moves to li (icon), the displayed picture is switched
// Use throttling mode to optimize pop-up changes caused by careless move in and removal
function $(id) {
  return document.getElementById(id);
}

function $tag(tag, container = document) {
  return container.getElementsByTagName(tag);
}

// Floating layer
var Layer = function (id) {
  // container
  this.container = $(id);
  // Floating layer in vessel
  this.layer = $tag("div", this.container)[0];
  this.lis = $tag("li", this.container);
  this.imgs = $tag("img", this.container);
  this.bindEvent();
};
Layer.prototype = {
  bindEvent() {
    var that = this;
    // Hidden floating layer
    function hide() {
      that.layer.className = "";
    }
    // Show floating layer
    function show() {
      that.layer.className = "show";
    }
    // Displayed when container enter s and not displayed when leave
    this.on(this.container, "mouseenter", function () {
      // Clear hidden floating layer method timer
      throttle(true, hide);
      // Delayed display floating layer method
      throttle(show);
    }).on(this.container, "mouseleave", function () {
      throttle(true, show);
      throttle(hide);
    });
    for (var i = 0; i < this.lis.length; i++) {
      this.lis[i].index = i;
      // Hide all pictures first, and then show the pictures of hover
      this.on(this.lis[i], "mouseenter", function () {
        var index = this.index;
        for (var j = 0; j < that.imgs.length; j++) {
          that.imgs[j].className = "";
        }
        that.imgs[index].className = "show";
      });
    }
  },
  on(ele, type, fn) {
    // Abbreviation
    ele.addEventListener(type, fn, false);
    return this;
  },
};
  • html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    img {
      display: none;
      width: 100px;
      height: 100px;
      border: 1px solid #ddd
    }

    li {
      border: 1px solid #ddd
    }

    #icon {
      border: 1px solid #ddd
    }

    #icon div {
      display: none;
      padding: 10px;
      border: 1px solid #ddd
    }

    .show {
      display: block !important;
    }
  </style>
</head>

<body>
  <div id="icon" class="icon">
    <ul class="icon">
      <li>hover I show wechat pictures</li>
      <li>hover I show microblog pictures</li>
      <li>hover I show other pictures</li>
    </ul>
    <div>
      <img class="show" src="img/1.png" alt="I'm wechat picture" />
      <img src="img/2.png" alt="I'm a microblog picture" />
      <img src="img/3.png" alt="I'm another picture" />
    </div>
  </div>
  <script src="./index.js"></script>
</body>

</html>
  • effect

Example 3: delayed loading of pictures

  • Demand: delayed loading of pictures (excessive loading of pictures will affect the loading of pages. The pictures of viewports shall be loaded first)
  • js
// Demand: delayed loading of pictures (excessive loading of pictures will affect the loading of pages. The pictures of viewports shall be loaded first)

class LazyLoader {
  constructor(id) {
    this.container = document.getElementById(id);
    // Get all picture elements
    this.imgs = this.getImgs();
    this.init();
  }
  // initialization
  init() {
    this.update();
    this.bindEvent();
  }
  // Get delayed loaded pictures
  getImgs() {
    return Array.from(this.container.getElementsByTagName("img"));
  }
  // Load picture
  update() {
    if (!this.imgs.length) return;
    var i = this.imgs.length - 1;
    // Traverse from back to front to optimize deletion performance
    for (; i >= 0; i--) {
      if (this.shouldShow(i)) {
        // Picture format < img SRC = "display picture in loading" data SRC = "actual picture" / >
        this.imgs[i].src = this.imgs[i].getAttribute("data-src");
        this.imgs.splice(i, 1);
      }
    }
  }
  // Judge whether the picture is in the viewport (at least one of the upper and lower edges is in the window)
  shouldShow(i) {
    var img = this.imgs[i],
      //Top height of visualization range
      scrollTop = document.documentElement.scrollTop || document.body.scrollTop,
      //Bottom height of visualization range
      scrollBtm = scrollTop + document.documentElement.clientHeight,
      // Picture top position
      imgTop = this.pageY(img),
      imgBtm = imgTop + img.offsetHeight;
    if (
      (imgTop > scrollTop && imgTop < scrollBtm) ||
      (imgBtm > scrollTop && imgBtm < scrollBtm)
    ) {
      return true;
    }
    return false;
  }
  // Get the element ordinate (recursion, accumulate the parent element offsetTop)
  pageY(ele) {
    if (ele.offsetParent) {
      return ele.offsetTop + this.pageY(ele.offsetParent);
    } else {
      return ele.offsetTop;
    }
  }
  // Binding event (simplified version)
  on(ele, type, fn) {
    ele.addEventListener(type, fn);
  }
  // Bind resize and scroll events
  bindEvent() {
    ["resize", "scroll"].forEach((event) => {
      this.on(window, event, () => {
        // context to this Bind this with the update method
        throttle(this.update, { context: this });
      });
    });
  }
}
window.onload = function () {
  new LazyLoader("root");
};
  • html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="../throttle.js"></script>
  <script src="index.js"></script>
  <style>
    div img {
      display: block;
      height: 300px;
    }
  </style>
</head>

<body>
  <div id="root">
    <img src="../imgs/loading.gif" data-src="../imgs/1.jpeg" />
    <img src="../imgs/loading.gif" data-src="../imgs/2.jpeg" />
    <img src="../imgs/loading.gif" data-src="../imgs/3.jpeg" />
    <img src="../imgs/loading.gif" data-src="../imgs/4.jpeg" />
    <img src="../imgs/loading.gif" data-src="../imgs/5.jpeg" />
    <img src="../imgs/loading.gif" data-src="../imgs/6.jpeg" />
    <img src="../imgs/loading.gif" data-src="../imgs/7.jpeg" />
    <img src="../imgs/loading.gif" data-src="../imgs/8.jpeg" />
    <img src="../imgs/loading.gif" data-src="../imgs/9.jpeg" />
    <img src="../imgs/loading.gif" data-src="../imgs/10.jpeg" />
    <img src="../imgs/loading.gif" data-src="../imgs/11.jpeg" />
    <img src="../imgs/loading.gif" data-src="../imgs/12.jpeg" />
  </div>
</body>

</html>
  • effect

Example 4: batch upload statistics

  • Requirements: optimize statistical packaging. Statistics are put into the array and uploaded in batches.
  • js
// Requirements: optimize statistical packaging. Statistics are put into the array and uploaded in batches.

// Package statistics object

var LogPack = (function () {
  var data = [], // Request cache array
    MaxNum = 10, // Request cache maximum
    itemSplitStr = "|", //Key value pair and key value pair spacer
    keyValueSplitStr = "*", // Key and value spacer
    img = new Image(); // Send a get request through img's src and report the data

  // Send request
  function sendLog() {
    var log = "",
      sendData = data.splice(0, MaxNum);
    // Traversal data list
    for (let i = 0; i < sendData.length; i++) {
      log += `log${i}=`;
      // Traversing object key value pairs
      for (let j in sendData[i]) {
        log += j + keyValueSplitStr + sendData[i][j] + itemSplitStr;
      }
      // Remove the last one (write dead?), Use & to connect query parameters
      log = log.replace(/\|$/, "") + "&";
    }
    log += "logLen=" + sendData.length;
    img.src = "xx.gif?" + log;
  }

  return function (param) {
    // No parameter call indicates that the data is reported directly
    if (!param) {
      sendLog();
      return;
    }
    // Add data
    data.push(param);
    data.length >= MaxNum && sendLog();
  };
})();

// Delegation mode
document.getElementsByTagName("div")[0].onclick = function (e) {
  const { nodeName, innerHTML, dataset } = e.target;
  if (nodeName === "BUTTON") {
    LogPack({
      id: dataset.id,
      context: innerHTML,
      type: "click",
    });
  }
};
// Direct transmission
document.getElementById("btn").onclick = function () {
  LogPack();
};
  • html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    div {
      margin-bottom: 10px;
    }
  </style>
</head>

<body>
  <div>
    <button class="btn" data-id="1">Statistics 1</button>
    <button class="btn" data-id="2">Statistics 2</button>
    <button class="btn" data-id="3">Statistics 3</button>
    <button class="btn" data-id="4">Statistics 4</button>
  </div>
  <button id="btn">send out</button>
  <script src="index.js"></script>
</body>

</html>
  • effect

Topics: Design Pattern