Help us understand the problem. What is going on with this article?

C言語で デザインパターンにトライ! ~Strategy パターン 戦略に合わせてインターフェイスの実体を選択しよう!

More than 1 year has passed since last update.

はじめに

「C言語でトライ! デザインパターン」
今回はStrategy パターン。インターフェースは変えず、戦略に合った実体を選択することで、実行側の動作を変えずに振る舞いを変えよう!というパターンです:thumbsup:
最初に見た時に一番用途がわからなかったパターン。なんですが、実は自分がとても馴染みのある構成でした:laughing:

デザインパターン一覧
作成したライブラリパッケージの説明
公開コードはこちら

Strategyパターン

Wikipedia大先生の説明はこちら。

Strategy パターンは、コンピュータープログラミングの領域において、アルゴリズムを実行時に選択することができるデザインパターンである。
ポリモーフィズムを持たないようなプログラミング言語では、このパターンによって解決される問題は、関数ポインタや委譲を使った記述によりリフレクションの形態で扱われる。

これが全くピンと来てなかったんですが、他のサンプルの方がわかりやすいよとコメントいただき整理したのがこちらです。

そこで分かったのが、Strategyパターンは「インターフェースクラスの実体やコンストラクタを変えることで、戦略に合わせてガラッと違う振る舞いが出来るものを作ろう:exclamation:」なんだなということでした。
この記事をまとめた当初はあ~、なるほどね~で終わったのですが、今こうして利用方法を考えると、実は自分の身近にある設計であることに気付きました。

対下位レイヤーへの対応

組込ミドルウェア等、対下位レイヤーとやり取りを行うモジュールを開発する人にはあるあるだと思うんですが、自分に割と習慣付いている設計感の一つにこんなのがあります。

  • 下位レイヤーとのやり取りはインターフェース化して下位の実体に依存しないようにしよう

下位の実体とは例えば以下のような感じ。

ミドルウェア層⇒「ハードを制御するドライバ層」、「実動作を行うエンコーダーみたいなちょっとハードよりのアプリ」
Webアプリ層⇒「ブラウザ依存の部分」、「対実機依存の部分」

こういった部分って、大体インターフェースはそのままにして、その起動時に情報を設定 or 取得してどれを使うか選択するようにしている気がします。これって状況に合わせて戦略を選択するStorategyパターンの発想ですよね。

C言語以外でのサンプル

エンコーダーを差し替える

Cといいつついきなりrubyのサンプルですいません:bow:

  • 動画の入力・出力に合わせてエンコーダー(FFmpegというOSSだったり別のエンコーダーだったり)を差し替えたい
  • 動画の種類やエンコーダーは増えるかもしれない(依存性を低くした方が安全)

というシステム設計をすることがありました。

その時自分がやったのはこんなイメージ。戦略家EncoderManagerを介して実体を取得して、encodeを実行!みたいな感じだったと思います。自分は「実体の管理者を設けて隠ぺいする」って感覚だったのでManagerとしましたが、「管理者」⇒「戦略家」と言葉を変え、「状況にあった戦略を実施できる実体を選択する戦略家を設ける」と考えれば、まさしくStorategyパターンですね。
(パターンにはめるならEncoderManagerEncoderStrategistって名前ですかね)

Encoder
class Encoder
  def encode(srcfile, distfile, setting)
    #エンコード自体は他のクラスに丸投げ
    EncoderManager.get_encoder(srcfile, distfile, setting).encode(srcfile, distfile)
  end
end

#エンコーダーは戦略家EncoderManagerが状況に合わせて動的に選択
class EncoderManager
  def get_encoder(srcfile, distfile, setting)
    case gettype(srcfile, distfile, setting)
    when typeA then
      return EncoderAppA.new(setting)
    when typeB then
      return EncoderAppB.new(setting)
    else
      return EncoderDefault.new(setting)
    end
  end

  def gettype(srcfile, distfile, setting)
    #入力にあったタイプを選択する。
  end
end

インターフェース設計はこんな感じ(rubyは定義する必要ないですけど)

EncoderIF
class EncoderIF
  def initialize(setting)
  end

  def encode(srcfile, distfile, setting)
    #FFmpeg等、エンコーダーに合わせた処理を実装する。
  end
end

Cサンプル

こちらも意図せず既にサンプルを作っていました。コードはこちら、design_pattern_for_c内のthreadpoolで利用しているevent_ifです。
threadpoolはイベントループ系のAPIをマルチスレッド化したものなのですが、そのthreadpool使用時にイベントループに何を使うのか、動的に選択することが出来るようにしています。

動的選択はこのAPIで行います。

event_threadpool.h
//ここのplugin_pathで、どのイベント用を利用するかを指定する。
//実装はプラグインのインターフェースに合わせて行っているため、処理を変えずにプラグイン(戦略)の差し替えが可能となる。
EventTPoolManager event_tpool_manager_new(int thread_num, int is_threadsafe, const char * plugin_path);

用意したインターフェースはこんな感じ。インターフェースに合った動作をするなら新しいイベントループも追加可能です。

event_if.h
/*イベントメインインスタンス作成*/
EventInstance event_if_new(void);
/*イベントメインインスタンス削除*/
void event_if_free(EventInstance this);

/*イベント追加*/
EventHandler event_if_add(EventInstance this, EventSubscriber subscriber, void *arg);
/*イベント更新*/
EventHandler event_if_update(EventInstance this, EventHandler handler, EventSubscriber subscriber, void *arg);
/*イベント削除*/
void event_if_del(EventInstance this, EventHandler handler);

/*イベントのFD取得*/
int event_if_getfd(EventHandler handler);

/*イベント受信メインループ*/
int event_if_loop(EventInstance this);
/*イベント受信メインループ終了*/
void event_if_loopbreak(EventInstance this);
/*イベントメインループ終了時処理*/
void event_if_exit(EventInstance this);

プラグインはselect, epoll, libevent, libevのイベントを用意。環境や状況に合わせてユーザーが使用するプラグイン(戦略)を指定します。
ライブラリは実装は変えずに指定されたプラグインを利用して機能を実現します。Storategyだこれ:clap:

実装を抜粋すると、こんな感じにdlopenとdlsymを使っています。

event_thread.c
//event_if.hで定義した関数を全部loadする。
static int event_tpool_thread_load_all_fun(void) {
    event_if_instance_g.new = dlsym(event_if_instance_g.handle, "event_if_new");
    if(!event_if_instance_g.new) return -1;

    event_if_instance_g.add = dlsym(event_if_instance_g.handle, "event_if_add");
    if(!event_if_instance_g.add) return -1;
...
    return 0;
}

//指定パスをdlopenする。
int event_tpool_thread_load_plugin(const char *plugin_path) {
    const char * path;
        //指定無しならデフォルトを使用。
    if(!plugin_path) {
        path = event_tpool_thread_get_defaullt_plugin();
    } else {
        path = plugin_path;
    }

        //プラグインに対してdlopen
    event_if_instance_g.handle = dlopen(path, RTLD_NOW);
    if(!event_if_instance_g.handle) {
        DEBUG_ERRPRINT("Failed to open %s, err=%s!\n", plugin_path , dlerror() );
        return -1;
    }

    return event_tpool_thread_load_all_fun();
}

…実際は自分の力ではなくて、GPLのlibevを利用している場合このライブラリはGPLになるのかの記事に対していただいたコメントを元にGPL対策しただけなんですけどね。コメントありがとうございます、綺麗なデザインになりました。:pray:

感想

まさか自分が最初に一番よくわからなかったパターンが、実は一番馴染みのある設計思想だとは思いませんでした:exclamation:
自分の経験ベースのまとめなので、「下位レイヤーの差し替え」に特化した記載をしてしまいましたが、特にミドル層とかその上の方を開発しているようなC言語開発者の方にもあるあるな情報を出せたのではないかと思っています。

もちろん英語のwikipediaサンプルのように状況に応じた戦略を動的に与えられると、例えば常時稼働するようなシステムに対して面白い効果が発揮できそうな気がします。(こちらはいい例が思いつきませんでした:dizzy_face:)

後絵文字つけて記事書くのやっぱ楽しい!:laughing:
素晴らしいチートシートがあったので参考に貼ってあります:thumbsup:

参考

pythonの例
英語版Wikipediaの例

顔文字
Qiita/Github/Slack/Discord 絵文字一覧

developer-kikikaikai
元CのLinux組み込み開発者→201904からとある会社でGo言語バックエンドのアーキテクトとして活動しています。 組み込み時代はミドルウェアより上位層が主戦場でした。たまにRubyやpython、Java/Androidも若干触ります。 技術の幅を増やすのはもちろんだけど、それ以上にチーム構築・チーム開発への貢献力を磨きたい
https://github.com/developer-kikikaikai
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away