Як попередити інших програмістів про виконання класу


96

Я пишу заняття, які "повинні використовуватися певним чином" (я думаю, всі класи повинні ...).

Наприклад, я створюю fooManagerклас, який вимагає дзвінка, скажімо, до Initialize(string,string). І, щоб просунути приклад трохи далі, клас був би марним, якщо ми не послухаємо його ThisHappenedдії.

Моя думка, клас, який я пишу, вимагає викликів методів. Але вона складеться чудово, якщо ви не зателефонуєте до цих методів і в кінцевому підсумку вийде з порожнім новим FooManager. У якийсь момент він або не спрацює, або може вийти з ладу, залежно від класу та того, що він робить. Програміст, який реалізує мій клас, очевидно, загляне всередину нього і зрозуміє "О, я не закликав ініціалізувати!", І це було б добре.

Але мені це не подобається. Ідеально хотілося б, щоб код НЕ компілювався, якби метод не викликався; Я здогадуюсь, це просто неможливо. Або щось, що було б відразу видно і зрозуміло.

Мене хвилює існуючий тут підхід, який полягає в наступному:

Додайте приватне булеве значення у класі та перевіряйте всюди, чи потрібно клас ініціалізований; якщо ні, я накину виняток із записом "Клас не був ініціалізований, ви впевнені, що ви телефонуєте .Initialize(string,string)?".

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

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

Що я тут шукаю:

  • Чи правильний мій підхід?
  • Чи є кращий?
  • Що ви робите / радите?
  • Я намагаюся вирішити питання, яке не стосується? Мені сказали колеги, що програміст повинен перевірити клас, перш ніж намагатися ним користуватися. Я з повагою не згоден, але я вважаю, що це інша справа.

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

ПОЯСНЕННЯ:

Щоб уточнити багато питань тут:

  • Я точно не кажу лише про ініціалізацію частини класу, а навпаки, це все життя. Не дозволяйте колегам викликати метод двічі, переконуючись, що вони дзвонять до X перед Y, і т. Д. Все, що в кінцевому підсумку було б обов'язковим і в документації, але що мені хотілося б у коді і якомога простішим і малим. Мені дуже сподобалась ідея Ассерта, хоча я впевнений, що мені потрібно буде змішати деякі інші ідеї, оскільки Ассерти не завжди будуть можливими.

  • Я використовую мову C #! Як я цього не згадав ?! Я перебуваю в середовищі Xamarin і будую мобільні додатки, зазвичай використовую близько 6 - 9 проектів у рішенні, включаючи проекти PCL, iOS, Android та Windows. Я був розробником близько півтора року (школа і робота разом), звідси мої іноді смішні заяви \ питання. Тут все, мабуть, не має значення, але занадто багато інформації - це не завжди погано.

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

як я можу переконатися, що він зареєструвався на цю подію?

як я можу переконатися, що він не забув "зупинити процес в якийсь момент"

Тут я пам’ятаю клас пошуку оголошень. До тих пір, поки видно вигляд, де відображається Оголошення, клас щохвилини отримуватиме нове оголошення. Цей клас потребує перегляду, коли він побудований там, де він може відображати Об'яву, і це очевидно може входити в параметр. Але як тільки перегляд не зникне, потрібно викликати StopFetching (). Інакше клас зберігатиме рекламу для такого перегляду, якого навіть немає, і це погано.

Крім того, у цьому класі є події, які потрібно слухати, наприклад, "AdClicked". Все працює добре, якщо його не слухають, але ми втрачаємо відстеження аналітики там, якщо крани не зареєстровані. Оголошення все ще працює, тому користувач і розробник не побачать різниці, а аналітика просто матиме неправильні дані. Цього потрібно уникати, але я не впевнений, як розробник може знати, що вони повинні зареєструватися на подію дао. Це спрощений приклад, але ідея є, "переконайтеся, що він використовує загальнодоступні дії, доступні", і в потрібний час, звичайно!


13
Забезпечити, що виклики до методів класу відбуваються у визначеному порядку, взагалі неможливо . Це одна з багатьох, багатьох проблем, що прирівнюються до проблеми зупинки. Хоча в деяких випадках можна було б виконати невідповідність помилки під час компіляції, загального рішення немає. Ось імовірно, чому мало мов підтримують конструкції, які дозволять вам поставити діагноз принаймні очевидних випадків, хоча аналіз потоку даних став досить складним.
Кіліан Фот


5
Відповідь на інше питання не має значення, питання не однакове, тому це різні питання. Якщо хтось шукає цю дискусію, він не вводить назву іншого питання.
Гіл Санд

6
Що заважає об'єднати вміст ініціалізації в ctor? Чи повинен дзвінок initializeздійснюватися пізно після створення об’єкта? Чи буде ctor занадто "ризикованим", в тому сенсі, що він може кидати винятки і розривати ланцюжок створення?
Знайдено

7
Ця проблема називається тимчасовою зв'язкою . Якщо ви можете, намагайтеся уникати ставлення об'єкта в недійсний стан, кидаючи винятки в конструктор при зіткненні з недійсними входами, таким чином ви ніколи не переходите до виклику, newякщо об'єкт не готовий до використання. Покладатися на метод ініціалізації неодмінно поверне переслідувати вас, і його слід уникати, якщо це абсолютно не потрібно, принаймні, це мій досвід.
Сералізуйте

Відповіді:


161

У таких випадках найкраще використовувати систему типів вашої мови, щоб допомогти вам у правильній ініціалізації. Як ми можемо запобігти FooManagerвід бути використані без ініціалізації? Шлях запобігання FooManagerвід того створюються без необхідної інформації для правильної ініціалізації його. Зокрема, за всю ініціалізацію покладається конструктор. Ви ніколи не повинні дозволяти вашому конструктору створювати об’єкт у незаконному стані.

Але абоненти повинні побудувати a, FooManagerперш ніж вони зможуть його ініціалізувати, наприклад, тому що FooManagerпередається навколо як залежність.

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

ctorArgs = ...;
getFooManager = (initInfo) -> new FooManager(ctorArgs, initInfo);
...
getFooManager(myInitInfo).fooMethod();

Проблема в цьому полягає в тому, що вам потрібно надавати інформацію про init кожного разу, коли ви отримуєте доступ до FooManager.

Якщо це потрібно вашою мовою, ви можете перенести getFooManager()операцію у фабрико-будівельний або будівельний клас.

Я дуже хочу перевірити час виконання, щоб initialize()метод викликався, а не використовувати рішення на рівні системи.

Можна знайти компроміс. Ми створюємо клас обгортки, MaybeInitializedFooManagerякий має get()метод, який повертає FooManager, але кидає, якщо FooManagerне був повністю ініціалізований. Це працює лише в тому випадку, якщо ініціалізація проводиться через обгортку, або якщо є FooManager#isInitialized()метод.

class MaybeInitializedFooManager {
  private final FooManager fooManager;

  public MaybeInitializedFooManager(CtorArgs ctorArgs) {
    fooManager = new FooManager(ctorArgs);
  }

  public FooManager initialize(InitArgs initArgs) {
    fooManager.initialize(initArgs);
    return fooManager;
  }

  public FooManager get() {
    if (fooManager.isInitialized()) return fooManager;
    throw ...;
  }
}

Я не хочу змінювати API свого класу.

У такому випадку ви хочете уникати if (!initialized) throw;умов у кожному методі. На щастя, для вирішення цього питання існує проста модель.

Об'єкт, який ви надаєте користувачам, - це лише порожня оболонка, яка делегує всі виклики об’єкту реалізації. За замовчуванням об’єкт реалізації видає помилку для кожного методу, який він не був ініціалізований. Однак initialize()метод замінює об'єкт реалізації об'єктом повністю сконструйованого.

class FooManager {
  private CtorArgs ctorArgs;
  private Impl impl;

  public FooManager(CtorArgs ctorArgs) {
    this.ctorArgs = ctorArgs;
    this.impl = new UninitializedImpl();
  }

  public void initialize(InitArgs initArgs) {
    impl = new MainImpl(ctorArgs, initArgs);
  }

  public X foo() { return impl.foo(); }
  public Y bar() { return impl.bar(); }
}

interface Impl {
  X foo();
  Y bar();
}

class UninitializedImpl implements Impl {
  public X foo() { throw ...; }
  public Y bar() { throw ...; }
}

class MainImpl implements Impl {
  public MainImpl(CtorArgs c, InitArgs i);
  public X foo() { ... }
  public Y bar() { ... }
}

Це витягує основну поведінку класу в MainImpl.


9
Мені подобаються перші два рішення (+1), але я не думаю, що ваш останній фрагмент з його повторенням методів в FooManagerі в UninitialisedImplє кращим, ніж повторення if (!initialized) throw;.
Бергі

3
@Bergi Деякі люди дуже люблять заняття. Хоча , якщо FooManagerє багато методів, це може бути простіше , ніж потенційно забути деякі з if (!initialized)перевірок. Але в цьому випадку вам, мабуть, слід віддати перевагу розбиванню класу.
іммібіс

1
A MaybeInitializedFoo не здається кращим, ніж ініціалізований Foo теж мене, але +1 за надання деяких варіантів / ідей.
Меттью Джеймса Бріггса

7
+1 Завод - правильний варіант. Ви не можете вдосконалити конструкцію, не змінюючи її, тому ІМХО останній бій відповіді про те, як наполовину, не допоможе ОП.
Натан Купер

6
@Bergi Я вважав, що техніка, представлена ​​в останньому розділі, надзвичайно корисна (див. Також "Замінити умови через поліморфізм" на рефакторинг, а також на зразок стану). Забути чек в одному методі легко, якщо ми використовуємо if / else, набагато складніше забути реалізувати метод, необхідний інтерфейсом. Найголовніше, що MainImpl відповідає точно об'єкту, де метод ініціалізації зливається з конструктором. Це означає, що реалізація MainImpl може користуватися більш сильними гарантіями, що дає це, що спрощує основний код. …
амон

79

Найефективніший і корисний спосіб запобігти клієнтам не «зловживати» об’єктом - це зробити його неможливим.

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

Якщо вам потрібно мати можливість отримати доступ до неініціалізованого об'єкта перед ініціалізацією, тоді ви могли б мати два стани, реалізовані як окремі класи, тому ви починаєте з UnitializedFooManagerекземпляра, який має Initialize(...)метод, який повертається InitializedFooManager. Методи, які можна викликати лише в ініціалізованому стані, існують лише у InitializedFooManager. Цей підхід можна поширити на кілька штатів, якщо вам потрібно.

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

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

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

Простий приклад: у вас є File-об'єкт, який дозволяє читати з файлу. Однак клієнту потрібно зателефонувати Openдо того, як ReadLineметод може бути викликаний, і йому потрібно пам’ятати, щоб завжди викликати Close(навіть якщо виняток), після чого ReadLineметод більше не повинен викликатись. Це тимчасова зв’язка. Цього можна уникнути, використовуючи єдиний метод, який приймає зворотний зв'язок або делегат як аргумент. Метод управляє відкриттям файлу, викликом зворотного виклику та закриттям файлу. Він може передавати чіткий інтерфейс із Readметодом до зворотного дзвінка. Таким чином, клієнт не може забути називати методи у правильному порядку.

Інтерфейс з тимчасовою муфтою:

class File {
     /// Must be called on a closed file.
     /// Remember to always call Close() when you are finished
     public void Open();

     /// must be called on on open file
     public string ReadLine();

     /// must be called on an open file
     public void Close();
}

Без тимчасової зв'язку:

class File {
    /// Opens the file, executes the callback, and closes the file again.
    public void Consume(Action<IOpenFile> callback);
}

interface IOpenFile {
    string ReadLine();
}

Більш важким рішенням було б визначити Fileабстрактний клас, який вимагає від вас застосувати (захищений) метод, який буде виконуватися у відкритому файлі. Це називається шаблон шаблону методу .

abstract class File {
    /// Opens the file, executes ConsumeOpenFile(), and closes the file again.
    public void Consume();

    /// override this
    abstract protected ConsumeOpenFile();

    /// call this from your ConsumeOpenFile() implementation
    protected string ReadLine();
}

Перевага та ж: Клієнт не повинен пам’ятати називати методи в певному порядку. Викликати методи в неправильному порядку просто неможливо.


2
Також я пам’ятаю випадки, коли від конструктора просто неможливо було піти, але я не маю приклад, щоб показати це зараз
Гіль Санд

2
Приклад тепер описує заводський зразок - але перше речення насправді описує парадигму RAII . Отже, яку саме цю відповідь слід рекомендувати?
Ext3h

11
@ Ext3h Я не думаю, що заводський зразок та RAII взаємовиключні.
Пітер Б

@PieterB Ні, це не так, фабрика може забезпечити RAII. Але лише з додатковим наслідком, що аргументи шаблону / конструктора (викликані UnitializedFooManager) не повинні мати спільний стан з екземплярами ( InitializedFooManager), як Initialize(...)насправді є Instantiate(...)семантичними. Інакше цей приклад призводить до нових проблем, якщо один і той же шаблон розробник використовує два рази. І це неможливо статично запобігти / перевірити, якщо мова явно не підтримує moveсемантику, щоб надійно споживати шаблон.
Ext3h

2
@ Ext3h: Я рекомендую перше рішення, оскільки воно є найпростішим. Але якщо клієнтові потрібно мати доступ до об'єкта перед тим, як викликати Initialize, він не може його використовувати, але ви, можливо, зможете використовувати друге рішення.
ЖакB

39

Як правило, я просто перевіряю наявність ініціалізації та кидаю (скажімо), IllegalStateExceptionякщо ви спробуєте використовувати його, але не ініціалізований.

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

ComponentBuilder builder = new ComponentBuilder();
Component forClients = builder.initialise(); // the 'Component' is what you give your clients

і таким чином ви контролюєте створення об'єктів та життєвий цикл, а ваші клієнти отримують це Componentв результаті вашої ініціалізації. Це фактично ледача інстанція.


1
Гарна відповідь - 2 хороших варіанти приємної лаконічної відповіді. Інші відповіді вище представлені гімнастики типової системи, які (теоретично) є дійсними; але для більшості випадків ці 2 простіші варіанти - ідеальний практичний вибір.
Thomas W

1
Примітка. В .NET, InvalidOperationException просувається Microsoft як те, що ви викликали IllegalStateException.
miroxlav

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

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

14

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

Якщо у вас є FooManagerHaskell, було б злочинним дозволити вашим користувачам будувати таку, яка не здатна керувати Foos, оскільки система типу робить це так просто, і це умовні умови, яких очікують розробники Haskell.

З іншого боку, якщо ви пишете C, ваші колеги будуть повністю в межах своїх прав, щоб забрати вас назад і застрелити вас за визначення окремих struct FooManagerі struct UninitializedFooManagerтипів, які підтримують різні операції, оскільки це призведе до зайвого складного коду за дуже невелику користь .

Якщо ви пишете Python, в мові немає механізму, який би дозволяв вам це робити.

Ви, мабуть, не пишете Haskell, Python чи C, але вони є наочними прикладами щодо того, наскільки / наскільки мало роботи очікується та яка система може виконати.

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


Мене трохи бентежить "Якщо ви пишете Python, в мові немає механізму, який би дозволяв вам це робити". У Python є конструктори, і створити заводські методи досить тривіально. То, може, я не розумію, що ви маєте на увазі під цим "цим"?
А. Л. Фланаган

3
Під "цим" я мав на увазі помилку часу компіляції, на відміну від помилки виконання.
Патрік Коллінз

Просто скачучи, згадуючи Python 3.5, який у поточній версії має типи через підключається тип типу. Однозначно можна змусити Python помилитися перед запуском коду лише тому, що він був імпортований. До 3,5 це було цілком правильно.
Бенджамін Грюнбаум

О, добре. Запізнілий +1. Навіть розгублено, це було дуже проникливим.
А. Л. Фланаган

Мені подобається твій стиль Патріка.
Гіль Сенд

4

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

Таким чином, у вас є перевірки в середовищі розробки (і будь-які тести, які колег-розробник назвав би WIL передбачувано), але ви не будете доставляти код замовнику, оскільки твердження (принаймні на Java) збираються лише вибірково.

Отже, клас Java, який використовує це, виглядатиме так:

/** For some reason, you have to call init and connect before the manager works. */
public class FooManager {
   private int state = 0;

   public FooManager () {
   }

   public synchronized void init() {
      assert state==0 : "you called init twice.";
      // do something
      state = 1;
   }

   public synchronized void connect() {
      assert state==1 : "you called connect before init, or connect twice.";
      // do something
      state = 2;
   }

   public void someWork() {
      assert state==2 : "You did not properly init FooManager. You need to call init and connect first.";
      // do the actual work.
   }
}

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

Крім того, вони досить тонкі і не займають величезну частину синтаксису if () кидання ... їх не потрібно ловити і т.д.


3

Розглядаючи цю проблему більш загально, ніж нинішні відповіді, які в основному зосереджені на ініціалізації. Розглянемо об'єкт, який матиме два способи, a()і b(). Вимога полягає в тому, що a()завжди називається раніше b(). Ви можете створити перевірку часу компіляції, що це відбувається, повернувши з нього новий об'єкт a()і перейшовши b()на новий, а не на початковий. Приклад реалізації:

class SomeClass
{
   private int valueRequiredByMethodB;

   public IReadyForB a (int v) { valueRequiredByMethodB = v; return new ReadyForB(this); }

   public interface IReadyForB { public void b (); }

   private class ReadyForB : IReadyForB
   {
      SomeClass owner;
      private ReadyForB (SomeClass owner) { this.owner = owner; }
      public void b () { Console.WriteLine (owner.valueRequiredByMethodB); }
   }
}

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


2

Коли я реалізую базовий клас, який вимагає додаткової інформації про ініціалізацію або посилання на інші об'єкти, перш ніж він стане корисним, я прагну зробити цей базовий клас абстрактним, а потім визначити кілька абстрактних методів у цьому класі, які використовуються в потоці базового класу (наприклад, abstract Collection<Information> get_extra_information(args);і abstract Collection<OtherObjects> get_other_objects(args);який за договором успадкування повинен бути реалізований конкретним класом, змушуючи користувача цього базового класу постачати всі речі, необхідні базовому класу.

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

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


2

Відповідь: Так, ні, а іноді. :-)

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

Перевірка часу компіляції, безумовно, є кращою для перевірки часу виконання, яка є переважною, ніж перевірка взагалі.

Час виконання RE:

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

Ви можете зробити так, щоб деякі речі відбувалися автоматично. Якщо взяти простий приклад, програмісти часто говорять про "ледачу популяцію" об'єкта. Коли створений екземпляр, ви встановлюєте прапор, що заповнюється, на значення false, або якесь ключове об'єктне посилання на null. Потім, коли викликається функція, яка потребує відповідних даних, вона перевіряє прапор. Якщо це неправда, він заповнює дані та встановлює прапор true. Якщо це правда, це просто продовжує припускати, що дані є. Те ж саме можна зробити і з іншими передумовами. Встановіть прапорця в конструкторі або як значення за замовчуванням. Потім, коли ви дістанетесь до точки, коли повинна була бути викликана якась функція попереднього стану, якщо вона ще не була викликана, зателефонуйте їй. Звичайно, це працює лише в тому випадку, якщо у вас є дані, щоб викликати їх у цей момент, але у багатьох випадках ви можете включити будь-які необхідні дані у параметри функції "

Час збирання RE:

Як говорили інші, якщо вам потрібно ініціалізуватись до того, як об’єкт можна буде використовувати, то ініціалізація покладеться в конструктор. Це досить типова порада для ООП. Якщо це взагалі можливо, вам слід змусити конструктор створити дійсний, корисний примірник об'єкта.

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

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

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

RE неможливо застосувати

Не рідкість для класу вимагати певного очищення, коли ви закінчите: закриття файлів або з'єднань, звільнення пам'яті, запис остаточних результатів у БД тощо. Немає простого способу змусити це статися в будь-який час компіляції або час виконання. Я думаю, що більшість мов OOP мають певне положення про функцію "фіналізатор", яка викликається, коли екземпляр збирає сміття, але я думаю, що більшість також говорить, що вони не можуть гарантувати, що це колись буде запущено. Мови OOP часто включають в себе функцію "dispose" з певним положенням для встановлення сфери використання екземпляра, пункту "використання" або "з", і коли програма виходить з цього обсягу, розпоряджається розпорядження. Але для цього потрібен програміст використовувати відповідний специфікатор області. Це не змушує програміста робити це правильно,

У випадках, коли неможливо змусити програміста правильно використовувати клас, я намагаюся зробити так, щоб програма вибухала, якщо він це зробив неправильно, а не давав неправильних результатів. Простий приклад: я бачив безліч програм, які ініціалізують купу даних до фіктивних значень, так що користувач ніколи не отримає виняток з нульовим вказівником, навіть якщо він не зможе викликати функції для правильного заповнення даних. І мені завжди цікаво, чому. Ви йдете зі свого шляху, щоб помилок важче було знайти. Якщо, коли програмісту не вдалося викликати функцію "завантаження даних", а потім грубо спробував використати дані, він отримав нульове виключення вказівника, виняток швидко покаже йому, де проблема. Але якщо приховати їх, зробивши дані порожніми або нульовими в таких випадках, програма може працювати до завершення, але давати неточні результати. У деяких випадках програміст може навіть не усвідомлювати, що є проблема. Якщо він все-таки помітить, йому, можливо, доведеться піти довгим слідом, щоб знайти, де він помилився. Краще провалитися рано, ніж намагатися сміливо продовжувати.

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

Але є також певний момент, коли ви повинні сказати: Програміст повинен прочитати документацію щодо функції.


1
Що стосується примусового очищення, існує шаблон, який може бути використаний, коли ресурс може бути виділений лише за допомогою виклику певного методу (наприклад, типовою мовою OO, він може мати приватний конструктор). Цей метод розподіляє ресурс, викликає функцію (або метод інтерфейсу), який передається йому з посиланням на ресурс, а потім знищує ресурс, як тільки ця функція повертається. За винятком різкого завершення потоку, ця схема гарантує, що ресурс завжди знищується. Це звичайний підхід у функціональних мовах, але я також бачив, що він застосовується в системах ОО для хорошого ефекту.
Жуль


2

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

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

Але тест на виявлення використання в час компіляції замість часу виконання - це те саме тест: все одно потрібно знати, щоб спочатку викликати належні функції та використовувати їх правильним чином.

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

Ви запитуєте: чи можна робити тести під час виконання для забезпечення належного використання, чи варто це робити?

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

Можливо, перевірка робиться лише в налагодній збірці.

Можливо, перевірка є складною (подумайте, наприклад, про купівлю), але вона присутня і її можна робити раз у раз, або дзвінки, що додаються тут і там, коли над кодом працює і з’являється якась проблема, або пройти тестування на переконайтесь, що в програмі тестування одиниць чи інтеграції, що посилається на один клас, немає такої проблеми .

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

Ваші інстинкти хороші. Тримай!


0

Принцип інформаційного приховування (інкапсуляції) полягає в тому, що суб'єкти поза класом не повинні знати більше, ніж потрібно для правильного використання цього класу.

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

  • Або явно запитайте потрібну інформацію у конструктора;
  • Або тримайте конструктори недоторканими та модифікуйте методи так, щоб для виклику методів вам потрібно було надати відсутні дані.

Слова мудрості:

* У будь-якому випадку вам доведеться переробляти клас та / або конструктор та / або методи.

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

* Якщо ви погано написали винятки та повідомлення про помилки, ви класу будете віддавати ще більше про себе.

* Нарешті, якщо сторонній чоловік повинен знати, що він повинен ініціалізувати a, b і c, а потім викликати d (), e (), f (int), то у вас є витік у вашій абстракції.


0

Оскільки ви вказали, що використовуєте C # життєздатне рішення для вашої ситуації, - це використовувати аналітики коду Рослін . Це дозволить вам негайно виявити порушення та навіть запропонувати виправлення коду .

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

public abstract class Foo
{
   [TemporallyCoupled(1)]
   public abstract void Init();

   [TemporallyCoupled(2)]
   public abstract void DoBar();

   [TemporallyCoupled(3)]
   public abstract void Close();
}

1: Я ніколи не писав аналізатор коду Росліна, тому це може бути не найкращою реалізацією. Ідея використовувати Roslyn Code Analyzer для перевірки правильності використання вашого API, є 100% звуком.


-1

Я вирішував цю проблему раніше з приватним конструктором та статичним MakeFooManager()методом у класі. Приклад:

public class FooManager
{
     public string Foo { get; private set; }
     public string Bar { get; private set; }

     private FooManager(string foo, string bar)
     {
         Foo = foo;
         Bar = bar;
     }

     public static FooManager MakeFooManager(string foo, string bar)
     {
         // Can do other checks here too.
         if(foo == null || bar == null)
         {
             return null;
         }
         else
         {
             return new FooManager(foo, bar);
         }
     }
}

Оскільки конструктор реалізований, немає жодної можливості створити екземпляр, FooManagerне проходячи MakeFooManager().


1
це здається просто повторити зауваження , зроблене і пояснено в попередньому рівні відповіді , який був розміщений за кілька годин до
комара

1
чи є ця взагалі якась перевага у простому недійсному перевірці в ctor? здається, що ви щойно створили метод, який не робить нічого, крім обгортання іншого методу. чому?
сара

Крім того, чому є змінні foo та bar bar?
Patrick M

-1

Існує кілька підходів:

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

  2. Пам’ятайте, як користуватися коментарями. Якщо клас самостійно пояснює, не пишіть коментарі. * Таким чином люди швидше читають ваші коментарі, коли клас насправді їм потрібен. І якщо у вас є один із таких рідкісних випадків, коли клас важко правильно використовувати та потребує коментарів, додайте приклади.

  3. Використовуйте твердження у своїх налагодженнях.

  4. Викиньте винятки, якщо використання класу недійсне.

* Ось такі коментарі не слід писати:

// gets Foo
GetFoo();
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.