Javascript: использование шаблона Null object pattern, RORO, undefined и т. Д. В качестве возвращаемого параметра для функции Get в случае, если запрошенный объект «not found» - PullRequest
0 голосов
/ 01 сентября 2018

Прежде всего, я не уверен, принадлежит ли тема StackOverflow или нет. Тем не менее, я нашел некоторые обсуждения относительно других языков программирования, которые затрагивают тему StackOverflow, но не касаются Javascript. Итак, я просто выброшу свой вопрос на ринг:

Каков наилучший способ / шаблон для обработки возможного сценария «не найден» в гипотетической функции GetUserByUsername в Javascript.

Почему я спрашиваю? Я хочу следовать определенному стандарту кодирования на протяжении всего проекта.

Мой более конкретный сценарий следующий:

  • У меня есть класс User
  • У меня есть список пользователей, который содержит экземпляры Класс User
  • Я ищу определенного пользователя, но этот пользователь не существует в списке

Как мне справиться с ситуацией «не найти» в моей функции?

Я нашел следующие подходы:

  • Возврат не определен
  • RORO (получить объект, вернуть объект)
  • Шаблон нулевого объекта
  • Выбрасывание пользовательской ошибки

Так что я имею в виду именно. Взяв следующий пример класса User.

module.exports = class User {
  constructor() {   
    this.userName = null,
    this.name = null,
    this.age = null
  }
  ...
  getName() {
    console.log(this.name);
  }
}

Где-то в моем коде у меня есть функция, которая предоставляет функцию возврата определенного пользователя по его имени из списка пользователей. Этот список является массивом, состоящим из экземпляров класса User.

Простая реализация может выглядеть так:

{
  ...

  getUserByUsername(pUserName) {
    /* listOfUsers array with objects of the class User */
    var lUser = this.listOfUsers.find(user => user.userName === pUserName);
    return lUser;
  }
  ...
}

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

Как я мог справиться с этим?

1.) Возврат не определен - как в примере кода.

/* We expect "Unknown" is not a user name in that list*/
var lRequestedUser = getUserByUsername("Unknown");

/* Check if requested user is undefined. Note: I'm using lodash for staff like that */
if(_.undefined(lRequestedUser)) {
  //Handling 
}

lRequestedUser.getName();

2.) RORO (получить объект, вернуть объект) Я передаю объект своей функции и возвращаю объект как результат. Внутри объекта, который я возвращаю, у меня есть индикатор, показывающий, прошла ли операция успешно.

getUserByUsername(pParams) {
  if(_.undefined(pParams.userName)) {
    //Throw custom error 
  }

  var lReturnObject = {
    isSuccessfull: false,
    data: null
  }

  /* listOfUsers array with objects of the class User */
  var lUser = this.listOfUsers.find(user => user.userName === pParams.userName);

  if(!(_.undefined(lUser))) {
    lReturnObject.isSuccessfull = true;
    lReturnObject.data = lUser
  }
  return lUser;
}
...
}

/* 
Building a object which is passed to the function. Username is a member of that object
 */
var lRequest = {
  userName: "Unknown"
}

/* We expect "Unknown" is not a user name in that list*/
var lRequestedUser = getUserByUsername(lRequest);

if(!(lRequestedUser.isSuccessfull)) {
//Handling 
}

lRequestedUser.data.getName();

3.) Шаблон нулевого объекта Что мне не нравится в этом решении, так это то, что мне всегда нужно улучшать класс для нулевых объектов, если основной класс получает дополнительную функцию.

  module.exports = class NullUser {
      constructor() {   
        this.userName = null,
        this.name = null,
        this.age = null
      }
      ...
      getName() {
        console.log(this.name);
      }
    }


{
  ...

  getUserByUsername(pUserName) {
    /* listOfUsers array with objects of the class User */
    var lUser = this.listOfUsers.find(user => user.userName === pUserName);

    if(_.undefined(lUser)) {
      return new NullUser();
    }

    return lUser;
  }
  ...
}


/* We expect "Unknown" is not a user name in that list*/
var lRequestedUser = getUserByUsername("Unknown");
lRequestedUser.getName();

4.) Выбрасывание пользовательской ошибки

{
  ...
  getUserByUsername(pUserName) {
    /* listOfUsers array with objects of the class User */
    var lUser = this.listOfUsers.find(user => user.userName === pUserName);

    if(_.undefined(lUser)) {
      throw new CustomError(...);
    }

    return lUser;
  }
  ...
}

/* We expect "Unknown" is not a user name in that list*/
try {
  var lRequestedUser = getUserByUsername("Unknown");
  lRequestedUser.getName();
} catch (e) {
  if (e instanceof CustomError) {
      //Handling
  }
}

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

То есть какие-нибудь мысли, которые были бы "лучшим" решением или способом, который я бы предпочел?

Ответы [ 2 ]

0 голосов
/ 01 сентября 2018

Лично я бы порекомендовал # 1. Есть несколько причин:

  1. Он отражает поведение других примитивов в (доступ к элементам массивов / хэшей, результат find и т. Д.), Поэтому другие разработчики Javascript будут знакомы с этим шаблоном.
  2. Сам ответ по своей сути является фальшивым, что позволяет очень кратко проверить случай «не найден». В этом случае, поскольку вы ожидаете объект (а не число, где 0 - фальси), это хороший шаблон.
  3. Шаблон нулевого объекта более громоздок. В любом случае, пользователи должны знать, как проверить объект на случай «не найден», и эта проверка будет очень специфичной для реализации нулевого объекта. Было бы лучше использовать языковой примитив, чтобы сформулировать, что ничего не существует.
  4. Я не считаю RORO существенно отличным от нулевых объектов. Пользователь по-прежнему должен знать, как проверить ответ, и RORO подразумевает, что возвращаемое значение само по себе не является полезной вещью ... вместо этого фактический ответ, который он ищет, встроен где-то в ответ.
  5. Исключения следует использовать для действительно "исключительных" случаев. Весьма разумно, чтобы список не содержал то, что вы ищете, так что это на самом деле не исключение. Кроме того, исключения требуют больше кода для обработки. Они хороши для определенных вещей, но это действительно не одна из них.
0 голосов
/ 01 сентября 2018

Ответ, на мой взгляд, зависит! Все подходы, которые вы упомянули, имеют конкретные варианты использования, где каждый из них доминирует. Давайте возьмем один за другим. (И критик в конце)

Асинхронное приложение с обратными вызовами

Хотя обратные вызовы рассматриваются как старые и не модные, они дают нам гибкость того, что я могу назвать «множественными возвращаемыми значениями».

getUserByName(name, callback) {
    // Caller can check the `err` and `status` to decide what to do next. 
    return callback(err, status, user);
}
  • Здесь выдача ошибок не будет работать должным образом. То есть вы не можете отловить сгенерированные ошибки с помощью try-catch. Конечно, вы можете «вернуть» пользовательскую ошибку. Но это кажется бессмысленным, имея возможность вернуть «статус» отдельно.
  • Возвращение нулевого объекта или RORO не имеет смысла с дополнительным параметром status. Вы можете предпочесть избавиться от status в пользу нулевого объекта или RORO.

Асинхронное приложение с обещаниями / async-await

Здесь будут выбрасывать ошибки. Если вы используете async-await, перехват try-catch получит ошибку. Или, если вы используете обещания как есть, выгода цепочки обещаний получит.

Синхронное приложение

Бросание ошибок и использование try-catch работает, как и ожидалось. Тем не менее, это будет редкий случай в реальном приложении, которое использует любую стороннюю службу.

Однако , существует проблема с использованием исключений в конце концов. Хотите ли вы рассматривать сценарий «без данных» как ошибку или нет, зависит от бизнес-логики.

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

Таким образом, решением этой проблемы будет использование пользовательской ошибки. С другой стороны, в вызываемом и вызывающем абоненте будет слишком много кода котельной пластины. И это может даже вызвать путаницу в семантическом значении логики. С другой стороны, слишком частое использование пользовательских ошибок может вызвать проблемы несогласованности и со временем привести к сложному обслуживанию приложения. (Это полностью зависит от того, как развивается приложение. Например, если разработчики часто меняются, разные люди будут иметь разные мнения о том, какими должны быть эти пользовательские ошибки)


Мнение ответа

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

Примечание: это (самоуверенное) решение основано на таких фактах, как

  1. Требуется количество кода для шаблона
  2. Семантическое значение кода
  3. Легко для нового разработчика начать работу над приложением
  4. ремонтопригодность

Не используйте ошибки для обработки сценариев без данных. вместо этого, проверьте явно для «пустых» данных и обрабатывать случай отдельно. Я буду образец RORO здесь, с небольшой разницей. Я возвращаю объект, но получаю параметры вместо объекта. Это зависит от количества параметров. Если число параметров слишком велико, то может иметь смысл получить объект. Но, по моему мнению, это может быть признаком плохого решения для бизнеса / архитектуры .

Рассмотрим следующий пример входа пользователя.

  1. Если при входе возникает ошибка, я показываю конечному пользователю «ошибочное» состояние.
  2. Если пользователь был успешно выбран, я регистрирую пользователя в
  3. Если пользователя там нет, я создаю нового пользователя. (при условии, что у меня достаточно данных для запроса само по себе)

Псевдокод для этого может выглядеть так:

// Controller code
async signIn(name, email) {
  try {
    let user = await userDao.getUserByUserName(name);
    if (!user.exists) { 
       user = await userDao.createUser(name, email);
    }
    return user; 
  } catch(e) {
    // handle error
  }
}

или как это,

    // Controller code
async signIn(name, email) {
  try {
    let result = await userDao.getUserByUserName(name);
    if (!result.user) { 
       result = await userDao.createUser(name, email);
    }
    return result.user; 
  } catch(e) {
    // handle error
  }
}

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

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

...