C++でif/elseの地獄から脱出する方法

毎日の英語学習、少しずつ成長:
  • "Without purpose, the days would have ended, as such days always end, in disintegration."
  • 「目標なくして、一日は終わる。それはいつも、バラバラな形で終わる。」

はじめに

以前、羊さん(羊哥)が書いた面白い記事『答应我,别再if/else走天下了可以吗 | CodeSheep 』があり、そこではJava言語を使って列挙型、ファクトリーパターン、ストラテジーパターンの3つの方法でif/elseの連鎖を排除する方法を紹介していました。内容は段階的に進み、浅いところから深いところへと進む形式で、とても好きです。

コメントの中にC++バージョンを求める声があったので、この記事を書きました(羊さんの許可を得ました)。しかし、C++には列挙型がないため、この方法は含まれません。しかし、この記事では、読者の方々にファクトリーパターンとストラテジーパターンの最適化方法を一つずつご紹介します。

本文

悪夢のif/else連鎖

if/elseは私たちがプログラミングを学ぶ際に最初に学ぶ分岐文です。簡単で理解しやすく、日常生活にもif/elseの例はたくさんあります:

妻がプログラマーの夫に電話:「下りしきって、一斤の包子を買ってきて。もしスイカを売っているのを見つけたら、一つ買ってきてね。」 その夜、プログラマーの夫は一つの包子を手に家に入ってきた... 妻が怒って言う:「なぜ包子が一つしか買ってこなかったの?!」 夫は答える:「スイカを売っているのを見つけたから。」

妻の思考:

一斤の包子を買う;
if( スイカを売っているのを見つけた )
  一つ(スイカ)を買う;

プログラマーの夫のプログラム:

if( スイカを売っているのを見つけなかった )
   一斤の包子を買う;
else
   一つ(包子)を買う;

非常に生き生きとした生活の例です!もしプログラマーであるあなたが同じ思考ミスを犯したら、妻に「なぜ?」と聞くのをやめて、キーボードで跪きなさい

本題に入りましょう。以下の例を考えてみましょう:一般的に、通常のバックエンド管理システムには「ロール(役割)」という概念があり、異なる管理者は異なる権限を持ち、行える操作も異なります。

  • システム管理者(ROLE_ROOT_ADMIN):A操作の権限を持つ
  • 注文管理者(ROLE_ORDER_ADMIN):B操作の権限を持つ
  • 一般ユーザー(ROLE_NORMAL):C操作の権限を持つ

ユーザーがログインしてきた場合、そのユーザーのロールに基づいてどのような行動ができるかを判断する必要があります。if/elseの連鎖を多用する私たちにとって、これは簡単に思えるでしょう。连环的(連鎖的な)な書き方を披露します:

class RoleEvaluator
{
public:
    std::string evaluate( std::string userRole )
    {
        std::string permissionInfo = "";
        if( userRole == "ROLE_ROOT_ADMIN" )       // システム管理者
        {
            permissionInfo = userRole + "はA権限を持っています";
        }
        else if( userRole == "ROLE_ORDER_ADMIN" ) // 注文管理者
        {
            permissionInfo = userRole + "はB権限を持っています";
        }
        else if( userRole == "ROLE_NORMAL" )       // 一般ユーザー
        {
            permissionInfo = userRole + "はC権限を持っています";
        }
        return permissionInfo;
    }
};

システムに数十のロールがある場合、それは数十のif/elseのネストになります。この視覚効果は絶対に酸っぱくて...この実装方法は非常に不格好です。

他人がこのコードを見たら、「クソ、どこの水物が書いたんだ!」と大声で叫ぶことでしょう!

この時、あなたがそれを聞いたら、「じゃあswitch/caseに変えよう」と言わないでください。言わないでください、言わないでくださいよ、さもないとクビになります...

なぜならswitch/caseif/elseと毛ほどの違いもなく、書くのが大変で、読みにくく、拡張しにくいコードだからです。

ここから、いくつかの改善方法を簡単に紹介します。if/elseに頼りすぎないでください。

ファクトリーパターン —— 香りはありませんか?

異なるロールが異なることをするということは、明らかにファクトリーパターンを使用する絶好の機会です。私たちは異なる状況を個別に定義し、それをファクトリーに集約するだけです。

まず、共通のインターフェースPermissionHandlerを定義します。クラスには、派生クラス(子クラス)で具体的に実装される純粋仮想関数handleがあります:

// 基底クラス
class PermissionHandler
{
public:
    virtual std::string handle() = 0; // 純粋仮想関数
    virtual ~PermissionHandler() {} // 仮想デストラクタ
};

次に、異なるロールクラスに対して、基底クラスを継承し、handle関数を実装します:

// システム管理者(A操作権限を持つ)
class RootHandler : public PermissionHandler {
public:
    RootHandler(const std::string &roleName)
            : m_RoleName(roleName) {}

    std::string handle() {
        return m_RoleName + "はA権限を持っています";
    }

private:
    std::string m_RoleName;
};


// 注文管理者(B操作権限を持つ)
class OrderHandler : public PermissionHandler {
public:
    OrderHandler(const std::string &roleName)
            : m_RoleName(roleName) {}

    std::string handle() {
        return m_RoleName + "はB権限を持っています";
    }

private:
    std::string m_RoleName;
};

// 一般ユーザー(C操作権限を持つ)
class UserHandler : public PermissionHandler {
public:
    UserHandler(const std::string &roleName)
            : m_RoleName(roleName) {}

    std::string handle() {
        return m_RoleName + "はC権限を持っています";
    }

private:
    std::string m_RoleName;
};

次に、ファクトリークラスPermissionFactoryを書きます。以下の2つのインターフェースを提供します:

  • ファクトリーにロールのポインターオブジェクトを登録するためのregisterHandlerメンバ関数
  • 対応するロールのポインターオブジェクトを取得するためのgetHandlerメンバ関数
// ロールファクトリー
class PermissionFactory {
public:
    // ファクトリーのシングルトンインスタンスを取得する。ファクトリーのインスタンスは一意です。
    static PermissionFactory& Instance() {
        static PermissionFactory instance; // C++11以上でスレッドセーフ
        return instance;
    }

    // ポインターオブジェクトをファクトリーに登録する
    void registerHandler(const std::string& name, PermissionHandler* registrar) {
        m_HandlerRegistry[name] = registrar;
    }

    // 名前nameに基づいて、対応するロールのポインターオブジェクトを取得する
    PermissionHandler* getHandler(const std::string& name) {

        std::map<std::string, PermissionHandler*>::iterator it;

        // mapから登録済みのロールを見つけ、ロールのポインターオブジェクトを返す
        it = m_HandlerRegistry.find(name);
        if (it != m_HandlerRegistry.end()) {
            return it->second;
        }

        return nullptr; // ロールが登録されていない場合はnullptrを返す
    }

private:
    // 外部からのコンストラクタとデストラクタを禁止
    PermissionFactory() {}
    ~PermissionFactory() {}

    // 外部からのコピーと代入操作を禁止
    PermissionFactory(const PermissionFactory &);
    const PermissionFactory &operator=(const PermissionFactory &);

    // 登録済みのロールを保存する。key:ロール名, value:ロールのポインターオブジェクト
    std::map<std::string, PermissionHandler *> m_HandlerRegistry;
};

すべてのロールをファクトリーに**登録(集約)**し、ロールの初期化関数InitializePermissionHandlersにカプセル化します:

void InitializePermissionHandlers() // ファクトリーにロールを初期化する
{
    static bool bInitialized = false;

    if (bInitialized == false) {
        // システム管理者を登録
        PermissionFactory::Instance().registerHandler("ROLE_ROOT_ADMIN", new RootHandler("ROLE_ROOT_ADMIN"));
        // 注文管理者を登録
        PermissionFactory::Instance().registerHandler("ROLE_ORDER_ADMIN", new OrderHandler("ROLE_ORDER_ADMIN"));
        // 一般ユーザーを登録
        PermissionFactory::Instance().registerHandler("ROLE_NORMAL", new UserHandler("ROLE_NORMAL"));
        bInitialized = true;
    }
}

上記のファクトリーを利用することで、ビジネスコードの呼び出しは一行で済み、if/elseは明確に排除されます:

class RoleEvaluator {
public:
    std::string evaluate(const std::string &userRole) {
        return PermissionFactory::Instance().getHandler(userRole)->handle();
    }
};

注意:Judgeを使用する際には、まずすべてのロールをファクトリーに初期化するInitializePermissionHandlers関数を呼び出す必要があります(main関数の先頭などに配置できます):

int main() {
    InitializePermissionHandlers(); // まずすべてのロールをファクトリーに初期化する

    RoleEvaluator evaluator;

    std::cout << evaluator.evaluate("ROLE_ROOT_ADMIN") << std::endl;
    std::cout << evaluator.evaluate("ROLE_ORDER_ADMIN") << std::endl;
    std::cout << evaluator.evaluate("ROLE_NORMAL") << std::endl;
}

ファクトリーパターンによる実装方法では、条件を拡張するのも簡単です。新しいコードを追加するだけで、以前のビジネスコードを変更する必要がなく、非常に「オープン・クローズド原則」に適合しています。

見つけましたか?上記のファクトリークラスの実装は、見た目は整然としていますが、不適切な使用をするとプログラムのクラッシュを招く可能性があります。どのような状況で発生するのでしょう?

まず、上記のファクトリークラスの外部インターフェースを分析してみましょう:

  • registerHandler:ロールのポインターオブジェクトをファクトリーに登録する
  • getHandler:ファクトリーからロールのポインターオブジェクトを取得する

ポインターオブジェクトが解放されずにリソースリークが発生するのでしょうか?いいえ、この問題ではありません。私たちはポインターオブジェクトを手動で解放する必要もありません。上記のファクトリーは「シングルトンパターン」であり、そのライフサイクルは最初の初期化からプログラム終了までです。プログラム終了後、オペレーティングシステムはファクトリークラス内のすべてのポインターオブジェクトのリソースを自然に回収します。

しかし、私たちがファクトリーから取得したロールのポインターオブジェクトを手動で解放すると、問題が発生します:

PermissionHandler* handlerPtr =  PermissionFactory::Instance().getHandler(userRole);
...
delete handlerPtr; // ポインターオブジェクトを手動で解放する

私たちがポインターオブジェクトを手動で解放すると、ファクトリーのmapに保存されているポインターオブジェクトはを指すようになり、次回使用時にプログラムのクラッシュを招きます!以下の例のように:

class RoleEvaluator {
public:
    std::string evaluate(const std::string &userRole) {
        PermissionHandler *handlerPtr = PermissionFactory::Instance().getHandler(userRole);
        std::string ret = handlerPtr->handle();
        delete handlerPtr; // ポインターオブジェクトを手動で解放する
        return ret;
    }
};

int main() {
    InitializePermissionHandlers(); // まずすべてのロールをファクトリーに初期化する

    RoleEvaluator evaluator;

    std::cout << evaluator.evaluate("ROLE_ROOT_ADMIN") << std::endl;
    std::cout << evaluator.evaluate("ROLE_ROOT_ADMIN") << std::endl; // エラー!プログラムはクラッシュして終了する!

    return 0;
}

上記のコードは、2回目にROLE_ROOT_ADMINロールのポインターオブジェクトを使用すると、プログラムがクラッシュします。なぜなら、ROLE_ROOT_ADMINロールのポインターオブジェクトは1回目の使用後に手動で解放されたため、ファクトリーのmapに保存されているのはnullptrだからです。

これを最適化できますか?なぜなら、プログラマーの一部はファクトリーから取得したポインターオブジェクトを手動で解放するからです。

上記のファクトリークラスの欠点は、newで初期化されたポインターオブジェクトが一度だけ初期化される点にあります。もしポインターオブジェクトを手動で解放すると、そのポインターオブジェクトは空を指すようになり、再度使用するとシステムのクラッシュを招きます。

この問題を改善するために、newによる初期化方法をファクトリークラスのポインターオブジェクトを取得するメンバ関数に移動させます。これにより、そのメンバ関数を呼び出すたびに、常に新しくnewで初期化されたポインターオブジェクトが返され、その時点で外部はポインターオブジェクトを手動で解放する必要があります

以下のファクトリークラスは、上記の問題を改善し、同時にテンプレート技術を採用してファクトリークラスをさらにカプセル化し、ロールクラスだけでなく、ポリモーフィズム特性を持つクラスであればどれでも使用できるようにしました。これは「万能」のファクトリークラスです:

次に、この新しい「万能」なファクトリーテンプレートクラスをこの例のロールオブジェクトに適用します。

1. ロールをファクトリーに登録(集約)する方法は、ProductRegistrarオブジェクトを構築することです。使用する際には注意してください:

  • テンプレートパラメータProductType_tは基底クラスを指定します(この例ではPermissionHandler
  • テンプレートパラメータProductImpl_tは派生クラスを指定します(この例ではRootHandler、OrderHandler、UserHandler

私たちは新しい登録(集約)方法を使用して、InitializePermissionHandlers初期化ロール関数を改善します。以下を参照してください:

void InitializePermissionHandlers() // ファクトリーにロールを初期化する
{
    static bool bInitialized = false;

    if (bInitialized == false) {
        // システム管理者を登録
        static ProductRegistrar<PermissionHandler, RootHandler> rootRegistrar("ROLE_ROOT_ADMIN");
        // 注文管理者を登録
        static ProductRegistrar<PermissionHandler, OrderHandler> orderRegistrar("ROLE_ORDER_ADMIN");
        // 一般ユーザーを登録
        static ProductRegistrar<PermissionHandler, UserHandler> normalRegistrar("ROLE_NORMAL");
        bInitialized = true;
    }
}

2. ファクトリーからロールのポインターオブジェクトを取得する関数はGetProductです。注意すべき点は:

  • ロールのポインターオブジェクトを使用した後、リソースを手動でdeleteする必要があること。

私たちは新しいロールオブジェクトの取得方法を使用して、RoleEvaluator::evaluate関数を改善します。以下を参照してください:

class RoleEvaluator {
public:
    std::string evaluate(const std::string &userRole) {
        ProductFactory<PermissionHandler>& factory = ProductFactory<PermissionHandler>::Instance();
        // ファクトリーから対応するポインターオブジェクトを取得する
        PermissionHandler *handlerPtr = factory.GetProduct(userRole);
        // ロールの対応する操作権限を呼び出す
        std::string result = handlerPtr->handle();
        // リソースを手動で解放する
        delete handlerPtr;
        return result;
    }
};

毎回リソースを手動で解放するのは、簡単に忘れてしまいます。もし忘れると、メモリリークが発生します。この確率の低い問題を避けるために、「スマートポインタ」を使用して管理させましょう:

class RoleEvaluator {
public:
    std::string evaluate(const std::string &userRole) {
        ProductFactory<PermissionHandler>& factory = ProductFactory<PermissionHandler>::Instance();
        std::shared_ptr<PermissionHandler> handlerPtr(factory.GetProduct(userRole));
        return handlerPtr->handle();
    }
};

std::shared_ptr参照カウントスマートポインタを採用することで、私たちはリソースを手動で解放するのを忘れることを心配する必要がなくなりました(私たちは通常忘れてしまいます...)。このスマートポインタは、参照カウントが0になったときに、自動的にポインターリソースを解放します

さて、ファクトリーパターンに加えて、ストラテジーパターンも試してみましょう

ストラテジーパターン —— 香りはありませんか?

ストラテジーパターンとファクトリーパターンの書き方は、実際にはそれほど違いはありません!ストラテジーパターンもオブジェクト指向の継承とポリモーフィズムのメカニズムを採用しています

上記のファクトリーパターンのコードに基づいて、ストラテジーパターンの指導原則に従い、いわゆるストラテジーコンテキストクラスを作成します。ここではPermissionContextと名付けます:

class PermissionContext {
public:
    PermissionContext(PermissionHandler *operation) : m_pOperation(operation) {
    }

    ~PermissionContext() {
        if (m_pOperation) {
            delete m_pOperation;
        }
    }

    std::string process() {
        return m_pOperation->handle();
    }

private:
    // 外部からのコピーと代入操作を禁止
    PermissionContext(const PermissionContext &);
    const PermissionContext &operator=(const PermissionContext &);

    PermissionHandler *m_pOperation;
};

明らかに、上記で渡されるパラメータoperationは異なる「戦略」を表します。ビジネスコードで異なるロールを渡すことで、異なる操作結果を得ることができます:

class RoleEvaluator {
public:
    std::string evaluate(PermissionHandler *pOperation) {
        PermissionContext context(pOperation);
        return context.process();
    }
};

int main() {
    RoleEvaluator evaluator;

    std::cout << evaluator.evaluate(new RootHandler("ROLE_ROOT_ADMIN")) << std::endl;
    std::cout << evaluator.evaluate(new OrderHandler("ROLE_ORDER_ADMIN")) << std::endl;
    std::cout << evaluator.evaluate(new UserHandler("ROLE_NORMAL")) << std::endl;

    return 0;
}

もちろん、上記のストラテジークラスはさらに最適化できます:

  • テンプレート技術をさらに使用してカプセル化し、ロールクラスに限定されないようにする。
// ストラテジークラステンプレート
// テンプレートパラメータ ProductType_t は基底クラスを表します
template <class ProductType_t>
class ProductContext {
public:
    ProductContext(ProductType_t *operation) 
                : m_pOperation(operation) {
    }

    ~ProductContext() {
        if (m_pOperation) {
            delete m_pOperation;
        }
    }

    std::string process() {
        return m_pOperation->handle();
    }

private:
    // 外部からのコピーと代入操作を禁止
    ProductContext(const ProductContext &);
    const ProductContext &operator=(const ProductContext &);

    ProductType_t* m_pOperation;
};

使用方法は、大きな違いはありません。クラステンプレートのパラメータが基底クラス(この例ではPermissionHandler)であることを指定するだけです:

class RoleEvaluator {
public:
    std::string evaluate(PermissionHandler *pOperation) {
        ProductContext<PermissionHandler> context(pOperation);
        return context.process();
    }
};

共に励まし合いましょう

C++とJavaはどちらもオブジェクト指向プログラミングの方法であり、ポリモーフィズムの特性を利用することでコードの結合度を下げ、同時にコードの拡張性を高めることができます。したがって、コードを書くことについては、急いで手を下すのではなく、より簡単で良い方法で実装できるか、まず考えてみましょう。

C++の父Bjarne Stroustrupは、プログラマーの三大美徳は怠惰、焦り、傲慢であると過去に述べています。そのうちの怠惰という美徳は、私たちに力を入れて考えることを促し、過度なエネルギーと体力(例えばコードを打つこと)を消費するのを避けるように伝えています。

誤りや不適切な点があれば、本公众号内でフィードバックしてください。一緒に学び、交流しましょう!

おすすめ読書:
  • 多態性の特性をマスターすれば、リーグ・オブ・レジェンドのコードがもっと少なくなる!
  • C++を学んだあなたに知っておいてほしい10の詳細!

公众号をフォローし、バックエンドで「我要学习」と返信すると、精選した「サーバー Linux C/C++」成長ロードマップ(書籍資料 + マインドマップ)を無料で入手できます!

タグ: C++ デザインパターン ファクトリーパターン ストラテジーパターン if/else

8月3日 10:15 投稿