Я работаю над большим проектом на C ++, который будет иметь графический интерфейс пользователя.
Пользовательский интерфейс будет использовать некоторый шаблон проектирования (MVVM / MVC), который будет опираться на шаблон наблюдателя.
Моя проблема в том, что в настоящее время у меня нет возможности предсказать, какие части Модели должны быть наблюдаемыми. И есть много, много частей.
Из-за этой проблемы меня тянет в нескольких направлениях:
Я чувствую, что из этих 3 (1), вероятно, является меньшим злом.
Тем не менее, я чувствую, что идеальное решение на самом деле должно существовать на каком-то языке (определенно не на C ++), но я не знаю, поддерживается ли оно где-нибудь.
Решение о единороге, о котором я думал, выглядит примерно так:
Учитывая класс данных, не должно быть возможным для клиентов, которые стремятся сделать данные наблюдаемыми сделать что-то вроде
@MakeObservable (Данные)
как время компиляции. Это, в свою очередь, позволило бы вызвать addObserver для объектов данных и изменить все назначения для элементов данных с помощью уведомителей. Это также заставит вас платить за производительность только за то, что вы получаете.
Итак, мой вопрос состоит из двух частей:
Если я правильно понимаю, вы обеспокоены стоимостью предоставления сигнала / уведомления для потенциально каждого наблюдаемого property
каждого объекта.
К счастью, вам повезло, поскольку хранение общего поточно-ориентированного уведомителя с каждым отдельным свойством каждого объекта обычно обходится очень дорого на любом языке или системе.
Вместо того, чтобы быть умным и пытаться решить эту проблему во время компиляции, что я рекомендовал бы исключить некоторые очень потенциально полезные опции для крупномасштабного проекта (например: плагины и скрипты), я бы посоветовал подумать о том, как сделать это дешевле во время выполнения. Вы хотите, чтобы ваши сигналы были сохранены на более грубом уровне, чем отдельные свойства объекта.
Если вы храните только один объект, который передает соответствующие данные о том, какое свойство было изменено во время события изменения свойства, чтобы отфильтровать клиентов, которых нужно уведомить, то теперь мы получаем намного дешевле. Мы обмениваем некоторые дополнительные разветвления и более крупные агрегаты для подключенных слотов, но вы получаете значительно меньший объект в обмен на потенциально более быстрый доступ для чтения, и я полагаю, что это очень ценный обмен на практике.
Вы все еще можете спроектировать свой открытый интерфейс и даже механизм уведомления о событиях так, чтобы клиенты работали с системой так, как будто они подключаются к свойствам, а не ко всему объекту, возможно, даже вызывая методы в свойстве (если это объект / proxy) для подключения слотов, если вам нужно или можете позволить себе обратный указатель на объект из свойства.
Если вы не уверены, я бы допустил ошибку при присоединении слотов событий к свойствам, а также их изменении как части интерфейса объекта, а не интерфейса свойств, поскольку у вас будет гораздо больше возможностей для оптимизации в обмен на немного другая эстетика клиента (та, которую я на самом деле не считаю менее удобной, чем просто «другая», или, по крайней мере, потенциально оправдывающая затраты на устранение обратного указателя на свойство).
Это в области удобства и вещей типа обертки. Но вам не нужно нарушать принцип открытого-закрытого доступа для достижения дизайна MVP в C ++. Не забивайтесь в угол представлением данных. У вас есть большая гибкость на уровне публичного интерфейса.
Обнаружив, что эффективность играет здесь важную роль, я бы предложил несколько основных способов мышления, чтобы помочь с этим.
Во-первых, только потому, что объект имеет какой-то something()
не означает, что связанные данные должны храниться в этом объекте. Его даже не нужно нигде хранить, пока не будет вызван этот метод. Если память является вашей заботой, она может храниться на каком-то внешнем уровне.
Большая часть программного обеспечения разбивается на иерархии агрегатов, владеющих ресурсами. Например, в трехмерном программном обеспечении вершина принадлежит мешу, который принадлежит графу сцены, который принадлежит корню приложения.
Если вам нужны дизайны, в которых вы практически не платите за память для вещей, которые не используются, то вы хотите связать данные с объектом на более грубом уровне. Если вы храните его непосредственно в объекте, то каждый объект платит за something()
возвращается независимо от того, нужно ли это. Если вы храните его косвенно в объекте с указателем, то вы платите за указатель на something()
но не для полной стоимости этого, если это не используется. Если вы связываете его с владельцем объекта, то его поиск требует затрат на поиск, но он не такой дорогой, как его привязка к владельцу владельца объекта.
Так что всегда есть способы получить что-то очень близкое к бесплатному для вещей, которые вы не используете, если вы общаетесь на достаточно грубом уровне. На детализированных уровнях вы уменьшаете издержки поиска и косвенного обращения, на грубых уровнях вы уменьшаете затраты на вещи, которые вы не используете.
Учитывая огромную масштабируемость, связанную с обрабатываемыми миллионами и миллиардами элементов, и все же желание потенциально некоторых из них генерировать события, если вы можете использовать асинхронный дизайн, я действительно рекомендую это здесь. У вас может быть свободная от блокировки очередь событий для каждого потока, для которой объекты с одним битовым флагом генерируют события. Если битовый флаг не установлен, они не установлены.
Этот вид отложенного асинхронного проектирования полезен при таком масштабе, поскольку он дает вам периодические интервалы (или, возможно, просто другие потоки, хотя вам понадобятся блокировки записи — а также блокировки чтения, хотя запись — это то, что нужно дешево — в в этом случае) для опроса и выделения полных ресурсов для массовой обработки очереди, в то время как более критичная по времени обработка может продолжаться без синхронизации с системой событий / уведомлений.
// Interned strings are very useful here for fast lookups
// and reduced redundancy in memory.
// They're basically just indices or pointers to an
// associative string container (ex: hash or trie).
// Some contextual class for the thread storing things like a handle
// to its event queue, thread-local lock-free memory allocator,
// possible error codes triggered by functions called in the thread,
// etc. This is optional and can be replaced by thread-local storage
// or even just globals with an appropriate lock. However, while
// inconvenient, passing this down a thread's callstack is usually
// the most efficient and reliable, lock-free way.
// There may be times when passing around this contextual parameter
// is too impractical. There TLS helps in those exceptional cases.
class Context;
// Variant is some generic store/get/set anything type.
// A basic implementation is a void pointer combined with
// a type code to at least allow runtime checking prior to
// casting along with deep copying capabilities (functionality
// mapped to the type code). A more sophisticated one is
// abstract and overriden by subtypes like VariantInt
// or VariantT<int>
typedef void EventFunc(Context& ctx, int argc, Variant** argv);
// Your universal object interface. This is purely abstract:
// I recommend a two-tier design here:
// -- ObjectInterface->Object->YourSubType
// It'll give you room to use a different rep for
// certain subtypes without affecting ABI.
class ObjectInterface
{
public:
virtual ~Object() {}
// Leave it up to the subtype to choose the most
// efficient rep.
virtual bool has_events(Context& ctx) const = 0;
// Connect a slot to the object's signal (or its property
// if the event_id matches the property ID, e.g.).
// Returns a connection handle for th eslot. Note: RAII
// is useful here as failing to disconnect can have
// grave consequences if the slot is invalidated prior to
// the signal.
virtual int connect(Context& ctx, InternedString event_id, EventFunc func, const Variant& slot_data) = 0;
// Disconnect the slot from the signal.
virtual int disconnect(Context& ctx, int slot) = 0;
// Fetches a property with the specified ID O(n) integral cmps.
// Recommended: make properties stateless proxies pointing
// back to the object (more room for backend optimization).
// Properties can have set<T>/get<T> methods (can build this
// on top of your Variant if desired, but a bit more overhead
// if so).
// If even interned string compares are not fast enough for
// desired needs, then an alternative, less convenient interface
// to memoize property indices from an ID might be appropriate in
// addition to these.
virtual Property operator[](InternedString prop_id) = 0;
// Returns the nth property through an index.
virtual Property operator[](int n) = 0;
// Returns the number of properties for introspection/reflection.
virtual int num_properties() const = 0;
// Set the value of the specified property. This can generate
// an event with the matching property name to indicate that it
// changed.
virtual void set_value(Context& ctx, InternedString prop_id, const Variant& new_value) = 0;
// Returns the value of the specified property.
virtual const Variant& value(Context& ctx, InternedString prop_id) = 0;
// Poor man's RTTI. This can be ignored in favor of dynamic_cast
// for a COM-like design to retrieve additional interfaces the
// object supports if RTTI can be allowed for all builds/modes.
// I use this anyway for higher ABI compatibility with third
// parties.
virtual Interface* fetch_interface(Context& ctx, InternedString interface_id) = 0;
};
Я не буду вдаваться в подробности представления данных — суть в том, что они гибкие. Что важно, так это купить себе комнату, чтобы изменить ее по мере необходимости. Сохранение объекта абстрактным, сохранение свойства в качестве прокси без сохранения состояния (за исключением обратного указателя на объект) и т. Д. Дает много места для профилирования и оптимизации.
Для асинхронной обработки событий каждый поток должен иметь связанную очередь, которая может быть передана через стек вызовов через этот Context
справиться. Когда происходят события, такие как изменение свойства, объекты могут передавать события в эту очередь, если has_events() == true
, Точно так же, connect
не обязательно добавляет какое-либо состояние к объекту. Он может создать ассоциативную структуру, опять же через Context
, который отображает объект / event_id на клиента. disconnect
также удаляет его из этого источника центрального потока. Даже действие по подключению / отключению слота к / от сигнала может быть передано в очередь событий для центрального, глобального места для обработки и создания соответствующих ассоциаций (опять же, предотвращая оплату любой стоимости памяти объектами без наблюдателей).
При использовании этого типа конструкции каждый поток должен иметь в своей точке входа обработчик выхода для потока, который передает события, помещенные в очередь событий потока, из локальной очереди потока в некоторую глобальную очередь. Это требует блокировки, но может быть сделано не слишком часто, чтобы избежать сильной конкуренции и чтобы не замедлять работу каждого потока из-за обработки событий в критических для производительности областях. Наподобие thread_yield
Вид функции также должен быть обеспечен такой конструкцией, которая также переносит из локальной очереди потока в глобальную очередь для долгоживущих потоков / задач.
Глобальная очередь обрабатывается в другом потоке, вызывая соответствующие сигналы для подключенных слотов. Там он может сосредоточиться на массовой обработке очереди, когда она не пуста, спящем / уступающем, когда она есть. Весь смысл всего этого состоит в том, чтобы повысить производительность: отправка в очередь является чрезвычайно дешевой по сравнению с возможностью отправки синхронных событий каждый раз, когда изменяется свойство объекта, а при работе с крупномасштабными входными данными это может быть очень дорогостоящим. , Поэтому простое добавление в очередь позволяет этому потоку избежать затрат времени на обработку событий, откладывая его на другой поток.
Временная библиотека может помочь вам полностью отделить GUI и доменную логику и сделать гибкими / расширяемыми / простыми в обслуживании системы сообщений и уведомителей.
Однако есть один недостаток — для этого вам нужна поддержка C ++ 11.
Проверьте эту статью укрощение
И пример в github: DecoupledGuiExamples
Таким образом, вам, вероятно, не нужны уведомители для каждого класса, вы можете просто снимать сообщения из внутренних функций и из определенных классов, где вы можете заставить любой класс отправлять любое сообщение, которое вы хотите, в GUI.