Чи є якісь принципи ОО, які практично застосовні для Javascript?


79

Javascript - це об'єктно-орієнтована мова, заснована на прототипі, але вона може стати класною на різних способах:

  • Написання функцій, які слід використовувати як класи самостійно
  • Використовуйте вишукану систему класів у рамках (наприклад, mootools Class.Class )
  • Створіть це з Coffeescript

На початку я прагну писати код на основі класу в Javascript і дуже покладався на нього. Однак останнім часом я використовую рамки Javascript і NodeJS , які відходять від цього поняття класів і більше покладаються на динамічний характер коду, наприклад:

  • Асинхронізуйте програмування, використовуючи та записуючи код написання, який використовує зворотні дзвінки / події
  • Завантаження модулів за допомогою RequireJS (щоб вони не просочилися до глобального простору імен)
  • Концепції функціонального програмування, такі як розуміння списку (карта, фільтр тощо)
  • Між іншим

Я зібрав дотепер те, що більшість принципів та моделей ОО, які я читав (такі як шаблони SOLID та GoF), були написані для мов OO на основі класів, як Smalltalk та C ++. Але чи є які-небудь з них застосовні для мови, заснованої на прототипі, наприклад Javascript?

Чи існують якісь принципи чи зразки, які характерні лише для Javascript? Принципи, щоб уникнути зворотного виклику , злого евалу чи будь-яких інших анти-шаблонів тощо.

Відповіді:


116

Після багатьох редагувань ця відповідь стала чудовиськом у довжину. Прошу вибачення заздалегідь.

По-перше, eval()це не завжди погано, і може принести користь у продуктивності, наприклад, якщо використовується при ледачій оцінці. Lazy-оцінка схожа на lazy-load, але ви по суті зберігаєте свій код у рядках, а потім використовуєте 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заяв більше немає .

Контрольний потік:

Як бачите, керування генераторами не є автоматичним. Кожне потрібно продовжувати вручну. Ось чому використовуються бібліотеки управління потоками типу 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. Конвенція використовує OOP за лаштунками, коли це необхідно, але це спрощує все.

Візьмемо для прикладу систему перегляду (клієнт або сервер).

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 дійсно світить у додатках Node.js через мову, що об'єднує, але він може працювати в будь-якій мові. Дуже добре працює в додатках, іграх у реальному часі тощо.

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

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

Спостерігач

Це може бути суб'єктивним, оскільки деякі люди вважають, що модель спостерігача спостерігається як pub / sub, але вони мають свої відмінності.

"Спостерігач - це схема дизайну, де об'єкт (відомий як суб'єкт) підтримує список об'єктів залежно від нього (спостерігачів), автоматично повідомляючи їх про будь-які зміни стану." - Шаблон спостерігача

Шаблон спостерігачів - це крок за межі типових паб / підсистем. Об'єкти мають суворі стосунки або способи спілкування один з одним. Об'єкт "Тема" зберігатиме список утриманців "Спостерігачі". Тема постійно оновлюватиме своїх спостерігачів.

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

Реактивне програмування - це менша, більш невідома концепція, особливо в 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 ('ім'я', 'Bob');

Він повторно виведе відображення console.log Hello Bob. Основний приклад, але ви можете застосувати цю методику до моделей даних та транзакцій у реальному часі. За цим протоколом можна створити надзвичайно потужні системи.

Метеори ...

Реактивна структура та модель спостерігача досить схожі. Основна відмінність полягає в тому, що модель спостерігача зазвичай описує потік даних цілими об'єктами / класами проти реактивного програмування, а натомість описує потік даних до певних властивостей.

Метеор - прекрасний приклад реактивного програмування. Виконання часу трохи складніше через відсутність у JavaScript JavaScript подій зміни цінності (проксі-сервери Harmony змінюють це). Інші рамки на стороні клієнта, Ember.js і AngularJS, також використовують реактивне програмування (певною мірою).

На пізніх двох фреймворках найчастіше використовується реактивний малюнок, особливо в їхніх шаблонах (тобто автоматичне оновлення). Angular.js використовує просту техніку брудної перевірки. Я б не назвав це точно реактивним програмуванням, але це близько, оскільки брудна перевірка не в режимі реального часу. Ember.js використовує інший підхід. Використання Ембер set()та get()методи, що дозволяють їм негайно оновлювати залежні значення. З їх біговим колесом він надзвичайно ефективний і дозволяє отримати більш залежні значення, де кутовий має теоретичну межу.

Обіцянки

Не виправляє зворотні дзвінки, але знімає деякий відступ і зводить вкладені функції до мінімуму. Це також додає непоганий синтаксис до проблеми.

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 та деякі досить строгі та вдосконалені зразки, які можуть працювати у вашій ситуації. Деякі хороші пости, на які я наголошу:

Інверсія управління

Хоча це не зовсім пов’язано з пекельним зворотним дзвоном, це може допомогти вам загальною архітектурою, особливо в одиничних тестах.

Дві основні підверсії інверсії керування - це впорскування та локатор обслуговування. Мені здається, що Локатор послуг є найпростішим в 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'));

Спрощений приклад. Для реального світу, ефективного використання, вам потрібно буде продовжити цю концепцію, але це може допомогти роз'єднати вашу систему, якщо ви дійсно хочете традиційного введення залежності. Можливо, вам доведеться трохи познайомитися з цією концепцією. Я не дуже багато розмірковував у попередньому прикладі.

Модель-перегляд-контролер

Найбільш очевидний зразок і найбільш використовуваний в Інтернеті. Кілька років тому в JQuery була вся ярость, і так, народилися плагіни JQuery. Вам не потрібна повна рамка на стороні клієнта, просто використовуйте jquery та кілька плагінів.

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

Якщо ви використовуєте традиційні прототипові інтерфейси, вам може бути важко отримати синтаксичний цукор або приємний API при роботі з MVC, якщо ви не хочете виконати ручну роботу. Ember.js вирішує це, створюючи систему "клас" / об'єкт ". Контролер може виглядати так:

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

Більшість бібліотек на стороні клієнта також розширюють шаблон MVC, вводячи помічників перегляду (перетворюючись у представлення) та шаблонів (перетворюючись у представлення).


Нові можливості JavaScript:

Це буде ефективно лише в тому випадку, якщо ви використовуєте Node.js, але, тим не менш, це неоціненно. Ця бесіда в NodeConf від Brendan Eich приносить кілька цікавих нових можливостей. Запропонований синтаксис функції та, особливо, бібліотека Task.js js.

Це, ймовірно, виправить більшість проблем із вкладенням функцій та принесе трохи кращі показники роботи через відсутність накладних функцій.

Я не надто впевнений, чи V8 підтримує це споконвічно, востаннє я перевірив, що вам потрібно включити деякі прапори, але це працює у порту Node.js, який використовує SpiderMonkey .

Додаткові ресурси:


2
Приємний запис. Я особисто не маю користі для МВ? бібліотеки. У нас є все, що потрібно для організації нашого коду для більш складних додатків. Всі вони занадто багато нагадують мені Java та C #, які намагаються кинути свої власні різні фіранки через те, що насправді відбувається в спілкуванні сервер-клієнт. Ми отримали DOM. Ми отримали делегацію подій. Ми отримали ООП. Я можу прив’язати власні події до тивм змін даних.
Ерік Реппен

2
"Замість того, щоб мати велике безладдя у зворотному звороті дзвінків, збережіть одну функцію в одній задачі і виконайте це завдання добре." - Поезія.
CuriousWebDeveloper

1
Javascript, коли через дуже темний вік на початку середини 2000-х років мало хто розумів, як писати великі програми, використовуючи його. Як каже @ErikReppen, якщо ви вважаєте, що програма JS виглядає як програма Java або C #, ви робите це неправильно.
backpackcoder

3

Додавання до відповіді Даніельса:

Спостережувані значення / компоненти

Ця ідея запозичена з MVVM Framework Knockout.JS ( ko.observable ), з ідеєю, що значення та об'єкти можуть бути об'єктами, які можна спостерігати, і коли зміна відбудеться в одному значенні чи об'єкті, воно автоматично оновить усіх спостерігачів. Це в основному схема спостерігача, реалізована в Javascript, і замість того, як реалізується більшість паб / підкадрів, "ключем" є сам предмет замість довільного об'єкта.

Використання полягає в наступному:

// 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 помітна, якщо вам доведеться сильно змінити код, оскільки це легше видалити об'єкти як крок рефакторингу. Я маю на увазі це тому, що як тільки ви видалите тему, всі, хто залежав від неї, не зможуть. Якщо код швидко виходить з ладу, ви знаєте, куди потрібно видалити решту посилань. Це на відміну від повністю роз'єднаної теми (наприклад, з рядковою клавішею в паб / під шаблоні) і має більший шанс залишитися в коді, особливо якщо використовувались динамічні клавіші і програміст технічного обслуговування не знав про це (мертвий код у програмуванні технічного обслуговування - це дратівлива проблема).

У грі програмуванні, це зменшує потребу в Ye Olde шаблон петлі поновлення і багато чого іншого в якості evented / реактивне програмування ідіоми, тому що як тільки що - то змінюється суб'єкт буде автоматично оновлювати всі спостерігач на змінах, без необхідності чекати циклу поновлення виконати. Існує використання для циклу оновлення (для речей, які потрібно синхронізувати з минулим ігровим часом), але іноді просто не хочеться його захаращувати, коли самі компоненти можуть автоматично оновлюватись цим шаблоном.

Фактична реалізація функції, що спостерігається, насправді легко та легко зрозуміти (особливо якщо ви знаєте, як обробляти масиви у 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.