Не знаете, как улучшить качество кода? Взгляните на эти шаблоны дизайна!

внешний интерфейс Шаблоны проектирования

Цель улучшения качества кода

Работа программистов заключается в написании кода, и написание высококачественного кода должно быть нашим стремлением и нашими собственными требованиями, потому что:

  1. Высококачественный код часто означает меньше ошибок и лучшую модульность, что является основой нашей масштабируемости и возможности повторного использования.
  2. Качественный код также означает лучшее написание, лучшее название, что хорошо для нашего обслуживания.

Какой код считается хорошим качеством

Как определить «хорошее» качество кода, в отрасли существует множество стандартов, в этой статье считается, что хороший код должен иметь следующие характеристики:

  1. Чистота кода, вроде отступов и т. д. Сейчас есть много инструментов, которые автоматически решают эту проблему, например eslint.
  2. Структура регулярная, длинной структуры нет, функции разбиты разумно, не будет функции с несколькими тысячами строк, не будет и десятков функций.if...else. Это требует от человека, пишущего код, некоторого опыта оптимизации, и в этой статье будут представлены несколько шаблонов для оптимизации таких ситуаций.
  3. Легко читается и понимается, и не будет нагроможденийa,b,cТакой вид именования должен быть как можно более семантическим, имена переменных и имена функций должны быть как можно более осмысленными, желательно, чтобы код был комментарием, чтобы другие могли знать, что вы делаете, когда они смотрят на ваш код.

Шаблоны проектирования, представленные в этой статье, в основном策略/状态模式,外观模式,迭代器模式,备忘录模式.

Режим политики/состояния

Базовая структура шаблона стратегии

Если нам нужно сделать калькулятор, нам нужно поддерживать сложение, вычитание, умножение и деление.Для того, чтобы определить, какую операцию нужно выполнить пользователю, нам нужно 4if...elseсудить, если больше операций поддерживается, тоif...elseЭто будет длиннее, не способствует чтению и не элегантно на вид. Таким образом, мы можем использовать шаблон стратегии для оптимизации следующим образом:

function calculator(type, a, b) {
  const strategy = {
    add: function(a, b) {
      return a + b;
    },
    minus: function(a, b) {
      return a - b;
    },
    division: function(a, b) {
      return a / b;
    },
    times: function(a, b) {
      return a * b;
    }
  }
  
  return strategy[type](a, b);
}

// 使用时
calculator('add', 1, 1);

В приведенном выше коде мы заменяем несколько объектов одним объектом.if...else, нужные нам операции соответствуют атрибуту в этом объекте, а имя атрибута соответствует тому, которое мы передали вtype, мы можем напрямую использовать это имя атрибута для получения соответствующей операции.

Базовая структура шаблона состояния

Режим состояния очень похож на режим стратегии.Есть также объект для хранения некоторых стратегий, но также есть переменная для хранения текущего состояния.Мы получаем определенные операции в соответствии с текущим состоянием:

function stateFactor(state) {
  const stateObj = {
    status: '',
    state: {
      state1: function(){},
      state2: function(){},
    },
    run: function() {
      return this.state[this.status];
    }
  }
  
  stateObj.status = state;
  return stateObj;
}

// 使用时
stateFactor('state1').run();

if...elseНа самом деле поведение кода изменяется в зависимости от различных условий, и режим стратегии и режим состояния могут изменять поведение в соответствии с поступающей стратегией или состоянием, поэтому мы можем использовать эти два режима для заменыif...else.

Пример: права доступа

Требование этого примера состоит в том, что наша страница должна отображать разный контент в соответствии с разными ролями, если мы используемif...elseНапишите это так:

// 有三个模块需要显示,不同角色看到的模块应该不同
function showPart1() {}
function showPart2() {}
function showPart3() {}

// 获取当前用户的角色,然后决定显示哪些部分
axios.get('xxx').then((role) => {
  if(role === 'boss'){
    showPart1();
    showPart2();
    showPart3();
  } else if(role === 'manager') {
    showPart1();
    showPart2();
  } else if(role === 'staff') {
    showPart3();
  }
});

В приведенном выше коде мы получаем роль текущего пользователя через API-запрос, а затем кучуif...elseЧтобы определить, какие модули должны отображаться, если ролей много, вотif...elseЭто может быть очень долго, мы можем попробовать оптимизировать с помощью режима состояния:

// 先把各种角色都包装到一个ShowController类里面
function ShowController() {
  this.role = '';
  this.roleMap = {
    boss: function() {
      showPart1();
      showPart2();
      showPart3();
    },
    manager: function() {
      showPart1();
    	showPart2();
    },
    staff: function() {
      showPart3();
    }
  }
}

// ShowController上添加一个实例方法show,用来根据角色展示不同的内容
ShowController.prototype.show = function() {
  axios.get('xxx').then((role) => {
    this.role = role;
    this.roleMap[this.role]();
  });
}

// 使用时
new ShowController().show();

В приведенном выше коде мы переписали модуль прав доступа через режим состояния и удалилиif...else, а отображение разных ролей инкапсулировано вroleMapВнутри будет гораздо удобнее увеличивать или уменьшать потом.

Пример: составное движение

Требование этого примера состоит в том, что теперь у нас есть маленький мяч, и нам нужно управлять его движением.Направление его движения может быть вверх, вниз, влево и вправо или составное движение, такое как верхнее левое и нижнее правое. Если мы также используемif...elseЧтобы написать, эта голова будет написана большими буквами:

// 先来四个方向的基本运动
function moveUp() {}
function moveDown() {}
function moveLeft() {}
function moveRight() {}

// 具体移动的方法,可以接收一个或两个参数,一个就是基本操作,两个参数就是左上,右下这类操作
function move(...args) {
  if(args.length === 1) {
    if(args[0] === 'up') {
      moveUp();
    } else if(args[0] === 'down') {
      moveDown();        
    } else if(args[0] === 'left') {
      moveLeft();        
    } else if(args[0] === 'right') {
      moveRight();        
    }
  } else {
    if(args[0] === 'left' && args[1] === 'up') {
      moveLeft();
      moveUp();
    } else if(args[0] === 'right' && args[1] === 'down') {
      moveRight();
      moveDown();
    }
    // 后面还有很多if...
  }
}

можно увидеть здесьif...elseВидя, что у нас большие головы, давайте воспользуемся режимом стратегии, чтобы оптимизировать его:

// 建一个移动控制类
function MoveController() {
  this.status = [];
  this.moveHanders = {
    // 写上每个指令对应的方法
    up: moveUp,
    dowm: moveDown,
    left: moveLeft,
    right: moveRight
  }
}

// MoveController添加一个实例方法来触发运动
MoveController.prototype.run = function(...args) {
  this.status = args;
  this.status.forEach((move) => {
    this.moveHanders[move]();
  });
}

// 使用时
new MoveController().run('left', 'up')

В приведенном выше коде мы также инкапсулируем все стратегии вmoveHandersвнутри, а затем передать метод экземпляраrunМетод, переданный для выполнения конкретной стратегии.

Внешний вид Режим

Базовая структура

Когда мы разрабатываем модуль, методы внутри могут быть спроектированы так, чтобы они были более тонкими, но при воздействии на внешний мир нет необходимости напрямую раскрывать эти небольшие интерфейсы.Внешним пользователям может понадобиться объединить некоторые интерфейсы для достижения определенной функции. , мы действительно можем организовать это, когда выставляем его. Это похоже на меню в ресторане.Есть много блюд.Пользователи могут заказать одно блюдо по одному,или напрямую заказать комплексное питание.Режим внешнего вида обеспечивает организованный комплексный обед,подобный этому:

function model1() {}

function model2() {}

// 可以提供一个更高阶的接口,组合好了model1和model2给外部使用
function use() {
  model2(model1());
}

Пример: инкапсуляция общего интерфейса

Режим внешнего вида на самом деле очень распространен. Многие модули сложны внутри, но может быть один или два внешних интерфейса. Нам не нужно знать сложные внутренние детали, просто вызовите единый высокоуровневый интерфейс, такой как следующая вкладка модуль:

// 一个选项卡类,他内部可能有多个子模块
function Tab() {}

Tab.prototype.renderHTML = function() {}    // 渲染页面的子模块
Tab.prototype.bindEvent = function() {}    // 绑定事件的子模块
Tab.prototype.loadCss = function() {}    // 加载样式的子模块

// 对外不需要暴露上面那些具体的子模块,只需要一个高级接口就行
Tab.prototype.init = function(config) {
  this.loadCss();
  this.renderHTML();
  this.bindEvent();
}

Режим инкапсуляции приведенного выше кода очень распространен, на самом деле используется и режим внешнего вида, который, конечно, также может раскрывать определенные детали.renderHTML,bindEvent,loadCssЭти подмодули, но внешние пользователи могут не заботиться об этих деталях, просто дают унифицированный высокоуровневый интерфейс на линии, эквивалентный изменению внешнего вида выставленных, так называемых外观模式.

Пример: инкапсуляция метода

Этот пример также очень распространен, который заключается в том, чтобы инкапсулировать некоторые похожие функции в метод, вместо того, чтобы писать его везде. В прошлом, когда IE доминировал в мире, нам нужно было выполнять много совместимой работы, просто обязательное событиеaddEventListener,attachEvent,onclickИ т.д., чтобы каждый раз не было этих проверок, мы можем инкапсулировать их в метод:

function addEvent(dom, type, fn) {
  if(dom.addEventListener) {
    return dom.addEventListener(type, fn, false);
  } else if(dom.attachEvent) {
    return dom.attachEvent("on" + type, fn);
  } else {
    dom["on" + type] = fn;
  }
}

потомaddEventДоступно для внешнего использования.На самом деле мы часто инкапсулируем такие методы в реальном коде, но мы можем не осознавать, что это режим внешнего вида.

шаблон итератора

Базовая структура

Шаблон шаблона итератора очень распространен в JS, и массив поставляется сforEachЭто приложение шаблона итератора, и мы также можем реализовать аналогичную функцию:

function Iterator(items) {
  this.items = items;
}

Iterator.prototype.dealEach = function(fn) {
  for(let i = 0; i < this.items.length; i++) {
    fn(this.items[i], i);
  }
}

В приведенном выше коде мы создаем новый класс итератора, конструктор получает массив, а метод экземпляраdealEachМожет получить обратный вызов на экземпляреitemsКаждый элемент выполняет этот обратный вызов.

Пример: итератор данных

Фактически, многие массивы JS использовали режим итератора, напримерfind,findПолучает тестовую функцию и возвращает первые данные, соответствующие этой тестовой функции. Что делает этот пример, так это расширяет эту функцию и возвращает все элементы данных, которые соответствуют этой тестовой функции, а также могут получать два параметра, первый параметр — это имя атрибута, второй параметр — это значение, а также возвращает все атрибуты и значения. , соответствующие элементы:

// 外层用一个工厂模式封装下,调用时不用写new
function iteratorFactory(data) {
  function Iterator(data) {
    this.data = data;
  }
  
  Iterator.prototype.findAll = function(handler, value) {
    const result = [];
    let handlerFn;
    // 处理参数,如果第一个参数是函数,直接拿来用
    // 如果不是函数,就是属性名,给一个对比的默认函数
    if(typeof handler === 'function') {
      handlerFn = handler;
    } else {
      handlerFn = function(item) {
        if(item[handler] === value) {
          return true;
        }
        
        return false;
      }
    }
    
    // 循环数据里面的每一项,将符合结果的塞入结果数组
    for(let i = 0; i < this.data.length; i++) {
      const item = this.data[i];
      const res = handlerFn(item);
      if(res) {
        result.push(item)
      }
    }
    
    return result;
  }
  
  return new Iterator(data);
}

// 写个数据测试下
const data = [{num: 1}, {num: 2}, {num: 3}];
iteratorFactory(data).findAll('num', 2);    // [{num: 2}]
iteratorFactory(data).findAll(item => item.num >= 2); // [{num: 2}, {num: 3}]

Вышеприведенный код инкапсулирует подобный массивуfindИтератор расширяет свою функциональность, что очень удобно для обработки больших объемов данных аналогичной структуры, возвращаемых API.

режим заметки

Базовая структура

Режим заметки похож на функцию кеша, часто используемую в JS. Он записывает состояние внутри, то есть в кеш. Когда мы снова обращаемся к нему, мы можем напрямую получить кешированные данные:

function memo() {
  const cache = {};
  
  return function(arg) {
    if(cache[arg]) {
      return cache[arg];
    } else {
      // 没缓存的时候先执行方法,得到结果res
      // 然后将res写入缓存
      cache[arg] = res;
      return res;
    }
}

Пример: Кэш статей

Этот пример также распространен в практических проектах: каждый раз, когда пользователь нажимает на новую статью, ему нужно запрашивать данные из API.Если он нажмет на ту же статью в следующий раз, мы можем захотеть напрямую использовать данные, запрошенные в прошлый раз. Запрос, мы можем использовать наш режим заметок в это время, просто используйте вышеприведенную структуру напрямую:

function pageCache(pageId) {
  const cache = {};
  
  return function(pageId) {
    // 为了保持返回类型一致,我们都返回一个Promise
    if(cache[pageId]) {
      return Promise.resolve(cache[pageId]);
    } else {
      return axios.get(pageId).then((data) => {
        cache[pageId] = data;
        return data;
      })
    }
  }
}

В приведенном выше коде для решения этой проблемы используется режим заметок, но код относительно прост, и реальный проект может потребовать более сложных требований, но к этой идее все же можно обратиться.

Пример: прямые и обратные функции

Требование этого примера состоит в том, что нам нужно сделать DIV, который можно перемещать.Пользователь может перемещать DIV по своему желанию, но иногда он может злоупотреблять или сожалеть об этом, и он хочет переместить DIV назад, то есть вернуть состояние в последний раз.При необходимости резервного состояния, конечно, необходимо парное прямое состояние. Мы можем выполнить это аналогичное требование с помощью шаблона меморандума:

function moveDiv() {
  this.states = [];       // 一个数组记录所有状态
  this.currentState = 0;  // 一个变量记录当前状态位置
}

// 移动方法,每次移动记录状态
moveDiv.prototype.move = function(type, num) {
  changeDiv(type, num);       // 伪代码,移动DIV的具体操作,这里并未实现
  
  // 记录本次操作到states里面去
  this.states.push({type,num});
  this.currentState = this.states.length - 1;   // 改变当前状态指针
}

// 前进方法,取出状态执行
moveDiv.prototype.forward = function() {
  // 如果当前不是最后一个状态
  if(this.currentState < this.states.length - 1) {
    // 取出前进的状态
    this.currentState++;
    const state = this.states[this.currentState];
    
    // 执行该状态位置
    changeDiv(state.type, state.num);
  }
}

// 后退方法是类似的
moveDiv.prototype.back = function() {
  // 如果当前不是第一个状态
  if(this.currentState > 0) {
    // 取出后退的状态
    this.currentState--;
    const state = this.states[this.currentState];
    
    // 执行该状态位置
    changeDiv(state.type, state.num);
  }
}

Приведенный выше код записывает все состояния, которыми управляет пользователь, через массив, и пользователь может перемещаться вперед и назад между состояниями в любое время.

Суммировать

Эти шаблоны проектирования, обсуждаемые в этой статье策略/状态模式,外观模式,迭代器模式,备忘录模式Все они хорошо понятны и очень распространены в практической работе, их умелое использование может эффективно уменьшить избыточный код и улучшить качество нашего кода.

  1. 策略模式добавив нашifСнижена политика перезаписи условия в барif...elseКоличество , выглядит более освежающе, и его удобнее расширять.状态模式а также策略模式Очень похоже, но есть еще одно состояние, и по этому состоянию можно выбрать конкретную стратегию.
  2. 外观模式Возможно, мы использовали его непреднамеренно, то есть для инкапсуляции какой-то внутренней логики модуля в интерфейсе более высокого уровня, или для инкапсуляции некоторых подобных операций в метод, чтобы сделать внешние вызовы более удобными.
  3. 迭代器模式Существует множество реализаций массивов JS, и мы также можем имитировать их для выполнения работы по обработке данных, что особенно подходит для обработки большого объема данных с аналогичными структурами, полученными из API.
  4. 备忘录模式Это добавление объекта кеша для записи состояния ранее полученных данных или операций, которые можно использовать для ускорения доступа или отката состояния позже.
  5. Опять же, смысл шаблонов проектирования заключается в понимании идей, и существует множество способов их реализации.

Эта статья является последней статьей для проектирования моделей, первые три статьи:

(500+ лайков!) Не умеете инкапсулировать код? Взгляните на эти шаблоны дизайна!

(100+ лайков!) Создавайте шаблоны в исходном коде фреймворка для улучшения масштабируемости.

Не знаете, как улучшить возможность повторного использования кода? Ознакомьтесь с этими шаблонами проектирования

В конце статьи спасибо, что потратили свое драгоценное время на чтение этой статьи. Если эта статья немного поможет вам или вдохновит, пожалуйста, не скупитесь на лайки и звезды GitHub. Ваша поддержка является движущей силой для автор продолжать творить.

Материал этой статьи взят изNetEase Senior Front-end Engineer Development Micro MajorКурс по шаблонам дизайна от Тан Лея.

Добро пожаловать, чтобы обратить внимание на мой общедоступный номербольшой фронт атакиПолучите высококачественные оригиналы впервые~

Цикл статей "Передовые передовые знания":nuggets.capable/post/684490…

Адрес GitHub с исходным кодом из серии статей «Advanced Front-end Knowledge»:GitHub.com/Денис — см....