Дизайн інтерфейсу, коли функції потрібно викликати у певній послідовності


24

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

1) Зберіть інформацію про конфігурацію. Це може статися в різний час і місця. Наприклад, модуль A і модуль B можуть вимагати (в різний час) деякі ресурси від мого модуля. Ці "ресурси" насправді є конфігурацією.

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

3) Тільки після цього може бути (і повинна) детальна конфігурація зазначених ресурсів.

4) Крім того, лише після 2) можна (і потрібно) прокласти маршрутизацію вибраних ресурсів до оголошених абонентів.


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


Етап 1 краще називати discoveryабо handshake?
rwong

1
Тимчасове з'єднання є антидіаграмою, і його слід уникати.

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

Відповіді:


45

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

Наприклад, замість first you init, then you start, then you stop

Ваш конструктор initоб'єкт, який можна запустити, і startстворює сеанс, який можна зупинити.

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

Тепер застосуйте цю техніку до власної справи.


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

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

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

@JoshuaTaylor моя відповідь - реалізація шаблону для побудови шаблону :)
Silviu Burcea

@SilviuBurcea Ваша відповідь не є впровадженням кроків, але я коментую це, а не тут.
Джошуа Тейлор

19

Ви можете мати спосіб запуску повернути в конфігурацію об'єкт, який є необхідним параметром:

Ресурс * MyModule :: GetResource ();
MySession * MyModule :: Startup ();
недійсний ресурс :: Налаштування (MySession * сесія);

Навіть якщо ваша MySessionлише порожня структура, це забезпечить безпеку типу, що жоден Configure()метод не може бути викликаний перед запуском.


Що заважає комусь займатися module->GetResource()->Configure(nullptr)?
svick

@svick: Нічого, але ви повинні це чітко робити. Такий підхід говорить вам, чого він очікує, і обійти це очікування - свідоме рішення. Як і у більшості мов програмування, ніхто не заважає вам стріляти в ногу. Але API завжди добре чітко вказувати, що ви так робите;)
Майкл Клемент

+1 виглядає чудово і просто. Однак я можу побачити проблему. Якщо у мене є об'єкти a, b, c, d, то я можу запустити a, і з його допомогою MySessionнамагаюся використовувати bяк вже запущений об'єкт, а насправді це не так.
Vorac

8

Спираючись на відповідь Cashcow - навіщо вам представляти абоненту новий об’єкт, коли ви можете просто представити новий інтерфейс? Шаблон ребренду:

class IStartable     { public: virtual IRunnable      start()     = 0; };
class IRunnable      { public: virtual ITerminateable run()       = 0; };
class ITerminateable { public: virtual void           terminate() = 0; };

Ви також можете дозволити ITerminateable реалізувати IRunnable, якщо сеанс можна запустити кілька разів.

Ваш об'єкт:

class Service : IStartable, IRunnable, ITerminateable
{
  public:
    IRunnable      start()     { ...; return this; }
    ITerminateable run()       { ...; return this; }
    void           terminate() { ...; }
}

// And use it like this:
IStartable myService = Service();

// Now you can only call start() via the interface
IRunnable configuredService = myService.start();

// Now you can also call run(), because it is wrapped in the new interface...

Таким чином ви можете викликати лише правильні методи, оскільки у вас є лише інтерфейс IStartable на початку, і ви отримаєте метод run (), доступний лише тоді, коли ви викликали start (); Ззовні це виглядає як візерунок з декількома класами та Об'єктами, але базовий клас залишається одним класом, на який завжди посилаються.


1
Яка перевага мати лише один базовий клас замість кількох? Оскільки це єдина відмінність від запропонованого нами рішення, мене зацікавив би цей конкретний момент.
Michael Le Barbier Grünewald

1
@ MichaelGrünewald Не потрібно реалізовувати всі інтерфейси з одним класом, але для об'єкта типу конфігурації це може бути найпростіша техніка реалізації для обміну даними між екземплярами інтерфейсів (тобто тому, що вони поділяються в силу того, що вони однакові об’єкт).
Джошуа Тейлор


@JoshuaTaylor Обмін даними між екземплярами інтерфейсу двояке: хоча це може бути простішим у впровадженні, ми повинні бути обережними і не отримувати доступ до "невизначеного стану" (наприклад, доступу до клієнтської адреси непідключеного сервера). Оскільки ОП ставить акцент на зручності використання інтерфейсу, ми можемо вважати два підходи рівними. Дякую, що ви цитуєте BTW "шаблону будівельного кроку".
Майкл Ле Барб'є Грюневальд

1
@ MichaelGrünewald Якщо ви взаємодієте з об'єктом лише через певний інтерфейс, який вказаний у певній точці, не повинно бути жодного способу (без кастингу тощо) для доступу до цього стану.
Джошуа Тейлор

2

Існує маса вагомих підходів до вирішення вашої проблеми. Басіль Старинкевич запропонував підхід «нульової бюрократії», який дає вам простий інтерфейс і покладається на програміста, використовуючи відповідний інтерфейс. Хоча мені подобається такий підхід, я представлю ще один, який має більше eingineering, але дозволяє компілятору вловлювати деякі помилки.

  1. Визначення різних станів пристрій може бути, так як Uninitialised, Started, Configuredі так далі. Список має бути кінцевим.¹

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

  3. Упакуйте всі способи лікування в одному об'єкті, DeviceStrategyде методи використовують структури, визначені в 2. як входи та виходи. Таким чином, у вас може бути DeviceStarted DeviceStrategy::start (DeviceUninitalised dev)метод (або будь-який еквівалент, згідно з умовами вашого проекту).

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

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

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

Підсумовуючи їх, ключовими моментами описаного нами проекту є:

  1. Через принцип заміщення об'єкти, що представляють стани пристроїв, повинні відрізнятися і не мати спеціальних відносин успадкування.

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

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

For: Для цього або дотримуйтесь своєї інтуїції або знайдіть класи еквівалентності методів у вашій реальній реалізації для відношення “method₁ ~ method₂ iff. допустимо використовувати їх на одному об’єкті ”- якщо припустити, що у вас є великий об’єкт, що інкапсулює всі способи лікування на вашому пристрої. Обидва способи списку станів дають фантастичні результати.


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

2

Використовуйте модель-конструктор.

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

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


1

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

У вас також може бути варіант налагодження бібліотеки, який виконує деякі перевірки часу виконання.

Можливо, правильно визначити та задокументувати деякі умови іменування (наприклад, preconfigure* , startup*, postconfigure*, run*....)

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


Діаграма переходу стану, подібна до життєвого циклу активності додатків Android , може знадобитися для передачі інформації.
rwong

1

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

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


Ви маєте в виду що - щось на зразок цього ?
Vorac

1
public class Executor {

private Executor() {} // helper class

  public void execute(MyStepsRunnable r) {
    r.step1();
    r.step2();
    r.step3();
  }
}

interface MyStepsRunnable {

  void step1();
  void step2();
  void step3();
}

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


В іншому коментарі ви назвали це реалізацією кроків, але це не так. Якщо у вас є екземпляр MyStepsRunnable, ви можете зателефонувати step3 перед step1. Реалізація крокових механізмів буде більше узгоджуватися з ідеєю.com/UDECgY . Ідея полягає лише в тому, щоб отримати щось із кроком2, запустивши step1. Таким чином, ви змушені викликати методи в потрібному порядку. Наприклад, див. Stackoverflow.com/q/17256627/1281433 .
Джошуа Тейлор

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

Це все ще не робить його кроком будівельника. У вашому коді нічого не може зробити користувач для запуску коду між різними кроками. Ідея полягає не лише в послідовності коду (незалежно від того, публічний він чи приватний, чи іншим чином інкапсульований). Як показує ваш код, це зробити досить просто step1(); step2(); step3();. Сенс конструктора кроків полягає в тому, щоб надати API, який викриває деякі етапи, і примусово виконати послідовність, в якій вони викликаються. Це не повинно заважати програмісту робити інші дії між кроками.
Джошуа Тейлор
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.