Что использовать вместо Исключений при написании кода проверки? - PullRequest
1 голос
/ 06 марта 2019

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

Возникают исключения, но я думаю, что исключения не должны использоваться при проверке пользовательского ввода. Как пишет @Blowdart:

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

From: https://stackoverflow.com/a/77175/125938. Я распространяю это чувство на все «неправильные» данные, вводимые пользователем.

Так что вопрос в том, что использовать вместо исключений. В определенных ситуациях я мог бы просто использовать метод IsValid…, который возвращает bool для достоверности, но что, если я хочу передать сообщение об ошибке вместе с ним? Должен ли я создать собственный объект «ValidationError» со свойством Message? Что имеет смысл и вызывает наименьшее удивление (желательно испытанный и проверенный шаблон)?

Ответы [ 3 ]

1 голос
/ 06 марта 2019

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

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

Допустим, вы задали процедуру входа следующим образом в контексте HTTP:

  1. Получить имя пользователя * и пароль * из запроса.
  2. Получить запись пользователя * по имени пользователя из базы данных *.
  3. Проверьте, равен ли пароль записи * введенному паролю.
  4. Если да, начать сеанс.
  5. Если какой-либо из вышеперечисленных шагов не завершился успешно, выведите соответствующее сообщение об ошибке.

Любой из предметов, отмеченных звездочкой выше , может потерпеть неудачу :

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

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

try {
    username = request.get('username')
    password = request.get('password')
    user = db.get(username=username)
    if (user.password.matches(password)) {
        session.start()
    } else {
        print 'Nope, try again'
    }
} catch (RequestDoesNotHaveThisDataException) {
    logger.info('Invalid request')
    response.status(400)
} catch (UserRecordNotFoundException) {
    print 'Nope, try again'
} catch (UnsupportedHashingAlgorithmException, PasswordIsNullException) {
    logger.error('Invalid password hash for user ' + user.id)
    response.status(500)
    print 'Sorry, please contact our support staff'
} catch (DatabaseDownException e) {
    // mostly for illustration purposes, 
    // this exception should probably not even be caught here
    logger.exception('SEND HALP!')
    throw e
}

Так что, да, это очень простой процесс, но буквально на каждом шагу пути есть один или несколько исключительных случаев . Вы задаете вопрос «какое имя пользователя отправил пользователь в запросе?» , и если на этот вопрос нет ответа, поскольку пользователь не отправил имя пользователя, у вас исключительный случай. Исключения здесь значительно упрощают поток управления, в отличие от попытки охватить каждый из этих случаев if..else.

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

Как видите, мы проверяем, является ли имя пользователя «действительным» или нет, пытаясь извлечь его запись из базы данных. Если у нас есть функция, целью которой является получение записей о пользователях из базы данных, а таких записей нет, то исключением является полностью допустимый ответ. Если мы определили эту функцию для проверки , существует ли такая запись, и null или false является допустимым возвращаемым значением ... отлично. Но в этом случае мы не писали это так, и, честно говоря, это приводит к более простому потоку управления, который я нахожу.

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

Сказав все это, вы можете заметить, что большинство этих случаев, за исключением действительно фатального с базой данных, внешне не приводит к исключению.Компонент здесь ожидает и обрабатывает определенные случаи, которые его подкомпоненты считают исключительными.Этот код здесь задает вопросы и готов обработать Mu в качестве ответа на некоторые из них.То есть общее правило, гласящее, что «исключения не должны использоваться в процессе X, Y или Z, потому что оно не является достаточно исключительным», слишком догматично.От цели каждого отдельного фрагмента кода зависит, является ли исключение оправданным или нет.


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

val = new LoginFormValidator()
val.setDataFromRequest(request)
val.validate()

if (val.isValid) {
    print 'Hurray'
} else {
    print 'You have errors:'

    for (error in val.errors) {
        print error.fieldName + ': ' + error.reason
    }
}

Независимо от того, использует ли этот валидатор внутренние исключения, вам не нужно заботиться об этом, но в конце он сохраняет их все как "Да "или" Нет "результат его внутренних свойств, откуда вы можете взять их в виде совокупности (val.isValid) или по отдельности (for (error in val.errors)).

1 голос
/ 06 марта 2019

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

Методу может потребоваться вернуть данные, а может и нет (подпрограмма в Visual Basic, пустота в Java / C #) - но в обоих случаях я хотел указание на успех / сбой и сообщение о потенциальной ошибке.

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

public (bool Success, string ErrorMessage) DoSomething()
{
    // implementation here
}

или

public (bool Success, someType Value, string ErrorMessage) DoSomething()
{
    // implementation here
}

Если нет, вы можете сделать то, что я сделал (это был c # 5 - так что без значений кортежей) и создать класс результата:

public class Result
{
    public static Result Success()
    {
        return new Result(true, null);
    }

    public static Result Fail(string errorMessage)
    {
        return new Result(false, errorMessage);
    }

    protected Result(bool success, string errorMessage)
    {
        Success = success;
        ErrorMessage = errorMessage;
    }

    public bool Success {get; private set;}
    public string ErrorMessage {get; private set;}  
}

public class Result<T>
{
    public static Result<T> Success(T value)
    {
        return new Result(true, null, value);
    }

    public new static Result<T> Fail(string errorMessage)
    {
        return new Result(false, errorMessage, default(T));
    }

    private Result<T>(bool success, string errorMessage, T value)
        : base(success, errorMessage)
    {
        Value = value;
    }

    public T Value {get; private set;}
}

И используйте это так:

public Result CouldBeVoid()
{
    bool IsOk;
    // implementation

    return IsOk ? 
    Result.Success() : 
    Result.Fail("Something went wrong") ;

}


public Result<int> CouldBeInt()
{
    bool IsOk;
    // implementation

    return IsOk ? 
    Result.Success(intValue) : 
    Result.Fail("Something went wrong") ;
}


var result = CouldBeVoid();
if(!result) 
    // do something with error message

var result = CouldBeInt()

if(result)
    // do something with int value
else
    // do something with error message
1 голос
/ 06 марта 2019

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

Допустим, мы анализируем дату из строки, введенной пользователем.

Мой первый класс инкапсулирует необработанное значение и пытается проанализировать дату (псевдокод):

class TextualDate {
   public TextualDate(string value) {
      // just initialize with the provided value
   }

   public Option<Date> AsDate() {
      // try parsing and either return the date or not
      // the Option<Date> type is here to suggest that the conversion might not succeed
   }
}

Далее у меня будет класс проверки, который создает экземпляр класса TextualDate, вызывает его метод AsDate () и возвращает результат проверки:

class ValidatedDate {
  public ValidatedDate(TextualDate value) {
    // initialize with the provided value
   _textualDate = value;
  }

  private TextualDate _textualDate;

  public ValidationResult Validated {
    var maybeDate = _textualDate.AsDate();
    // see whether we have a date or not
    return new ValidationResult(...);
  }
}

В нашем классе ValidationResult мы можем найти некоторый статуссвойство (OK, Failed), любое сообщение об ошибке, предоставленное напрямую или в качестве ключа для последующего поиска в каталоге сообщений и т. д.

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

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