ブログ 7 分で読める

モダンC++デザインを探る:MemCache++ ケーススタディ

Share this article
モダンC++デザインを探る:MemCache++ ケーススタディ

MemCache++ は、軽量で型安全、使いやすく、機能が充実したMemcacheクライアントです。開発したのは、現在Google Australiaで働くC++愛好家のDean Michael Berris氏です。彼はISO C++委員会へのGoogle代表団の一員でもあります。

優れた設計のライブラリを研究することは、C++の設計・実装スキルを向上させる優れた方法です。この記事の目的は、memcache++を理解しやすく、使いやすいものにしている設計上の選択をいくつか探ることです。

名前空間によるモジュール化

名前空間は、アプリケーションをモジュール化するための優れた方法です。残念ながら、この機能はC++プロジェクトでは十分に活用されていません。オープンソースのC++プロジェクトをいくつか無作為に見れば、それはすぐに分かります。さらに、C++名前空間の定義を調べると、一般的な説明は次のようになります:

A namespace defines a new scope. They provide a way to avoid name collisions.

多くの場合、C#やJavaのようにアプリケーションを構造化する手段としてではなく、名前衝突の回避が名前空間の主な動機として説明されます。しかし、Boostのようないくつかの現代的なC++ライブラリでは、ライブラリを適切に構造化するために名前空間が使われ、開発者にもその利用が促されています。

memcache++における名前空間のモジュール化はどうなっているでしょうか?

memcache++の名前空間間の依存関係グラフは次のとおりです:

名前空間は主に2つの理由で使用されています:

    • ライブラリをモジュール化する。
    • 「memcache::detail」名前空間のように詳細を隠す。このアプローチは、この名前空間内の型を直接使用する必要がないことをライブラリ利用者に伝えたい場合に非常に興味深いものです。C#では「internal」キーワードがその役割を果たしますが、C++には公開型をライブラリ利用者から隠す方法がありません。

memcache++は名前空間を効果的に使用しています。ただし、memcacheとmemcache::detailの間には依存関係サイクルが存在します。この依存関係サイクルは、memcacheからmemcache::detailによって使用されている型を検索することで解消できます。

そのためには、次のCQLinqクエリを実行します:

from t in Types where t.IsUsedBy("memcache.detail") 
&& t.ParentNamespace.Name=="memcache"
select new { t,t.TypesUsingMe }

クエリ実行後の結果は次のとおりです:

memcache dependencies graph

依存関係サイクルを解消するには、pool_directiveとserver_pool_testをmemcache名前空間へ移動できます。

現代的なC++コードで最も使われているパラダイムは、ジェネリックプログラミングとOOPのどちらでしょうか?

C++の世界では、オブジェクト指向プログラミングとジェネリックプログラミングという2つの学派が非常に人気があり、それぞれの支持者がいます。こちらの 記事 で、両者の緊張関係が説明されています。

memcache++で最も使われているパラダイムはどちらでしょうか?

この問いに答えるために、まずジェネリック型を検索してみましょう:

from t in Types where t.IsGeneric && !t.IsThirdParty select t 

非ジェネリック型はどうでしょうか?

from t in Types where !t.IsGeneric && !t.IsGlobal && !t.IsNested 
&& !t.IsEnumeration  && t.ParentProject.Name=="memcache"
select t

非ジェネリック型のほとんどは例外クラスです。その割合をより正確に把握するには、ツリーマップビューが非常に役立ちます。

memcache treemap view

青い長方形はCQLinqクエリの結果を表しています。ご覧のとおり、ライブラリのうち非ジェネリック型に関係する部分はごくわずかです。

最後に、ジェネリックメソッドを検索できます:

from m in Methods where m.IsGeneric && !m.IsThirdParty select m

ご覧のとおり、memcache++では主にジェネリクスが使われています。しかし、それだけではC++のジェネリックプログラミングアプローチに従っているとは断定できません。それを確認する良い指標は、OOPが多用する継承と動的ポリモーフィズムの使用状況です。一方、ジェネリックアプローチでは継承は非常に限定的で、動的ポリモーフィズムは避けられます。

基底クラスを持つ型を検索してみましょう。

from t in Types where t.BaseClasses.Count()>0 && !t.IsThirdParty 
&& t.ParentProject.Name=="memcache"
select t

例外クラスが継承を使うのは自然です。では、他のクラスはどうでしょうか。動的ポリモーフィズムを目的として継承を使用しているのでしょうか。この問いに答えるために、すべての仮想メソッドを検索してみましょう。

from  m in Methods where m.IsVirtual select m

仮想メソッドを持つのは例外クラスだけです。

動的ポリモーフィズムを使わない場合、特定のクラスに異なる振る舞いが必要になったときには、どのようなアプローチを採用できるでしょうか?

現代的なC++アプローチにおける一般的な解決策は、ポリシーを使用することです。以下はWikipediaによる簡単な定義です:

"The central idiom in policy-based design is a class template(called the host class),taking several type parameters as input, which are instantiated with types selected by the user (called policy classes), each implementing a particular implicit interface (called a policy)."

memcache++には、memcache.policies名前空間内に多くのポリシーがあります。

ポリシーベース設計をよりよく理解するために、memcache++の例を見てみましょう。

memcache++はbasic_handle型を使用して、キャッシュに対するadd、set、get、deleteなどのすべてのコマンドを実装しています。このクラスは次のように定義されています:

    template <
        class threading_policy = policies::default_threading, 
        class data_interchange_policy = policies::binary_interchange,
        class hash_policy = policies::default_hash
    >
    struct basic_handle 

memcache++はスレッドセーフであり、マルチスレッド環境では同期を管理する必要があります。デフォルトのthreading_policyは「default_threading」で、特別な処理は必要ありません。一方、マルチスレッドの場合は「boost_threading」ポリシーが使用されます。

connectメソッドの実装を見てみましょう。

    void connect(boost::uint64_t timeout = MEMCACHE_TIMEOUT) { 
       typename threading_policy::lock scoped_lock(*this);
       for_each(servers.begin(), servers.end(), connect_impl(service_, timeout));
    };

threading_policyが「default_threading」の場合、lockコンストラクタは何もしないため、最初の行は効果がありません。一方、boost_threadingの場合、lockはBoostを使用してスレッド間の同期を行います。

ポリシーを使用すると、比較的理解しやすく使いやすい状態を保ちながら、異なる振る舞いを実装する柔軟性が高まります。

ジェネリックファンクタ

memcache++は、add、get、set、deleteなど、キャッシュと対話するための多くのコマンドを実装しています。このような場合にはCommandパターンが適しています。memcache++はジェネリックファンクタを使ってこのパターンを実装しています。すべてのファンクタを取得するCQLinqクエリは次のとおりです:

from t in Types where t.Methods.Where(a=>a.IsOperator 
&& a.Name.Contains("()")).Count()>0
select t

ファンクタは関数呼び出しをその状態とともにカプセル化するもので、呼び出しを後のタイミングまで遅延させ、コールバックとして機能させることができます。ジェネリックファンクタは、通常のファンクタよりも高い柔軟性を提供します。

公開されているパブリックインターフェース

ライブラリがその機能をどのように公開するかは非常に重要です。柔軟性と使いやすさの両方に影響するからです。それを確認するために、テストプロジェクトとmemcache++ライブラリとの間のやり取りを検索してみましょう。

from m in Methods where m.IsUsedBy ("test")
select m 

テストプロジェクトは主にジェネリックメソッドを使用してmemcache++の機能を呼び出しています。テンプレートメソッドを使用する利点は何でしょうか。なぜクラスや関数を使わないのでしょうか。

OOPアプローチでは、ライブラリインターフェースはクラスと関数で構成され、適切に設計されたものでは、低結合を強制する契約として抽象クラスが使用されます。この解決策は非常に興味深いものですが、いくつかの欠点があります:

  • インターフェースがより複雑になり、頻繁に変更される可能性があります。これを説明するために、memcache++が公開しているaddメソッドを見てみましょう。ジェネリックアプローチを使わない場合、int、double、stringなど、特定の型ごとに多くのメソッドを追加する必要があります。

ジェネリックなaddメソッドはadd<T>として宣言され、Tは型を表します。この場合、必要なメソッドは1つだけです。別の型を追加したい場合でも、インターフェースを変更する必要はありません。

  • インターフェースの柔軟性が低下します。たとえば、次のようなメソッドを公開するとします:

calculate(IAlgo* algo).

利用者はIAlgoを継承するクラスを提供しなければなりません。しかし、ジェネリクスを使用してcalculate<T>として定義すれば、利用者は必要なメソッドを持つクラスを提供するだけでよく、必ずしもIAlgoを継承する必要はありません。また、新しいメソッドが追加されてIAlgoがIAlgo2へ変更されても、ライブラリ利用者は影響を受けません。

理想的には、ライブラリが公開するインターフェースに破壊的変更があってはならず、ライブラリに変更が導入されても利用者が影響を受けないようにすべきです。ジェネリックアプローチは変更への耐性が非常に高いため、このような制約に最も適しています。

使用されている外部API

memcache++が使用している外部型は次のとおりです:

memcache++は主にBoostとSTLを使用して目的を達成しています。使用されているBoostの機能には次のようなものがあります:

  •  マルチスレッド。
  •  アルゴリズム。
  •  Spirit。
  • Asio。
  • ユニットテスト。

STLでは、コンテナが最も頻繁に使用されています。

では最終的に、ジェネリックアプローチを使用する利点は何でしょうか?

  • memcache++の設計上の選択が効率的であることを示す最初の指標は、コード行数(LOC)です。わずか約600行しかありません。この結果には主に2つの理由があります:
  • ジェネリックアプローチを採用したことで定型コードが削減されている。
  • BoostとSTLの豊富な機能を活用している。
  • 2つ目の強みは柔軟性です。変更があっても影響を受けるのはごく一部のコードだけです。
Share this article