NodeJS (Javascript) Шаблоны проектирования, чтобы избежать асинхронного беспорядка - PullRequest
2 голосов
/ 27 июня 2019

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

    var mysql = require('mysql');
module.exports = class MyClass {

    constructor() {

    }

    async init(){
        await this._initDbConnection();
    }

    _initDbConnection(){
        return new Promise(function(resolve, reject){
            this.db = mysql.createConnection({
                ...
            });
            this.db.connect(function(err) {
                ...    
            });    
        });
    }

    tableExists(tableName){
        return new Promise...            
    }

    createTable(tableName){
        return new Promise...
    }
    async save(data){
        try{
            if( ! (await this.tableExists()) ){
                await this.createTable();
            }
            return new Promise(function(resolve, reject){
                this.db.query(sql, function (err, result) {
                    ...                    
                });
            });
        }
        catch(e){

        }
    }

};

const myclass = new MyClass();
await myclass.init();
await myclass.save();
await 
await 
await !

То же самое для каждого запроса или всего, что выполняет асинхронное выполнение.
Это действительно уродливое решение.
Я имею в виду, что если мне нужно что-то из БД, я хочу подключиться к БД в первой строке, затем выполнить запрос во второй строке и затем обработать результаты в третьей строке. С JS для этого мне нужно создать много обратных вызовов или использовать await в каждой строке ???

Ответы [ 3 ]

1 голос
/ 27 июня 2019

Если что-то является асинхронным, вы должны все равно обработать это либо с помощью «then» async / wait, либо с помощью обратных вызовов. Теперь тот факт, что у вас есть «классы» в JavaScript, не означает, что вы должны их использовать. Я не большой поклонник классов и классической ООП.
Я пишу вещи по-другому ... что-то, что люди не одобряют, но в любом случае такова жизнь. Класс, который вы написали, похоже, не имеет никакого состояния. Я не вижу смысла в использовании класса, но это вопрос предпочтений.
Похоже, это сервисный класс.
Хорошая вещь неиспользования классов - вам не нужно ставить префикс перед всем уродливым «этим» дерьмом. Вы можете написать приведенный выше код в модуле только с функциями.

Также имейте в виду, что вам не нужно явно возвращать Promise, если функция асинхронная

const { log, error } = console;

async function promiseMe(shouldIthrow) {
  if (!shouldIthrow) {
    return 'I Promise you'; //See? no Promise, it will be wrapped in a promise for you
  } else throw Error('I promise an Error')
}

// somewhere else
(async function run() {
  try {
    const result = await promiseMe(false)
    log('Look mum, a promise', result);

  } catch (r) {

  }

})();
// Or "then"
promiseMe(false).then(value => log('Look mum, a promise'));
promiseMe(true).then(_ => { }).catch(e => error('Oh men!'));

Теперь вот как я бы написал код, который вы запрашиваете (на самом деле это рабочий код, хотя и бесполезный)

const db = {
  query: function (sql, callback) {
    //sanitze your sql
    callback && callback({ result: 'database deleted' });
  },
  initConnection: async function () {
    !dbStarted && (dbStarted = true) && (log('DB Started'));
    return db; 
  }
}

function Completer() {
  let resolve, reject;
  const promise = new Promise((res, rej) => {
    resolve = res;
    reject = rej;
  });
  return { resolve, reject, promise };
}

//Higher order function to decorate anything that uses a db
// to ensure there's a db connection 
function withDb(decorated) {
  return async function decorator() {
    await  db.initConnection();
    decorated() 
  }
}
const tableExists = withDb(async function tableExists() {
  log('tableExists');
  return false ///whatever code you need here
});

async function createTable() {
  log('createTable');
  return false ///whatever code you need here
}

function saveHandler(completer){
  return function (data) {
      data.result && completer.resolve(data.result);
      data.error && completer.reject(data.result);
    }
}

async function save(data) {
  try {
    (!await tableExists()) && await createTable();

    const completer = Completer();
    db.query('DROP DATABASE databasename;', saveHandler(completer)); 

    return completer.promise;
  }
  catch (e) {
    //Nah no errors
  }
}

save('blah blah').then(result => { log('[Saved?] oh no:', result) });

// or
(async function run() {
  const result = await save('blah blah');
  log('[Saved?] oh no:', result);
})();
1 голос
/ 27 июня 2019

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

1.Функция инициализации

Как вы уже продемонстрировали в своем коде, это один из способов сделать это.По сути, у вас есть асинхронный метод для инициализации вашего ресурса.Это похоже на функцию .ready() в jQuery.Есть несколько способов написать функцию инициализации.Наиболее простым, вероятно, является принятие обратного вызова, позволяющего вам продолжить свою логику:

class Foo {
    init (callback) {
        connectToDB().then(db => {
            this.db = db;
            callback(this);
        });
    }
}

использование:

let foo = new Foo();
foo.init(async function(){
    await foo.save();
});

2.Шаблон Builder

Этот шаблон проектирования более распространен в мире Java и реже встречается в javascript.Шаблон строителя используется, когда ваш объект нуждается в сложной инициализации.Потребность в асинхронном ресурсе - это как раз та сложность, которая хорошо подходит для шаблона компоновщика:

class Foo {
    constructor (db) {
        if (typeof db === 'undefined') {
            throw new Error('Cannot be called directly');
        }
        this.db = db;
    }

    static async build () {
        let db = await connectToDB();
        return new Foo(db);
    }
}

использование:

Foo.build().then(foo => {
    foo.save();
});

3.Инициализация по требованию / скрытая инициализация

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

class Foo {
    constructor () {
        this.db = null;
    }

    db () {
        if (this._dbConnection !== null) {
            return Promise.resolve(this._dbConnection);
        }
        else {
            return connectToDB().then(db => {
                this._dbConnection = db;
                return db;
            })
        }
    }

    async save (data) {
        let db = await this.db();
        return db.saveData(data);
    }

}

использование:

async function () {
    let foo = new Foo();
    await foo.save(something);  // no init!!
    await foo.save(somethingElse);
}

Бонус

Если вы оглянетесь назадна примере функции init вы увидите, что обратный вызов выглядит как структура управления - вроде while() или if().Это одна из главных особенностей анонимных функций - возможность создавать управляющие структуры.Хорошие примеры этого есть в стандартном javascript, таком как .map() и .forEach() и даже в старом добром .sort().

Вы можете создавать асинхронные управляющие структуры (coalan / async и async-qбиблиотеки являются хорошими примерами этого).Вместо:

if( ! (await this.tableExists()) ) { ...

Вы можете записать это как:

this.ifTableNotExist(()=>{
    return this.createTable();
})
.then(()=>{ ...

возможная реализация:

  ifTableNotExist (callback) {
      return new Promise((ok,err) => {
          someAsyncFunction((table) => {
              if (!table) ok(callback());
          });
      });
  }

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

Бонус за 2-й

В примере для шаблона инициализации по требованию пример использования сохраняет две части данныхпоследовательно с помощью await.Это произошло потому, что код дважды инициализировал соединение с БД, если мы не дожидаемся его завершения.

Но что, если мы захотим ускорить код и выполнить оба сохранения параллельно?Что если мы хотим сделать это:

// Parallel:
await Promise.all([
    foo.save(something),
    foo.save(somethingElse)
]);

Что мы можем сделать, так это то, что мы можем проверить метод .db(), если есть ожидающее обещание:

// method to get db connection:
db () {
    if (this._dbConnection !== null) {
        return Promise.resolve(this._dbConnection);
    }
    else {
        if (this._dbPromise === null) {
            this._dbPromise = connectToDB().then(db => {
                this._dbConnection = db;
                return db;
            })
        }
        return this._dbPromise;
    }
}

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

// method to get db connection:
db () {
    if (this._dbPromise === null) {
        this._dbPromise = connectToDB();
    }
    return this._dbPromise;
}
1 голос
/ 27 июня 2019

db.js

const options = require('../options')
var mysql = require('mysql');

class DataBase {
  constructor(options){
    this.options = options
    this.db = mysql.createConnection(this.options)
  }

  connect(){
    if(this.db.connected){
      return Promise.resolve(this.db)
    }
    return new Promise(function(resolve, reject){
      this.db.connect(function(err) {
        if (err) {
          reject(err);
        } else {
          console.log("Connected to MySQL!");
          resolve(this.db);
        }
      });  
    })
  }
}

module.exports = new Database(options)

index.js

const db = require('./db')

db.connect()

anywhere.js

 const db = require('../db')

 async function(){
   await db.connect()
   db.db.doWhatever()
 }

Очевидно, вам нужны только избыточные средства, ожидайте db.connect () в операциях, которые вы хотите выполнить при запуске, например, в маршрутах вы уже знаете, что он связан с запуском:

routes.js

const db = require('../db').db

app.get('/posts', async(req, res) => {
  const posts = await db.query('select * from posts')
  res.send(posts)
}
...