Идиомы Attorney-Client и Passkey для выборочного доступа к методам класса

от автора

При проектировании приложений на C++ временами возникает необходимость предоставления доступа к закрытым методам класса другому классу или свободной функции. Для этого в языке C++ есть ключевое слово friend, которое предоставляет полный доступ не только к публичному интерфейсу класса, но и к закрытому, и всем деталям реализации. Таким образом friend работает по принципу «все или ничего» и «все» может быть слишком много. Например, когда есть класс Facade и несколько клиентов Client1, Client2, то может потребоваться предоставить каждому клиенту доступ только к определенному набору методов, причем каждому клиенту к своему набору, не предоставляя доступа к деталям реализации. Для решения такой задачи в C++ есть все возможности. В этой статье я расскажу про две идиомы Attorney-Client и Passkey и как их использовать с нулевыми накладными расходами.

Итак задача такая: есть классы Server, Client и Intruder. Клиент должен получить доступ к Server::some_method(), но не к деталям реализации. При этом Intruder не должен получить доступ к Server.

class Server { private: // закрытый интерфейс     void some_method(); // метод для Client     void one_more_method(); // этот метод должен остаться закрытым private:     // далее детали реализации класса... }; class Client; class Intruder; 

Attorney-Client

Идиома Attorney-Client более простая и прямолинейная, но длинная — с нее и начнем. Для предоставления Client требуемого доступа нельзя просто сделать его другом Server (он получит доступ ко всему содержимому сервера), также нельзя просто сделать требуемый метод публичным (к нему получит доступ и взломщик). В такой ситуации на помощь приходит доверенный посредник, а точнее Attorney.

class Attorney; 

Цепочка доверия будет организована таким образом: Client будет другом Attorney, а тот другом Server. В классе Attorney будет закрытый inline static метод, проксирующий запросы к Server.

class Server { private: // закрытый интерфейс     void some_method(); // метод для Client     void one_more_method(); // этот метод должен остаться закрытым private:     // далее детали реализации класса...      friend class Attorney; };  class Attorney { private:     static void proxy_some_method( Server& server )     {         server.some_method();     }     friend class Client; };  class Client { private:     void do_something(Server& server); };  void Client::do_something( Server& server ) {     // server.some_method(); // <- так не сработает     Attorney::proxy_some_method( server );     // server.one_more_method(); // <- этот метод тоже не доступен }  class Intruder { private:     void do_some_evil_staff( Server& server )     {         // server.some_method(); // <- это не сработает     } }; 

  • Прокси методы в классе-адвокате должны быть inline, тогда любой оптимизатор их удалит и будет напрямую вызывать методы класса CardAccount. Это довольно легко проверить скопипастив код на godbolt и сравнить генерируемый код для варианта с proxy_some_method() и прямого вызова (поменяв private на public).
  • Доступ к закрытым методам можно предоставлять и свободной функции. Для этого нужно ее назначить другом в классе-адвокате.

Passkey

Второй способ предоставления выборочного доступа к закрытому интерфейсу — идиома Passkey. Она короче и код получается чище, поэтому, мне нравится больше, но чуть более неочевидная. Задача та же: Server, Client, Intruder, но на этот раз прокси методы объявляются публичными, однако к ним добавляется специальный параметр Passkey с закрытым конструктором, который может быть вызван только явно перечисленными друзьями (классами, свободными функциями). Параметр Passkey служебный, его создают непосредственно в момент вызова прокси-функции и он уничтожается при выходе из нее (это временный объект, его не сохраняют в переменную). В результате void some_method( Passkey ) может вызвать только тот класс, который сможет вызвать конструктор Passkey (а все эти классы перечислены в друзьях Passkey).

class Server { public:     class Passkey     {     private:         friend class Client; // только Client сможет вызвать конструктор Passkey         Passkey() noexcept {}         Passkey( Passkey&& ) {}     };     void some_method( Passkey ) // экземпляр Passkey может создать только Client     {         some_method();     }  private: // закрытый интерфейс     void some_method(); // метод для Client     void one_more_method(); // этот метод должен остаться закрытым private:     // далее детали реализации класса... };  class Client { private:     void do_something( Server& server ); };  void Client::do_something( Server& server ) {     // server.some_method(); // <- так не сработает     // server.one_more_method(); // <- этот метод тоже не доступен     server.some_method( Server::Passkey() );     // или так, если не возникает неопределенности при вызове перегруженных методов     server.some_method( {} ); }  class Intruder { private:     void do_some_evil_staff( Server& server )     {         // server.some_method(); // <- это не сработает     } }; 

Для улучшения читаемости и избавления от дублирования включения кода класса Passkey в других классах, его можно сделать шаблонным и вынести в отдельный заголовочный файл.

template <typename T> class Passkey { private:     friend T;     Passkey() noexcept {}     Passkey( Passkey&& ) {}     Passkey( const Passkey& ) = delete;     Passkey& operator=( const Passkey& ) = delete;     Passkey& operator=( Passkey&& )      = delete; }; 

Единственное назначение Passkey в том, чтобы создать его временный экземпляр и передать в proxy метод, для этого нужны пустые конструктор по умолчанию и перемещения, все остальные конструкторы и операторы присваивания запрещены (на всякий случай, чтобы не использовать Passkey не по назначению).

Окончательный вариант

// === passkey.hpp template <typename T> class Passkey { private:     friend T;     Passkey() noexcept {}     Passkey( Passkey&& ) {}      Passkey( const Passkey& ) = delete;     Passkey& operator=( const Passkey& ) = delete;     Passkey& operator=( Passkey&& )      = delete; };  // === server.hpp class Client; class SuperClient;  class Server { public:     void proxy_some_method( Passkey<Client> ); // proxy для Client     void proxy_some_method( Passkey<SuperClient> ); // proxy для SuperClient  private: // закрытый интерфейс     void some_method(); // метод для Client     void one_more_method(); // этот метод должен остаться закрытым private:     // далее детали реализации класса... };  inline void Server::proxy_some_method( Passkey<Client> ) {     some_method(); }  inline void Server::proxy_some_method( Passkey<SuperClient> ) {     some_method(); }  // === client.hpp class Client { private:     void do_something( Server& server ); };  void Client::do_something( Server& server ) {     // server.some_method(); // <- так не сработает     // server.one_more_method(); // <- этот метод тоже не доступен     server.proxy_some_method( Passkey<Client>() );     // server.proxy_some_method( {} ); // <- на этот раз возникает неопределенность в перегруженных методах }  // evil.hpp class Intruder { private:     void do_some_evil_staff( Server& server )     {         // server.some_method(); // <- это не сработает         // server.proxy_some_method( Passkey<Client>() ); // и это тоже         // server.proxy_some_method( {} ); // и это...     } }; 

Вызывающие классы (Client, SuperClient) опять же смогут вызвать только каждый “свои” публичные методы, для которых смогут сконструировать параметр Passkey. Детали реализации Server им совсем недоступны, как и “чужие” методы.

  • В этом варианте прокси-функции также должны быть inline и просто проксировать вызов дальше, в таком случае (после работы оптимизатора) никакой временный объект Passkey<> создаваться не будет и накладные расходы будут нулевыми.
  • Passkey<> нельзя делать аргументом по умолчанию, т.е. такой вариант не сработает:

class Server { public:     void proxy_some_method( Passkey<Client> pass = Passkey<Client>() ); private:     void some_method(); }; 

  • Прокси методы я назвал с префиксом proxy_ исключительно для учебных целей, чтобы было понятнее.

Заключение

Описанные идиомы Attorney-Client и Passkey позволяют выборочно предоставить доступ к закрытым методам класса. Оба эти способа работают с нулевыми накладными расходами времени выполнения, однако, требуют написания дополнительного кода и делают интерфейс класса не таким очевидным, по сравнению с использованием ключевого слова friend. Нужно ли городить весь этот огород в Вашем проекте или оно того не стоит – это уже решать Вам.

ссылка на оригинал статьи https://habrahabr.ru/post/326540/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *