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

0d1bb0a845a84e4d8f716ccfc88c9cfb.jpg При проектировании приложений на 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 
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 
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 ); // proxy для Client
    void proxy_some_method( Passkey ); // proxy для SuperClient

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

inline void Server::proxy_some_method( Passkey )
{
    some_method();
}

inline void Server::proxy_some_method( Passkey )
{
    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() );
    // server.proxy_some_method( {} ); // <- на этот раз возникает неопределенность в перегруженных методах
}

// evil.hpp
class Intruder
{
private:
    void do_some_evil_staff( Server& server )
    {
        // server.some_method(); // <- это не сработает
        // server.proxy_some_method( Passkey() ); // и это тоже
        // server.proxy_some_method( {} ); // и это...
    }
};


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


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

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

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

Заключение


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

Комментарии (8)

  • 14 апреля 2017 в 17:00

    +1

    Во всех подобных случаях, гораздо полезней не запрещать, а скрывать реализацию.
    например, когда есть класс Facade и несколько клиентов Client1, Client2, то может потребоваться предоставить каждому клиенту доступ только к определенному набору методов, причем каждому клиенту к своему набору, не предоставляя доступа к деталям реализации…

    Оба «паттерна», на самом деле ничего не скрывают. Оба клиента, по-прежнему, видят все «кишки» и зависят друг от друга. А нужно, и всего-то, просто предоставить два интерфейса: InterfaceForClient1, и InterfaceForClient2. В этом случае вы действительно решите поставленную цель.
    • 14 апреля 2017 в 17:59

      0

      А нужно, и всего-то, просто предоставить два интерфейса: InterfaceForClient1, и InterfaceForClient2
      Чем это отличается от создания отдельных Attorney для каждого из клиентов?
      • 14 апреля 2017 в 18:13

        0

        Так если ничем не отличается, зачем плодить новые сущности без необходимости. Про бритву Оккама слышали.
        • 14 апреля 2017 в 18:16

          0

          Так если ничем не отличается
          Простите, но это я у вас хотел узнать: отличаются или не отличаются? Можете на этот вопрос ответить?
  • 14 апреля 2017 в 17:28

    0

    Через интерфейсы будет удобнее.
    Вообще, считаю появление friend — поводом задуматься об архитектуре.
    Если вернуться к статье, то последний вариант (Passkey)
    можно написать просто так:
    // === server.hpp
    class Client;
    class SuperClient;
    
    class Server
    {
    public:
        void proxy_some_method( Client& ); // proxy для Client
        void proxy_some_method( SuperClient& ); // proxy для SuperClient
    
    private: // закрытый интерфейс
        void some_method(); // метод для Client
        void one_more_method(); // этот метод должен остаться закрытым
    private:
        // далее детали реализации класса...
    };
    
    inline void Server::proxy_some_method( Client& )
    {
        some_method();
    }
    
    inline void Server::proxy_some_method( 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( *this );
    }
    
    // evil.hpp
    class Intruder
    {
    private:
        void do_some_evil_staff( Server& server )
        {
            // server.some_method(); // <- это не сработает
            // server.proxy_some_method( *this ); // и это тоже
            // server.proxy_some_method( Client() ); // можно сломать так, но зачем...
        }
    };
    


    Хотя выходит несколько странно.
    • 14 апреля 2017 в 17:45

      0

      Согласен, мой пример хуже Passkey.
  • 14 апреля 2017 в 17:47

    0

    Согласен насчет friend-a. Вообще friend обеспечивает самую слабую степень инкапсуляции и является костылем для случаев, когда у вас нет другого выхода. Например, в случае каких-то сторонних библиотек или legacy.

    Algoritmist А вам не кажется что у вас просто Visitor получился?

    • 14 апреля 2017 в 17:55

      0

      Да, что-то погорячился…

© Habrahabr.ru