кодові договори / твердження: що з дублюючими чеками?


10

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

Приклад ситуації: Спочатку я записую наступну функцію

void DoSomething( object obj )
{
  Contract.Requires<ArgumentNullException>( obj != null );
  //code using obj
}

потім через кілька годин я записую ще одну функцію, яка викликає першу. Оскільки в пам’яті все ще свіже, я вирішу не дублювати контракт, оскільки знаю, що вже DoSomethingперевіряю нульовий об’єкт:

void DoSomethingElse( object obj )
{
  //no Requires here: DoSomething will do that already
  DoSomething( obj );
  //code using obj
}

Очевидна проблема: DoSomethingElseтепер залежить від DoSomethingперевірки того, що obj не є нульовим. Тож DoSomethingколи-небудь слід вирішувати більше не перевіряти, або якщо я вирішу використовувати іншу функцію, Obj може більше не перевірятися. Що змушує мене написати цю реалізацію:

void DoSomethingElse( object obj )
{
  Contract.Requires<ArgumentNullException>( obj != null );
  DoSomething( obj );
  //code using obj
}

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

Яка найпоширеніша практика для подібної ситуації?


3
ArgumentBullException? Це новий :)
CVn

lol @ мої навички друку ... я відредагую це.
стинь

Відповіді:


13

Особисто я перевірив би на нуль будь-яку функцію, яка вийде з ладу, якщо вона отримає нуль, а не в будь-якій функції, яка не буде.

Отже, у вашому прикладі вище, якщо doSomethingElse () не потрібно перенаправляти obj, я б не перевіряв obj на нуль.

Якщо DoSomething () робить dereference obj, він повинен перевірити наявність null.

Якщо обидві функції відмежують це, то вони повинні обидві перевірити. Отже, якщо DoSomethingElse dereferences obj, то він повинен перевірити на null, але DoSomething також повинен перевірити наявність null, як це може бути викликано з іншого шляху.

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


1
Я повністю згоден. Передумови кожного методу повинні стояти самостійно. Уявіть, що ви перезаписуєтесь DoSomething()таким чином, що попередня умова більше не потрібна (навряд чи в цьому конкретному випадку, але це може статися в іншій ситуації), і зніміть перевірку передумови. Зараз якийсь, здавалося б, абсолютно неспоріднений метод порушено через відсутність передумови. Я візьму трохи дублювання коду для ясності над дивними помилками, такими як бажання зберегти кілька рядків коду в будь-який день.
CVn

2

Чудово! Я бачу, що ви дізналися про кодові контракти для .NET. Код контрактів йде набагато далі, ніж ваші середні твердження, найкращим прикладом яких є статична перевірка . Це може бути недоступним для вас, якщо у вас не встановлено Visual Studio Premium або вище, але важливо зрозуміти намір, який стоїть за ним, якщо ви збираєтесь використовувати кодові контракти.

Коли ви застосовуєте контракт до функції, це буквально є контрактом . Ця функція гарантує поведінку відповідно до договору, і гарантується, що вона буде використовуватись лише у визначеному договором.

У вашому наведеному прикладі DoSomethingElse()функція не відповідає договору, як зазначено DoSomething(), оскільки нуль може бути передано, і статична перевірка вкаже на цю проблему. Спосіб вирішити це - додавши той самий контракт до DoSomethingElse().

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

Це повинно змусити вас переглянути, чому ви поділите поведінку на дві функції. Я завжди вважав ( всупереч поширеній думці ), що не слід розділяти функції, які викликаються лише з одного місця . Викриваючи інкапсуляцію шляхом застосування контрактів, це стає ще більш очевидним. Здається, я знайшов додаткову аргументацію для своєї справи! Дякую! :)


щодо вашого останнього абзацу: у фактичному коді обидві функції були членами двох різних класів, тому вони розділені. Крім того, я стільки разів опинявся в такій ситуації: напишіть тривалу функцію, вирішіть не розділяти її. Пізніше з’ясуємо, що частина логіки дублюється десь ще, тому розділіть її все одно. Або через рік прочитайте її ще раз і знайдіть її нечитаючою, тому розділіть її все одно. Або при налагодженні: розділені функції = менший стук по клавіші F10. Причин більше, тому особисто я вважаю за краще розколотися, хоча це означає, що іноді це може бути занадто екстремально.
Стін

(1) "Пізніше з'ясуємо, що частина логіки дублюється десь ще" . Ось чому я вважаю важливішим завжди "розвиватися до API", ніж просто розділяти функції. Постійно думайте про повторне використання не тільки в межах поточного класу. (2) "Або через рік прочитайте його ще раз і вважаєте його нечитабельним" Оскільки функції мають імена, це краще? Ви ще більше читаєте, якщо використовуєте те, що коментатор у моєму блозі описав як "параграфи коду". (3) "розділені функції = менший стук по клавіші F10" ... Я не розумію, чому.
Стівен Євріс

(1) узгоджена (2) читаність - це особисті переваги, тому для мене це насправді не щось, що обговорюється. (3) проходження функції 20 рядків вимагає удару F10 20 разів. Перегляд функцій, у яких 10 таких рядків у розділеній функції, вимагає, щоб я міг лише натиснути F10 11 разів. Так, у першому випадку я можу поставити точки перерви або вибрати "стрибок на курсор", але це все ж більше зусиль, ніж у другому випадку.
Стін

@stijn: (2) погодився; p (3) дякую за уточнення!
Стівен Євріс
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.