Дизайн интерфейса, где функции должны вызываться в определенной последовательности

24

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

1) Соберите информацию о конфигурации. Это может случиться в разное время и в разных местах. Например, модуль A и модуль B могут одновременно запрашивать (в разное время) некоторые ресурсы из моего модуля. Эти «ресурсы» на самом деле представляют собой конфигурацию.

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

3) Только после этого можно (и нужно) проводить детальную настройку указанных ресурсов.

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


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

Vorac
источник
Этап 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 Мой ответ - реализация шаблонов Step Builder :)
Сильвиу Бурча
@SilviuBurcea Ваш ответ не является реализацией пошаговой инструкции , но я ее прокомментирую, а не здесь.
Джошуа Тейлор
19

Вы можете заставить метод запуска возвращать объект, который является обязательным параметром для конфигурации:

Resource * MyModule :: GetResource ();
MySession * MyModule :: Startup ();
void Resource :: Configure (MySession * session);

Даже если ваша структура MySessionпросто пустая, благодаря безопасности типов это обеспечит невозможность Configure()вызова метода перед запуском.

JPA
источник
Что мешает кому-то делать module->GetResource()->Configure(nullptr)?
свик
@svick: Ничего, но вы должны сделать это явно. Этот подход говорит вам, что он ожидает, и обход этого ожидания является сознательным решением. Как и в большинстве языков программирования, никто не мешает вам выстрелить себе в ногу. Но API всегда хорошо показывает, что вы это делаете;)
Майкл Клемент,
+1 выглядит великолепно и просто. Тем не менее, я вижу проблему. Если у меня есть объекты a, b, c, d, я могу начать aи использовать их, MySessionчтобы попытаться использовать bв качестве уже запущенного объекта, тогда как в действительности это не так.
Vorac
8

Основываясь на ответе Cashcow - зачем вам представлять вызывающему объект новый объект, когда вы можете просто представить новый интерфейс? Rebrand-шаблон:

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-Interface в начале и вы получите метод run (), доступный только тогда, когда вы вызвали start (); Со стороны это выглядит как шаблон с несколькими классами и объектами, но базовый класс остается одним классом, на который всегда ссылаются.

Falco
источник
1
В чем преимущество наличия только одного базового класса вместо нескольких? Поскольку это единственное отличие от предложенного мною решения, меня бы заинтересовал этот конкретный момент.
Михаэль Ле Барбье Грюневальд
1
@ MichaelGrünewald Необязательно реализовывать все интерфейсы с одним классом, но для объекта типа конфигурации, это может быть самый простой способ реализации для обмена данными между экземплярами интерфейсов (т. Е. Потому что они являются общими благодаря тому, что они одинаковы объект).
Джошуа Тейлор
1
По сути, это шаблон построения шагов .
Джошуа Тейлор
@JoshuaTaylor Совместное использование данных между экземплярами интерфейса имеет две стороны: хотя это может быть проще для реализации, мы должны быть осторожны, чтобы не получить доступ к «неопределенному состоянию» (например, к адресу клиента неподключенного сервера). Поскольку OP делает упор на удобство использования интерфейса, мы можем судить, что два подхода одинаковы. Спасибо вам за цитирование «образец строителя шагов».
Михаэль Ле Барбье Грюневальд
1
@ MichaelGrünewald Если вы взаимодействуете с объектом только через определенный интерфейс, указанный в данной точке, не должно быть никакого способа (без приведения и т. Д.) Получить доступ к этому состоянию.
Джошуа Тейлор
2

Существует много подходящих подходов для решения вашей проблемы. Бэзил Старынкевич предложил подход «нулевой бюрократии», который оставляет вам простой интерфейс и полагается на программиста, использующего соответствующий интерфейс. Несмотря на то, что мне нравится этот подход, я представлю другой, который имеет больше возможностей, но позволяет компилятору отлавливать некоторые ошибки.

  1. Определение различных состояний устройство может быть, так как Uninitialised, Started, Configuredи так далее. Список должен быть конечным.

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

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

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

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

Хотя я описал в 3. уникальном DeviceStrategyклассе, есть ситуации, когда вы можете разделить функциональность, которую он предоставляет, на несколько классов.

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

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

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

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

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

Михаэль Ле Барбье Грюневальд
источник
1
Вместо того, чтобы определять отдельные структуры, может быть достаточно определить необходимые интерфейсы, которые должен представлять объект на каждой фазе. Тогда это шаблон построения ступеней .
Джошуа Тейлор
2

Используйте шаблон строителя.

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

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

Philipp
источник
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, которая будет создавать исполнителей с пользовательскими путями выполнения.

Сильвиу Бурча
источник
В другом комментарии вы назвали это реализацией Step Builder, но это не так. Если у вас есть экземпляр MyStepsRunnable, вы можете вызвать step3 до step1. Реализация пошагового компоновщика была бы более похожа на ideone.com/UDECgY . Идея состоит в том, чтобы получить что-то только с помощью step2, запустив step1. Таким образом, вы вынуждены вызывать методы в правильном порядке. Например, см. Stackoverflow.com/q/17256627/1281433 .
Джошуа Тейлор
Вы можете преобразовать его в абстрактный класс с защищенными методами (или даже по умолчанию), чтобы ограничить его использование. Вы будете вынуждены использовать исполнителя, но у меня есть некоторые недостатки в текущей реализации.
Сильвиу Бурча
Это все еще не делает это строителем шага. В вашем коде пользователь ничего не может сделать для запуска кода между различными шагами. Идея не только в коде последовательности (независимо от того , его публичным или частным порядком , или иным образом инкапсулируются). Как показывает ваш код, сделать это достаточно просто step1(); step2(); step3();. Задача построителя шагов состоит в том, чтобы предоставить API, который предоставляет некоторые шаги, и обеспечить последовательность, в которой они вызываются. Это не должно мешать программисту делать другие вещи между шагами.
Джошуа Тейлор