При написании библиотек иногда сталкиваешься с такой ситуацией: есть некий публичный класс X с набором публичных методов (т.е. этот класс видит и использует пользователь библиотеки). Есть набор других классов в этой библиотеке, не важно публичных или нет, которые так или иначе используют класс X. Пока они работают только с публичными методами класса X, то все хорошо. Но что делать, если им требуется получить доступ к закрытой части X?
Для того, чтобы не уходить в дебри абстракции, перейду к более приближенным к реальности вещам, с которыми приходится иметь дело. Допустим, что есть класс Environment, который является оберткой над всеми потрохами библиотеки. Пользователю создает экземпляр Environment, после чего дергает те или иные публичные методы Environment-а для управления библиотекой. Например, Environment может выглядеть вот так:
class Environment { public : void start(); void shutdown(); void join(); template< typename AGENT, typename... ARGS > AGENT * start_agent( ARGS &&... args ); template< typename DISPATCHER, typename... ARGS > DISPATCHER * start_dispatcher( ARGS &&... args ); ... }; |
Есть так же публичные классы Agent и Dispatcher, которые хранят в себе ссылку на Environment:
class Agent { Environment & m_env; public : Agent( Environment & env ) : m_env{ env } {} ... }; class Dispatcher { Environment & m_env; public : Dispather( Environment & env ) : m_env{ env } {} ... }; |
И вот в своей работе Agent-ам и Dispatcher-ам потребовалось обращаться к методам Environment, которые не должны быть доступны пользователю.
Например, появляется класс MessageTracer. Его задача -- осуществлять трассировку того, как Agent-ы и Dispatcher-ы выполняют обработку сообщений. Экземпляр MessageTracer может создаваться в конструкторе Environment-а и храниться внутри Environment-а. В своем публичном интерфейсе Environment не предоставляет метода для доступа к MessageTracer-у, т.к. эта часть внутренней кухни и пользователя нужно держать от этой кухни подальше. Не только для того, чтобы пользователь своими шаловливыми ручками чего-нибудь не сломал. Но и для того, чтобы пользователю не пришлось переделывать свой код, если со временем MessageTracer будет заменен на какой-нибудь InternalsTracer и StatisticsCollector.
Итак, грубо говоря, класс Environment принимает вид:
class Environment { public : void start(); void shutdown(); void join(); template< typename AGENT, typename... ARGS > AGENT * start_agent( ARGS &&... args ); template< typename DISPATCHER, typename... ARGS > DISPATCHER * start_dispatcher( ARGS &&... args ); ... private : MessageTracer * m_msg_tracer; }; |
И задача сводится к тому, чтобы дать классам Agent и Dispatcher возможность обращаться к Environment::m_msg_tracer.
Первое, что приходит в голову -- это сделать Agent и Dispatcher френдами для Environment:
class Environment { friend class Agent; friend class Dispatcher; public : |
Однако, это не очень хороший способ, на мой взгляд. Во-первых, список френдов со временем может разрастись до неприличных размеров и в этот список, возможно, придется включать какие-то внутренние классы, о которых пользователю и знать-то незачем. Например, класс impl::AgentRegistrationProcedure, используемый при операции старта нового агента -- это же детали реализации, незачем их выносить в публичный интерфейс.
Во-вторых, класс Dispatcher легко объявить френдом для Environment, но вот наследники Dispatcher френдами для Environment уже не будут. Что ведет нас либо к необходимости включать наследников Dispatcher в список друзей Environment индивидуально, либо же к необходимости в Dispatcher делать набор методов для доступа к внутренностям Environment-а, вроде вот такого:
class Dispatcher { Environment & m_env; protected : MessageTracer * msg_tracer() { return m_env.m_msg_tracer; } public : Dispather( Environment & env ) : m_env{ env } {} ... }; |
К счастью, такая проблема с разделением интерфейса Environment на открытую и закрытие части возникает не часто. Но когда она возникает, хочется использовать что-то красивое, что не выглядело бы, как набор костылей.
На днях как раз столкнулся с такой проблемой и решил ее за счет введения дополнительного класса, имя которого видно пользователю, а вот реализация скрыта. Что-то вроде:
namespace impl { class InternalEnvironmentIface; } class Environment { friend class impl::InternalEnvironmentIface; public : void start(); void shutdown(); void join(); ... }; |
Сам же класс InternalEnvironmentIface определен в заголовочном файле, лежащем в потрохах библиотеки (т.е. он не часть публичного интерфейса библиотеки). При этом InternalEnvironmentIface является очень простой оберткой вокруг ссылки на Environment:
class InternalEnvironmentIface { Environment & m_env; public : InternalEnvironmentIface( Environment & env ) : m_env{ env } {} inline MessageTracer * msg_tracer() { return m_env.m_msg_tracer; } }; |
И используется он вот таким образом:
auto tracer = InternalEnvironmentIface{m_env}.msg_tracer(); |
Что дает возможность оптимизирующему компилятору свести все это дело к простому обращению к полю класса Environment.
Вот такой простой способ.
Одним из его достоинств является то, что приватный интерфейс Environment-а можно нарезать на более тонкие ломтики. Так, наследникам Agent может быть доступен один набор приватных методов/полей Environment, а наследникам Dispatcher -- другой. Можно сделать несколько классов, скажем, InternalEnvironmentIfaceForAgents и InternalEnvironmentIfaceForDispatchers, со своими наборами методов.
Еще этот способ удобен в случае, когда Environment не является простым классом. А, например, реализуется посредством идиомы PImpl. Т.е. когда детали Environment вообще скрыты от всех:
class Environment { friend class impl::InternalEnvironmentIface; public : void start(); void shutdown(); void join(); ... template< typename AGENT, typename... ARGS > AGENT * start_agent( ARGS &&... args ); template< typename DISPATCHER, typename... ARGS > DISPATCHER * start_dispatcher( ARGS &&... args ); ... private : struct Implementation; std::unique_ptr<Implementation> m_impl; }; |
Правда, в этом случае InternalEnvironmentIface уже не будет простой оберткой с inline-овыми методами, детали его реализации так же будут скрыты. Но зато есть возможность реализовать InternalEnvironmentIface там же, где и Environment::Implementation и никому вообще не показывать, как же там все устроено. Что резко сокращает количество зависимостей между частями библиотеки. Правда, ценой дополнительной косвенности (но в случае PImpl-а эту цену всегда нужно платить).
Вот такой простой способ. Ни в коем случае не претендую на оригинальность. Скорее всего он мне на глаза где-то попадался, поэтому и вспомнился в нужный момент. Но где и когда с таким подходом сталкивался уже не вспомню :)
Так же, наверняка, использование подобных трюков свидетельствует о том, что где-то на более ранних стадиях проектирования была допущена ошибка и наличие одного Environment-а является просчетом. Вероятно, нужно было изначально делать Environment и, скажем, impl::ActualEnvironment, доступный только внутри библиотеки. Но в свое оправдание могу сказать то, что при сопровождении старого кода не всегда есть возможность устранить такие серьезные просчеты, допущенные много лет назад.
Комментариев нет:
Отправить комментарий