Чи потрібно перевіряти використання всього модуля або просто аргументи публічних методів?


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на наступному рядку. То чому перевірка аргументів настільки особлива?

Не перевіряйте використання модуля. Дуже непопулярна думка, ви можете пояснити чому?


Перевірки слід проводити під час призначення місця, щоб забезпечити збереження інваріантів.
Василевс

@Basilevs Цікаво ... Це з ідеології Code Contracts чи щось старе? Ви можете порекомендувати щось прочитати (пов'язане з вашим коментарем)?
астеф

Це основний поділ проблем. Усі ваші справи охоплені, а дублювання коду мінімальне, а обов'язки чітко визначені.
Василевс

@Basilevs Отже, не перевіряйте поведінку інших модулів взагалі, а лише перевіряйте власні інваріанти стану. Звучить розумно. Але чому я не бачу такої простої квитанції у відповідних питаннях щодо перевірки аргументів?
астеф

Ну, деякі бехеворіальні перевірки все ще потрібні, але їх слід виконувати лише на фактично використаних значеннях, а не на тих, які передаються в інше місце. Наприклад, ви покладаєтесь на виконання списку, щоб перевірити помилки OOB, на відміну від перевірки індексу в коді клієнта. Зазвичай вони є низькими рамковими відмовами, і їх не потрібно випускати вручну.
Basilevs

Відповіді:


2

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

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

Розглянемо наступні принципи:

  • Здоровий глузд
  • Збій швидко
  • СУХИЙ
  • SRP

Визначення

  • Компонент - блок, що забезпечує API
  • Клієнт - користувач API компонента

Стан, що змінюється

Проблема

В мовах імперативу, симптом помилки та її причина можуть бути розділені годинами важкого підйому. Державна корупція може приховувати себе і мутувати, що призводить до незрозумілих невдач, оскільки перевірка поточного стану не може виявити повний процес корупції і, отже, походження помилок.

Рішення

Кожна зміна держави повинна бути ретельно продумана і перевірена. Один із способів подолати стан, що змінюється, - це звести його до мінімуму. Це досягається:

  • тип системи (декларації const та кінцевих членів)
  • введення інваріантів
  • перевірка кожної зміни стану компонента через загальнодоступні 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();            
    }
}

Підсумок

Стан клієнта складається з значень власних полів та частин стану компонента, які не охоплені його власними інваріантами. Перевірку слід проводити лише до фактичної зміни стану клієнта.


1

Клас відповідає за власну державу. Тож підтверджуйте настільки, що він утримує або ставить речі у прийнятний стан.

Якщо модуль буде використаний неправильно, ми хочемо негайно кинути виняток замість будь-якої непередбачуваної поведінки.

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

(привіт, любителі ін'єкційних наркотиків!)

Але хоч я йду через тінь долини параметрів, я не буду боятися аргументів. До попереднього я також максимально використовую параметри за замовчуванням та необов'язкові параметри.

Все вищесказане дозволяє внутрішній обробці продовжувати роботу. У конкретній програмі я маю десятки методів у кількох класах із лише одним місцем, де викидається виняток. Навіть тоді це не через нульові аргументи або через те, що я не міг продовжувати обробку, тому що код в результаті створив об'єкт "нефункціональний" / "null".

редагувати

цитую мій коментар цілком. Я думаю, що дизайн не повинен просто "відмовлятися", коли стикається з "null". Особливо з використанням композитного об’єкта.

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

завершити редагування


1
Якщо не додати недійсний елемент колекції, це дуже непередбачувана поведінка.
Basilevs

1
Якщо всі інтерфейси будуть розроблені таким толерантним способом, одного дня через банальну помилку програми ненароком розбудять і знищать людство.
астеф

Тут ми забуваємо ключові поняття / припущення - encapsulation& single responsibility. nullПісля першого шару, що взаємодіє з клієнтом, практично немає перевірки. Код є <strike> толерантним </strike> надійним. Класи розроблені з умовами за замовчуванням, і тому вони працюють, не пишучи так, ніби взаємодіючий код є помилковим, несанкціонованим мотлохом. Складеному батькові не потрібно дотягувати дочірні шари, щоб оцінити обгрунтованість (і, як наслідок, перевірити, чи немає nullу всіх куточках). Батько знає, що означає стан за замовчуванням дитини
radarbob
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.