Существуют ли какие-либо ОО-принципы, которые практически применимы для Javascript?


79

Javascript - это объектно-ориентированный язык на основе прототипов, но он может быть основан на классах различными способами:

  • Написание функций для самостоятельного использования в качестве классов
  • Используйте изящную систему классов в фреймворке (например, mootools Class.Class )
  • Создайте его из Coffeescript

Сначала я писал код на основе классов в Javascript и очень полагался на него. Однако в последнее время я использую фреймворки Javascript и NodeJS , которые отходят от этого представления о классах и больше полагаются на динамическую природу кода, такую ​​как:

  • Асинхронное программирование, использование и написание кода, который использует обратные вызовы / события
  • Загрузка модулей с RequireJS (чтобы они не попадали в глобальное пространство имен)
  • Концепции функционального программирования, такие как списки (карта, фильтр и т. Д.)
  • Среди прочего

До сих пор я понял, что большинство принципов и шаблонов ОО, которые я прочитал (таких как шаблоны SOLID и GoF), были написаны для таких ОО-языков, как Smalltalk и C ++. Но есть ли какие-либо из них применимы для языка на основе прототипов, таких как Javascript?

Существуют ли какие-либо принципы или шаблоны, специфичные только для Javascript? Принципы, позволяющие избежать обратного вызова ада , зла Эвала или любых других анти-паттернов и т. Д.

Ответы:


116

После многих правок этот ответ стал монстром в длину. Я заранее прошу прощения.

Прежде всего, eval()это не всегда плохо, и может принести выгоду в производительности, например, при использовании отложенной оценки. Ленивая оценка похожа на отложенную загрузку, но вы по существу сохраняете свой код в строках, а затем используете evalили new Functionдля оценки кода. Если вы используете какие-то уловки, то это станет намного полезнее зла, но если вы этого не сделаете, это может привести к плохим вещам. Вы можете посмотреть на мою систему модулей, которая использует этот шаблон: https://github.com/TheHydroImpulse/resolve.js . Resolve.js использует eval вместо того, чтобы в new Functionпервую очередь моделировать CommonJS exportsи moduleпеременные, доступные в каждом модуле, и new Functionоборачивает ваш код в анонимную функцию, хотя в итоге я оборачиваю каждый модуль в функцию, которую я делаю вручную, в сочетании с eval.

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

Генераторы Гармонии

Теперь, когда генераторы наконец-то приземлились в V8 и, следовательно, в Node.js, под флагом ( --harmonyили --harmony-generators). Это значительно уменьшает количество ада обратного вызова, которое у вас есть. Это делает написание асинхронного кода действительно великолепным.

Лучший способ использовать генераторы - это использовать некую библиотеку потоков управления. Это позволит потоку продолжить работу, поскольку вы уступаете в генераторах.

Резюме / Обзор:

Если вы не знакомы с генераторами, это практика приостановки выполнения специальных функций (называемых генераторами). Эта практика называется уступкой с использованием yieldключевого слова.

Пример:

function* someGenerator() {
  yield []; // Pause the function and pass an empty array.
}

Таким образом, всякий раз, когда вы вызываете эту функцию в первый раз, она возвращает новый экземпляр генератора. Это позволяет вам вызывать next()этот объект для запуска или возобновления работы генератора.

var gen = someGenerator();
gen.next(); // { value: Array[0], done: false }

Ты будешь звонить nextдо doneвозвращения true. Это означает, что генератор полностью завершил свое выполнение, и больше нет yieldоператоров.

Control-Flow:

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

Пример:

var co = require('co');

co(function*() {
  yield query();
  yield query2();
  yield query3();
  render();
});

Это дает возможность писать все в Node (и браузере с регенератором Facebook, который принимает в качестве входных данных исходный код, использующий генераторы гармонии и разделяющий полностью совместимый код ES5) в синхронном стиле.

Генераторы все еще довольно новы, и поэтому требуют Node.js> = v11.2. На момент написания этой статьи v0.11.x по-прежнему нестабилен, поэтому многие собственные модули не работают и будут работать до v0.12, где собственный API будет успокаиваться.


Чтобы добавить в мой оригинальный ответ:

Недавно я предпочел более функциональный API в JavaScript. Соглашение использует ООП за кулисами, когда это необходимо, но оно все упрощает.

Возьмите, например, систему просмотра (клиент или сервер).

view('home.welcome');

Гораздо легче читать или следовать, чем:

var views = {};
views['home.welcome'] = new View('home.welcome');

viewФункция просто проверяет , является ли тот же вид уже существует в локальной карте. Если вид не существует, он создаст новый вид и добавит новую запись на карту.

function view(name) {
  if (!name) // Throw an error

  if (view.views[name]) return view.views[name];

  return view.views[name] = new View({
    name: name
  });
}

// Local Map
view.views = {};

Очень простой, правда? Я считаю, что это значительно упрощает общедоступный интерфейс и упрощает его использование. Я также использую способность цепи ...

view('home.welcome')
   .child('menus')
   .child('auth')

Tower, фреймворк, который я разрабатываю (с кем-то еще) или разрабатываю следующую версию (0.5.0), будет использовать этот функциональный подход в большинстве своих интерфейсов.

Некоторые люди используют волокна как способ избежать "ада обратного вызова". Это совершенно другой подход к JavaScript, и я не большой его поклонник, но многие фреймворки / платформы используют его; включая Meteor, так как они обрабатывают Node.js как платформу потоков / соединений.

Я бы предпочел использовать абстрактный метод, чтобы избежать ада обратного вызова. Это может стать громоздким, но это значительно упрощает реальный код приложения. Помогая в создании инфраструктуры TowerJS , она решила многие наши проблемы, хотя, очевидно, у вас все еще будет некоторый уровень обратных вызовов, но вложение не глубокое.

// app/config/server/routes.js
App.Router = Tower.Router.extend({
  root: Tower.Route.extend({
    route: '/',
    enter: function(context, next) {
      context.postsController.page(1).all(function(error, posts) {
        context.bootstrapData = {posts: posts};
        next();
      });
    },
    action: function(context, next) {
      context.response.render('index', context);
      next();
    },
    postRoutes: App.PostRoutes
  })
});

Пример нашей, в настоящее время разрабатываемой, системы маршрутизации и «контроллеров», хотя довольно сильно отличается от традиционных «рельсовых». Но пример очень мощный и сводит к минимуму количество обратных вызовов и делает вещи довольно очевидными.

Проблема этого подхода в том, что все абстрагировано. Ничто не работает как есть, и требует "рамки" позади этого. Но если такого рода функции и стиль кодирования реализованы в рамках, то это огромный выигрыш.

Для шаблонов в JavaScript это, честно говоря, зависит. Наследование действительно полезно только при использовании CoffeeScript, Ember или любой "классовой" инфраструктуры / инфраструктуры. Когда вы находитесь в «чистой» среде JavaScript, использование традиционного интерфейса прототипа работает как чудо:

function Controller() {
    this.resource = get('resource');
}

Controller.prototype.index = function(req, res, next) {
    next();
};

Ember.js начал, по крайней мере для меня, использовать другой подход к конструированию объектов. Вместо того чтобы конструировать каждый метод-прототип независимо, вы бы использовали интерфейс, подобный модулю.

Ember.Controller.extend({
   index: function() {
      this.hello = 123;
   },
   constructor: function() {
      console.log(123);
   }
});

Все это разные стили «кодирования», но добавьте в свою базу кода.

Полиморфизм

Полиморфизм не широко используется в чистом JavaScript, где работа с наследованием и копирование «классовой» модели требует большого количества стандартного кода.

Событийный / Компонентный дизайн

Модели, основанные на событиях и компонентах, являются победителями IMO, или с ними проще всего работать, особенно при работе с Node.js, который имеет встроенный компонент EventEmitter, хотя реализация таких эмиттеров тривиальна, это просто хорошее дополнение ,

event.on("update", function(){
    this.component.ship.velocity = 0;
    event.emit("change.ship.velocity");
});

Просто пример, но это хорошая модель для работы. Особенно в игровом / компонентно-ориентированном проекте.

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

Компонентный дизайн имеет свое применение. Это зависит от того, какой тип системы ваше здание. Я уверен, что он будет работать с веб-приложениями, но он будет работать очень хорошо в игровой среде из-за количества объектов и отдельных систем, но другие примеры наверняка существуют.

Pub / Sub Pattern

Привязка к событию и pub / sub аналогичны. Паттерн pub / sub действительно сияет в приложениях Node.js благодаря унифицированному языку, но он может работать на любом языке. Очень хорошо работает в приложениях, играх и т. Д. В реальном времени.

model.subscribe("message", function(event){
    console.log(event.params.message);
});

model.publish("message", {message: "Hello, World"});

наблюдатель

Это может быть субъективным, так как некоторые люди предпочитают думать о паттерне Observer как о пабе / саб, но у них есть свои отличия.

«Наблюдатель - это шаблон проектирования, в котором объект (известный как субъект) поддерживает список объектов в зависимости от него (наблюдателей), автоматически уведомляя их о любых изменениях состояния». - Шаблон наблюдателя

Наблюдательский паттерн - это шаг за пределы типичных паб / подсистем. Объекты имеют строгие отношения или методы связи друг с другом. Объект «Субъект» будет вести список зависимых «Наблюдателей». Субъект будет держать своих наблюдателей в актуальном состоянии.

Реактивное программирование

Реактивное программирование - это небольшая, более неизвестная концепция, особенно в JavaScript. Есть одна инфраструктура / библиотека (о которой я знаю), которая позволяет легко работать с API для использования этого «реактивного программирования».

Ресурсы по реактивному программированию:

По сути, это набор данных для синхронизации (будь то переменные, функции и т. Д.).

 var a = 1;
 var b = 2;
 var c = a + b;

 a = 2;

 console.log(c); // should output 4

Я считаю, что реактивное программирование в значительной степени скрыто, особенно в императивных языках. Это удивительно мощная парадигма программирования, особенно в Node.js. Метеор создал свой собственный реактивный двигатель, в основе которого лежит основа. Как реактивность Метеора работает за кулисами? отличный обзор того, как это работает внутри.

Meteor.autosubscribe(function() {
   console.log("Hello " + Session.get("name"));
});

Это будет выполняться нормально, отображая значение name, но если мы изменим его

Session.set ('name', 'Bob');

Он выведет на экран файл console.log Hello Bob. Простой пример, но вы можете применить эту технику к моделям данных и транзакциям в реальном времени. Вы можете создавать чрезвычайно мощные системы за этим протоколом.

Метеора ...

Реактивный паттерн и паттерн Observer очень похожи. Основное отличие состоит в том, что шаблон наблюдателя обычно описывает поток данных с целыми объектами / классами, а реактивное программирование описывает поток данных к определенным свойствам.

Метеор - отличный пример реактивного программирования. Его среда выполнения немного сложна из-за отсутствия в JavaScript событий изменения собственных значений (прокси-серверы Harmony изменяют это). Другие клиентские среды, Ember.js и AngularJS, также используют реактивное программирование (в некоторой степени).

Последние две платформы используют шаблон реагирования, особенно в своих шаблонах (то есть автоматическое обновление). Angular.js использует простой метод грязной проверки. Я бы не назвал это точно реактивным программированием, но оно близко, так как грязная проверка не в реальном времени. Ember.js использует другой подход. Использование Ember set()и get()методы, которые позволяют им немедленно обновлять зависимые значения. С их runloop это чрезвычайно эффективно и допускает больше зависимых значений, где угловое имеет теоретический предел.

обещания

Не исправляет обратные вызовы, но убирает некоторые отступы и сводит вложенные функции к минимуму. Это также добавляет хороший синтаксис к проблеме.

fs.open("fs-promise.js", process.O_RDONLY).then(function(fd){
  return fs.read(fd, 4096);
}).then(function(args){
  util.puts(args[0]); // print the contents of the file
});

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

Другой подход заключается в объединении событий и обещаний, когда у вас должна быть функция для соответствующей отправки событий, тогда реальные функциональные функции (те, которые имеют реальную логику внутри них) будут связываться с определенным событием. Затем вы передадите метод диспетчера внутри каждой позиции обратного вызова, однако вам придется выработать некоторые изюминки, которые могут прийти в голову, такие как параметры, зная, какую функцию отправлять и т. Д.

Функция одной функции

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

В конце я бы предложил разработать или использовать небольшую «инфраструктуру», в основном просто основу для вашего приложения, и потратить время на создание абстракций, выбор системы, основанной на событиях, или «загрузки небольших модулей, которые независимая "система. Я работал с несколькими проектами Node.js, где код был чрезвычайно запутан, в частности, в ад-коллбэке, но также не хватало мысли, прежде чем они начали кодировать. Не торопитесь, чтобы обдумать различные возможности с точки зрения API и синтаксиса.

Бен Надель сделал несколько действительно хороших постов в блоге о JavaScript и некоторых довольно строгих и продвинутых шаблонах, которые могут работать в вашей ситуации. Несколько хороших постов, которые я выделю:

Инверсия контроля

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

Две основные подверсии инверсии управления - Внедрение зависимостей и Сервисный локатор. Я считаю Service Locator самым простым в JavaScript, в отличие от внедрения зависимостей. Почему? Главным образом потому, что JavaScript - это динамический язык и статической типизации не существует. Java и C #, в частности, «известны» внедрением зависимостей, потому что вы можете обнаруживать типы, и они имеют встроенные интерфейсы, классы и т. Д. Это довольно просто. Однако вы можете заново создать эту функцию в JavaScript, хотя она не будет идентичной и немного хакерской, я предпочитаю использовать локатор служб внутри своих систем.

Любой вид инверсии контроля резко разделит ваш код на отдельные модули, которые можно смоделировать или подделать в любое время. Разработал вторую версию вашего движка рендеринга? Круто, просто замените старый интерфейс на новый. Локаторы сервисов особенно интересны с новыми прокси-серверами Harmony, однако, эффективно используемыми только в Node.js, они предоставляют более приятный API, а не используют Service.get('render');и вместо Service.render. В настоящее время я работаю над такой системой: https://github.com/TheHydroImpulse/Ettore .

Хотя отсутствие статической типизации (статическая типизация является возможной причиной эффективного использования при внедрении зависимостей в Java, C #, PHP - это не статическая типизация, но у него есть подсказки типов.) Можно рассматривать как отрицательную точку, вы можете определенно превратить это в сильную сторону. Поскольку все динамично, вы можете создать «поддельную» статическую систему. В сочетании с локатором службы каждый компонент / модуль / класс / экземпляр может быть привязан к типу.

var Service, componentA;

function Manager() {
  this.instances = {};
}

Manager.prototype.get = function(name) {
  return this.instances[name];
};

Manager.prototype.set = function(name, value) {
  this.instances[name] = value;
};

Service = new Manager();
componentA = {
  type: "ship",
  value: new Ship()
};

Service.set('componentA', componentA);

// DI
function World(ship) {
  if (ship === Service.matchType('ship', ship))
    this.ship = new ship();
  else
    throw Error("Wrong type passed.");
}

// Use Case:
var worldInstance = new World(Service.get('componentA'));

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

Model-View-Controller

Самая очевидная модель, и наиболее часто используемые в Интернете. Несколько лет назад JQuery был в моде, и поэтому родились плагины JQuery. Вам не нужен полноценный фреймворк на стороне клиента, просто используйте jquery и несколько плагинов.

Сейчас идет огромная война фреймворков на стороне клиента. Большинство из них используют шаблон MVC, и все они используют его по-разному. MVC не всегда реализуется одинаково.

Если вы используете традиционные прототипные интерфейсы, вам может быть трудно получить синтаксический сахар или хороший API при работе с MVC, если вы не хотите выполнять какую-то ручную работу. Ember.js решает эту проблему, создавая систему «класс» / объект ». Контроллер может выглядеть так:

 var Controller = Ember.Controller.extend({
      index: function() {
        // Do something....
      }
 });

Большинство клиентских библиотек также расширяют шаблон MVC, представляя помощники представления (становящиеся представлениями) и шаблоны (становящиеся представлениями).


Новые возможности JavaScript:

Это будет эффективно, только если вы используете Node.js, но, тем не менее, это бесценно. Этот доклад Брендана Айча на NodeConf привнес несколько интересных новинок . Предложенный синтаксис функции, и особенно библиотека Task.js js.

Это, вероятно, решит большинство проблем с вложением функций и принесет немного лучшую производительность из-за отсутствия служебных функций.

Я не слишком уверен, поддерживает ли V8 это изначально, в последний раз я проверял, нужно ли вам включить некоторые флаги, но это работает в порту Node.js, который использует SpiderMonkey .

Дополнительные ресурсы:


2
Хорошая рецензия. Мне лично бесполезно для МВ? библиотеки. У нас есть все необходимое для организации нашего кода для более крупных и сложных приложений. Все они слишком напоминают мне о том, что Java и C # пытаются бросить свои различные занавески в то, что на самом деле происходит в коммуникации сервер-клиент. У нас есть ДОМ. Мы получили делегацию мероприятия. Мы получили ООП. Я могу привязать свои собственные события к изменениям данных tyvm.
Эрик Реппен

2
«Вместо огромного беспорядка в адском обратном вызове, оставьте одну функцию для одной задачи и выполняйте эту задачу хорошо». - Поэзия.
CuriousWebDeveloper

1
Javascript, когда в очень мрачный век в начале и середине 2000-х годов, когда мало кто понимал, как писать большие приложения, используя его. Как говорит @ErikReppen, если вы обнаружите, что ваше JS-приложение выглядит как приложение на Java или C #, вы делаете это неправильно.
рюкзака

3

Добавляем к Дэниелсу ответ:

Наблюдаемые значения / компоненты

Эта идея заимствована из фреймворка MVVM Knockout.JS ( ko.observable ) с идеей, что значения и объекты могут быть наблюдаемыми субъектами, и как только изменение произойдет в одном значении или объекте, оно автоматически обновит всех наблюдателей. В основном это шаблон наблюдателя, реализованный в Javascript, и вместо того, как реализовано большинство фреймворков pub / sub, «ключом» является сам субъект, а не произвольный объект.

Использование заключается в следующем:

// the subjects
// plain old javascript object with observable values
var shipComponent = {
    velocity : observable(0)
};

// the observer, a player user interface
// implemented with revealing module pattern
var playerUi = (function(ship) {

  var module = {
    setVelocity: function (x) { 
      // ... sets the velocity on the player user interface
    },

    // only called once
    init: function() {

      // subscribe to changes on the velocity value
      // using the module's function as callback
      module.velocity.onChange(playerUi.setVelocity);
    }
  };

  return module;
})(shipComponent).init();

// the player ui will change when the velocity value is changed
shipComponent.velocity.set(10);

Идея состоит в том, что наблюдатели обычно знают, где находится предмет и как подписаться на него. Преимущество этого перед пабом / сабом заметно, если вам приходится много менять код, так как с него проще удалять темы в качестве шага рефакторинга. Я имею в виду это, потому что как только вы удалите тему, каждый, кто зависел от нее, потерпит неудачу. Если код не работает быстро, то вы знаете, где удалить оставшиеся ссылки. Это контрастирует с полностью отделенным субъектом (например, со строковым ключом в шаблоне pub / sub) и имеет более высокий шанс остаться в коде, особенно если использовались динамические ключи и программист обслуживания не знал об этом (мертвый) код в программировании обслуживания является раздражающей проблемой).

В игровом программировании это уменьшает потребность в старом шаблоне цикла обновления и т. Д. В идиоме четного / реактивного программирования, поскольку, как только что-то меняется, субъект автоматически обновляет всех наблюдателей при изменении, не ожидая цикла обновления. выполнить. Существуют способы использования цикла обновления (для вещей, которые необходимо синхронизировать с истекшим временем игры), но иногда вы просто не хотите загромождать его, когда сами компоненты могут автоматически обновлять себя с помощью этого шаблона.

Реальная реализация наблюдаемой функции на самом деле удивительно проста для написания и понимания (особенно если вы знаете, как обрабатывать массивы в javascript и шаблоне наблюдателя ):

var observable = function(v) {
    var val = v, subscribers = [];

    // the observable object,
    // as revealing module
    var output = {

        // subscribes to event
        onChange : function(func) {
            // idiomatic JS to add object to the
            // subscribers array
            subscribers.push(func);

            return output: // enables chaining
        },

        // the method that changes the observable object
        // and emits the event
        set : function(v) {
            var i;
            val = v;
            for (i = 0, i < subscribers.length; i++) {
                // this is hardly fault tolerant but as long
                // as subscribers are functions it'll work
                subscribers[i](v);
            }

            return output;
        }

    };

    return output;
};

Я сделал реализацию наблюдаемого объекта в JsFiddle, которая продолжает это с наблюдением компонентов и возможностью удалять подписчиков. Не стесняйтесь экспериментировать с JsFiddle.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.