Нужно ли проверять использование всего модуля или только аргументы открытых методов?

9

Я слышал, что рекомендуется проверять аргументы открытых методов:

Мотивация понятна. Если модуль будет использоваться неправильно, мы хотим немедленно вызвать исключение, а не непредсказуемое поведение.

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

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

Я попытался учесть все эти условия и написать простой модуль с одним методом (извините, не-C # ребята):

public sealed class Room
{
    private readonly IDoorFactory _doorFactory;
    private bool _entered;
    private IDoor _door;
    public Room(IDoorFactory doorFactory)
    {
        if (doorFactory == null)
            throw new ArgumentNullException("doorFactory");
        _doorFactory = doorFactory;
    }
    public void Open()
    {
        if (_door != null)
            throw new InvalidOperationException("Room is already opened");
        if (_entered)
            throw new InvalidOperationException("Double entry is not allowed");
        _entered = true;
        _door = _doorFactory.Create();
        if (_door == null)
            throw new IncompatibleDependencyException("doorFactory");
        _door.Open();
        _entered = false;
    }
}

Теперь безопасно =)

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

Подводя итог, что это правильный путь и почему? Если вы можете выбрать один из вариантов ниже, пожалуйста, ответьте на дополнительные вопросы.

Проверьте использование всего модуля. Нужны ли нам юнит-тесты? Есть ли примеры такого кода? Следует ли ограничивать использование зависимостей в использовании (так как это приведет к большей логике проверки)? Разве не практично переносить эти проверки во время отладки (не включать в выпуск)?

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

Не проверяйте использование модуля. Это довольно непопулярное мнение, вы можете объяснить, почему?

astef
источник
Проверки должны быть сделаны во время назначения поля, чтобы гарантировать, что инварианты сохранены.
Basilevs
@Basilevs Интересно ... Это из идеологии Code Contracts или что-то более старое? Можете ли вы порекомендовать что-нибудь почитать (связанное с вашим комментарием)?
Astef
Это основное разделение интересов. Все ваши дела покрыты, в то время как дублирование кода минимально, а обязанности четко определены.
Васильев
@Basilevs Итак, вообще не проверяйте поведение других модулей, а проверяйте собственные инварианты состояния. Звучит разумно. Но почему я не вижу эту простую квитанцию ​​в связанных вопросах о проверке аргументов?
Astef
Что ж, некоторые поведенческие проверки все еще необходимы, но они должны выполняться только для реально используемых значений, а не тех, которые передаются в другом месте. Например, вы используете реализацию List для проверки ошибок OOB, а не для проверки индекса в клиентском коде. Обычно это низкоуровневые сбои фреймворка, которые не нужно выдавать вручную.
Васильев

Ответы:

2

TL; DR: проверить изменение состояния, полагаться на [действительность] текущего состояния.

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

Рассмотрим следующие принципы:

  • Здравый смысл
  • Терпеть неудачу быстро
  • DRY
  • SRP

Определения

  • Компонент - блок, предоставляющий API
  • Клиент - пользователь API компонента

Изменчивое состояние

проблема

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

Решение

Каждое изменение состояния должно быть тщательно продумано и проверено. Один из способов справиться с изменчивым состоянием - это свести его к минимуму. Это достигается путем:

  • система типов (объявления const и final членов)
  • введение инвариантов
  • проверка каждого изменения состояния компонента через публичные API

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

пример

// Wrong
class Natural {
    private int number;
    public Natural(int number) {
        this.number = number;
    }
    public int getInt() {
      if (number < 1)
          throw new InvalidOperationException();
      return number;
    }
}

// Right
class Natural {
    private readonly int number;
    /**
     * @param number - positive number
     */
    public Natural(int number) {
      // Going to modify state, verification is required
      if (number < 1)
        throw new ArgumentException("Natural number should be  positive: " + number);
      this.number = number;
    }
    public int getInt() {
      // State is guaranteed by construction and compiler
      return number;
    }
}

Повторение и сплоченность ответственности

проблема

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

Решение

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

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

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

пример

class Store {
  private readonly List<int> slots = new List<int>();
  public void putToSlot(int slot, int data) {
    if (slot < 0 || slot >= slots.Count) // Unnecessary, validated by List, only needed for custom error message
      throw new ArgumentException("data");
    slots[slot] = data;
  }
}

class Natural {
   int _number;
   public Natural(int number) {
       if (number < 1)
          number = 1;  //Wrong: client can't rely on argument verification, additional state uncertainity is introduced.  Right: throw new ArgumentException(number);
       _number = number;
   }
}

Ответ

Когда описанные принципы применяются к рассматриваемому примеру, мы получаем:

public sealed class Room
{
    private bool _entered = false;
    // Do not use lazy instantiation if not absolutely necessary, this introduces additional mutable state
    private readonly IDoor _door;
    public Room(IDoorFactory doorFactory)
    {
        // Rely on system null check
        IDoor door = _doorFactory.Create();
        // Modifying own state, verification is required
        if (door == null)
           throw new ArgumentNullException("Door");
        _door = door;
    }
    public void Enter()
    {
        // Room invariants do not guarantee _entered value. Door state is indirectly a part of our state. Verification is required to prevent second door state change below.
        if (_entered)
           throw new InvalidOperationException("Double entry is not allowed");
        _entered = true;     
        // rely on immutability for _door field to be non-null
        // rely on door implementation to control resulting door state       
        _door.Open();            
    }
}

Резюме

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

Basilevs
источник
1

Класс отвечает за свое состояние. Так что проверяйте в той степени, в которой он сохраняет или переводит вещи в приемлемое состояние.

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

Нет, не бросайте исключение, а поставляйте предсказуемое поведение. Следствием государственной ответственности является сделать класс / приложение настолько терпимым, насколько это практически возможно. Например, переходя nullна aCollection.Add()? Только не добавляй и продолжай. Вы получаете nullвход для создания объекта? Создайте нулевой объект или объект по умолчанию. Выше doorуже open? И что, продолжай. DoorFactoryаргумент нулевой? Создать новый. Когда я создаю, у enumменя всегда есть Undefinedчлен. Я свободно использую Dictionarys и enumsопределяю вещи явно; и это имеет большое значение для обеспечения предсказуемого поведения.

(привет, любители инъекций зависимости!)

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

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

редактировать

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

Мы забываем ключевые понятия / предположения здесь - encapsulation& single responsibility. После первого слоя, взаимодействующего с клиентом, проверка нуля практически отсутствует. Код толерантный и надежный. Классы спроектированы с состояниями по умолчанию и поэтому работают без написания, как если бы во взаимодействующем коде было много ошибок, мошенник. Составной родительский элемент не должен обращаться к дочерним слоям для оценки достоверности (и, косвенно, проверять наличие нулевых значений во всех закоулках и закоулках). Родитель знает, что означает состояние ребенка по умолчанию

конец редактирования

radarbob
источник
1
Не добавление недопустимого элемента коллекции - очень непредсказуемое поведение.
Basilevs
1
Если все интерфейсы будут разработаны таким терпимым образом, однажды, из-за банальной ошибки, программы случайно пробудятся и уничтожат человечество.
Астеф
Мы забываем ключевые понятия / предположения здесь - encapsulation& single responsibility. nullПосле первого уровня, взаимодействующего с клиентом, проверки практически отсутствуют . Код <strike> терпимый </ strike> надежный. Классы спроектированы с состояниями по умолчанию и поэтому работают без написания, как если бы во взаимодействующем коде было много ошибок, мошенник. Составной родительский элемент не должен обращаться к дочерним слоям для оценки достоверности (и, как следствие, проверять nullвсе закоулки). Родитель знает, что означает состояние ребенка по умолчанию
радар Боб