2016-08-09 9 views
4

私のライブラリでは、実装の詳細が気にならないきれいなパブリックAPIを公開したいと考えています。あなたがそれを持っているので、これらの細部は公共の領域にも漏れています:いくつかのクラスは、ライブラリの他の部分で使用される有効なパブリックメソッドを持っていますが、APIのユーザにとってあまり有用ではないので、それの一部である必要があります。公共のコードの簡単な例:C++ APIの設計:パブリックインターフェイスをクリアする

class Cookie; 

class CookieJar { 
public: 
    Cookie getCookie(); 
} 

class CookieMonster { 
public: 
    void feed(CookieJar cookieJar) { 
     while (isHungry()) { 
      cookieJar.getCookie(); 
     } 
    } 

    bool isHungry(); 
} 

CookieJargetCookie()方法はおそらくとにかくクッキーを好きではないライブラリの利用者に有用ではありません。しかし、それは与えられたときにそれ自身を供給するためにCookieMonsterによって使用されます。

この問題の解決に役立ついくつかのイディオムがあります。 Pimplイディオムはクラスのプライベートメンバーを隠すことを提供しますが、APIの一部ではないパブリックメソッドを隠すことはほとんどありません。それらを実装クラスに移すことも可能ですが、残りのライブラリを使用するためには直接アクセスする必要があります。このようなヘッダーは次のようになります。

class Cookie; 
class CookieJarImpl; 

class CookieJar { 
public: 
    CookieJarImpl* getImplementation() { 
     return pimpl.get(); 
    } 
private: 
    std::unique_ptr<CookieJarImpl> pimpl; 
} 

あなたが本当にこれらのメソッドへのユーザーアクセスを防止する必要がある場合には便利ですが、それは単に迷惑だ場合、これはあまり役に立ちません。実際には、ユーザーがCookieJarImplの実装にアクセスできないため、新しいメソッドは最後のものよりも無駄になりました。

代替の方法は、インターフェイスを抽象基本クラスとして定義することです。これにより、公開APIの一部であることを明示的に制御できます。このインタフェースの実装には、ユーザがアクセスできないプライベートな詳細を含めることができます。注意すべきことは、結果としての仮想呼び出しがPimplイディオムよりもパフォーマンスに影響を与えることです。より洗練されたAPIの取引速度は、高性能ライブラリであるとされるものにとってあまり魅力的ではありません。

完全なものにするには、問題のあるメソッドをプライベートにして、外部からアクセスするために必要な場合にフレンドクラスを使用することもできます。ただし、これにより、ターゲットオブジェクトには本当にプライベートメンバーにアクセスできるようになり、カプセル化がやや壊れます。

これまでのところ、私にとって最善の解決策はPythonのやり方だと思われます。実装の詳細を隠そうとするのではなく、パブリックAPIの一部ではないと容易に識別でき、定期的な使用。頭に浮かぶ命名規則は、接頭辞のアンダースコアを使用していますが、明らかにそのような名前はコンパイラ用に予約されており、その使用は推奨されません。

ライブラリ以外から使用する予定のメンバーを区別するための他のC++命名規則はありますか?あるいは、上記の選択肢の1つ、または私が逃した何か他のものを使用するように私に提案しますか?

+0

私は残りの部分はプライベートに、あなたは、エンドユーザーのパブリックから隠さたいメソッドを作ると思うし、あなたのAPIのニーズの残りの部分は、ちょうどの友人になるこれらのクラスを宣言し、今、民間のもののためにあなたのクラスはここですか? https://msdn.microsoft.com/en-us/library/465sdshe.aspx – Cody

+2

このデザインアプローチが役立つかもしれません:http://stackoverflow.com/questions/27492132/how-can-i-remove-refactor-友人依存宣言 - 正しく –

+0

@Cody私は私の質問でそのオプションを考慮しました。 2つのクラスは、私が見てきた多くのスタイルガイドによれば、ここで適切な友達を使用するのに十分に関連していません。 – Quinchilion

答えて

2

このアイデアは、パブリックAPIがインターフェイスとして明示的に定義されているインターフェイス実装の関係に基づいていますが、実装の詳細は拡張された別のクラスにあり、ユーザーはアクセスできませんがアクセス可能です図書館の残りの部分に。

CRTPを使って静的な多型をπάνταῥεasとして実装することの途中で、仮想呼び出しのオーバーヘッドを避けるために提案されていますが、この種の設計では、多態性は実際には必要ありません。それはどんな種類の動的なディスパッチでも無意味にします。実際には、これは、静的多型から得た醜いテンプレートをすべて平坦化し、非常に単純なものに終わることを意味します。友達なし、テンプレートなし、(ほとんど)仮想通話なし。ここでの唯一の変更は、抽象クラスにCookieJarを回して、代わりに工場出荷時のパターンを使用している

class CookieJar { 
public: 
    static std::unique_ptr<CookieJar> Create(unsigned capacity); 

    bool isEmpty(); 
    void fill(); 

    virtual ~CookieJar() = 0 {}; 
}; 

class CookieMonster { 
public: 
    void feed(CookieJar* cookieJar); 
    bool isHungry(); 
}; 

void main() { 
    std::unique_ptr<CookieJar> jar = CookieJar::Create(20); 
    jar->fill(); 
    CookieMonster monster; 
    monster.feed(jar.get()); 
} 

:ここ

は使用例とちょうどパブリックAPIを含む、ヘッダである:のは、上記の例に適用してみましょうコンストラクタの

実装:

struct Cookie { 
    const bool isYummy = true; 
}; 

class CookieJarImpl : public CookieJar { 
public: 
    CookieJarImpl(unsigned capacity) : 
     capacity(capacity) {} 

    bool isEmpty() { 
     return count == 0; 
    } 

    void fill() { 
     count = capacity; 
    } 

    Cookie getCookie() { 
     if (!isEmpty()) { 
      count--; 
      return Cookie(); 
     } else { 
      throw std::exception("Where did all the cookies go?"); 
     } 
    } 

private: 
    const unsigned capacity; 
    unsigned count = 0; 
}; 

// CookieJar implementation - simple wrapper functions replacing dynamic dispatch 
std::unique_ptr<CookieJar> CookieJar::Create(unsigned capacity) { 
    return std::make_unique<CookieJarImpl>(capacity); 
} 

bool CookieJar::isEmpty() { 
    return static_cast<CookieJarImpl*>(this)->isEmpty(); 
} 

void CookieJar::fill() { 
    static_cast<CookieJarImpl*>(this)->fill(); 
} 

// CookieMonster implementation 
void CookieMonster::feed(CookieJar* cookieJar) { 
    while (isHungry()) { 
     static_cast<CookieJarImpl*>(cookieJar)->getCookie(); 
    } 
} 

bool CookieMonster::isHungry() { 
    return true; 
} 

これは、全体の固溶体のように思えます。それは工場のパターンを使用して強制的に、コピーと移動が必要な場合は、上記と同様の方法でラッパーを自分で定義する必要があります。とにかくこれを使用するために必要なクラスは、重量のあるリソースなので、私のユースケースでは受け入れられます。

私が気付いたもう一つの興味深い点は、static_castsをreinterpret_castsに置き換えることができ、インターフェイスのすべてのメソッドがデストラクタを含めて定義したラッパーである限り、安全に任意のオブジェクトをあなたが定義するインターフェース。不透明ラッパーやその他の偽薬を作るのに便利です。

+0

私はまだ、この方法が単にメソッドを明確な方法で命名するよりも優れているかどうかはわかりません。そのため、パブリックAPIと混同することはできません。アンダースコア接頭辞を使用することはお勧めできませんので、私はこれに既存の命名規則があるかどうか尋ねました。誰もまだその質問に答えていませんでした... – Quinchilion

+0

面白いデザインですが、あなたがあなたの質問で提案したpimplソリューションを本当にどのように改善するかはわかりません...パブリックAPI側で本当に必要な 'struct Cookie'の定義もあります? – shrike

+0

@shrike Pimplデザインで少なくとも1つの不要なパブリックメソッドを持つことを避ける方法はありません。また、ポインタ逆参照をユーザーコードに移動します。ポインタは、とにかく使用される可能性があります。そしてあなたは正しい。私はCookieがAPIの一部ではないことを忘れていました。ヘッダの中には全く必要ありません。 – Quinchilion

0

CookieJarクラスのコンテナを使用する必要があります。このコンテナは、コンストラクタが呼び出されたときにクッキーで処理されます。以下のコードでは、STL C++ライブラリのベクターをコンテナとして使用しました。これは便利ですが、他のもの(配列、リスト、マップなど)を使用でき、クッキーのプロパティを非公開にしました。より良いカプセル化のために、モンスターisHungry属性も隠すことができます。

ライブラリのユーザからgetCookie()方法を非表示にしたい場合は、あなたは、このメソッドをプライベートにするとCookieJarのフレンドクラスとしてCookieMonsterクラスを検討し、そのCookieMonstergetCookie()メソッドを使用することができます必要がありますユーザーはできません。

#include<vector> 
    using namespace std; 
    class Cookie 
    { 
     private: 
     string type; 
     string chocolateFlavor; 
    } 

    class CookieJar { 
    friend class CookieMonster; 
    public: 
     CookieJar(){ 
      //loads a cookie jar with 10 cookies 
      for (int i = 0; i = 10; i++) { 
       Cookie cookie; 
       cookieContainer.push_back(cookie); 
      } 
     } 

    private: 
     vector<Cookie> cookieContainer; 
     Cookie getCookie(){ 
      //returns a cookie to feed and deletes one in the container 
      Cookie toFeed = cookieContainer[0]; 
      cookieContainer[0] = *cookieContainer.back(); 
      cookieContainer.pop_back(); 
      return toFeed; 
     } 
    } 

    class CookieMonster { 
    public: 
     void feed(CookieJar cookieJar) { 
      while (isHungry()) { 
       cookieJar.getCookie(); 
      } 
     } 
    private: 
     bool isHungry(); 
    } 
+0

私はそう思う実装に感謝しますが、私の問題は解決しません。 'getCookie()'メソッドはAPIの一部ではなく、公開されているので、 APIの一部です。コードは説明のためのものでした。 – Quinchilion

+0

@Quinchilion getCookie()メソッドは、その一部です。 – wayland700

+0

サンプルコードはライブラリです。このファイルをインクルードしてクラスを使用したい場合は、インターフェースの一部であるメソッドに 'getCookie()'を混同してしまいます。 。 – Quinchilion

1

は、次のコードを考えてみましょう:

struct Cookie {}; 

struct CookieJarData { 
    int count; 
    int cost; 
    bool whatever; 
    Cookie cookie; 
}; 

struct CookieJarInternal { 
    CookieJarInternal(CookieJarData *d): data{d} {} 
    Cookie getCookie() { return data->cookie; } 
private: 
    CookieJarData *data; 
}; 

struct CookieJar { 
    CookieJar(CookieJarData *d): data{d} {} 
    int count() { return data->count; } 
private: 
    CookieJarData *data; 
}; 

template<typename... T> 
struct CookieJarTemplate: CookieJarData, T... { 
    CookieJarTemplate(): CookieJarData{}, T(this)... {} 
}; 

using CookieJarImpl = CookieJarTemplate<CookieJar, CookieJarInternal>; 

class CookieMonster { 
public: 
    void feed(CookieJarInternal &cookieJar) { 
     while (isHungry()) { 
      cookieJar.getCookie(); 
     } 
    } 

    bool isHungry() { 
     return false; 
    } 
}; 

void userMethod(CookieJar &cookieJar) {} 

int main() { 
    CookieJarImpl impl; 
    CookieMonster monster; 

    monster.feed(impl); 
    userMethod(impl); 
} 

を基本的な考え方は、同時にデータであり、サブクラスの束から派生クラスを作成することです。
そのため、のサブクラスはで、適切なタイプを選択していつでも使用できます。 このように、クラスを組み合わせると完全なインターフェースを持ち、同じデータを共有するコンポーネントがいくつか構築されますが、仮想メソッドを持たないそのクラスの縮小ビューを簡単に返すことができます。

+0

私は、コンポーネントシステムにテンプレートを使用する考えが好きです。しかし、CookieMonsterクラスもパブリックAPIの一部です。 CookieJarを作成してCookieMonsterに渡すのは、ユーザーです。これを行うには、あなたは 'CookieJarInternal'をユーザに公開する必要はないでしょうか?私はそれをもっと明確にしないことを謝罪します。 – Quinchilion

+0

@Quinchilion「フィード」とは誰ですか?私は別の答えを掲示するつもりですが、主にこの質問に依存しています。お知らせ下さい。 – skypjack

+0

ファイルのユーザーは誰でも。どこからでも呼び出すことができます。 – Quinchilion

0

次の例のように別の可能なアプローチは、二重派遣の種類によって次のようになります。

struct Cookie {}; 

struct CookieJarBase { 
    Cookie getCookie() { return Cookie{}; } 
}; 

struct CookieMonster; 
struct CookieJar; 

struct CookieJar: private CookieJarBase { 
    void accept(CookieMonster &); 
}; 

struct CookieMonster { 
    void feed(CookieJarBase &); 
    bool isHungry(); 
}; 

void CookieJar::accept(CookieMonster &m) { 
    CookieJarBase &base = *this; 
    m.feed(base); 
} 

void CookieMonster::feed(CookieJarBase &cj) { 
    while (isHungry()) { 
     cj.getCookie(); 
    } 
} 

bool CookieMonster::isHungry() { return false; } 

int main() { 
    CookieMonster monster; 
    CookieJar cj; 

    cj.accept(monster); 

    // the following line doesn't compile 
    // for CookieJarBase is not accesible 
    // monster.feed(cj); 
} 

あなたは仮想メソッドを持ってgetCookieません。この方法では、クラスのユーザーがアクセスできませんCookieMonster
正直なところ、問題はfeedに移動しました。これはユーザーからは使用できなくなり、直接acceptメソッドとして使用されることを意味します。

の問題を解決するにはは、仮想テンプレートメソッドです。これは単純に不可能です。
上記の例のように使用できないメソッドを公開したくない場合は、仮想メソッドやフレンド宣言を避けることはできません。

とにかく、利用可能にしたくない内部メソッドgetCookieを隠すのに役立ちます。

+0

別の興味深い解決策。時間をいただきありがとうございます:)私は満足のいく解決策を見つけましたが、まもなく答えとして投稿する予定です。 – Quinchilion

1

私には2つのアイデアがあります。最初の例ではCookieJarPrivateクラスを作成して、専用のCookieJarメソッドをライブラリの他の部分に公開します。 CookieJarPrivateは、パブリックAPIの一部ではないヘッダーファイルで定義されます。 CookieJarは、CookieJarPrivatefriendであると宣言します。 cookiejar.hcookiejarprivate.hを含める必要は技術的には必要ありませんが、friendを悪用してクライアントが実装の詳細にアクセスするには、CookieJarPrivateを定義してください。

class Cookie; 

class CookieJarPrivate { 
public: 
    Cookie getCookie(); 
private: 
    CookieJarPrivate(CookieJar& jar) : m_jar(jar) {} 
    CookieJar& m_jar; 
}; 

class CookieJar { 
    friend class CookieJarPrivate; 
public: 
    CookieJarPrivate getPrivate() { return *this; } 
private: 
    Cookie getCookie(); 
}; 

class CookieMonster { 
public: 
    void feed(CookieJar cookieJar) { 
     while (isHungry()) { 
      cookieJar.getPrivate().getCookie(); 
     } 
    } 

    bool isHungry(); 
}; 

Cookie CookieJarPrivate::getCookie() { 
    return m_jar.getCookie(); 
} 

コンパイラはCookieJarPrivateコンストラクタとgetPrivate()方法をインライン化することができるはずなので、パフォーマンスはプライベートgetCookie()への直接の呼び出しと同等でなければなりません。コンパイラがCookieJarPrivate::getCookie()の実装でm_jar.getCookie()への呼び出しをインライン化しないことを選択した場合、1つの余分な関数呼び出しのペナルティを支払う可能性があります。 は、両方のメソッドが同じ翻訳単位で定義されている場合、特にプライベートgetCookie()が他の場所で呼び出されていないことを証明できれば、確かに保証されます。


方法はこれだけダミータイプを構築することができるコードで呼び出すことができるように第二のアイデアは、プライベートコンストラクタとCookieMonsterfriend関係で、すなわち唯一CookieMonsterクラス型のダミーパラメータです。これは通常のfriendと似ていますが、粒度は細かいです。

template <class T> class Restrict { 
    friend T; 
private: 
    Restrict() {} 
}; 

class Cookie; 
class CookieMonster; 

class CookieJar { 
public: 
    Cookie getCookie(Restrict<CookieMonster>); 
}; 

class CookieMonster { 
public: 
    void feed(CookieJar cookieJar) { 
     while (isHungry()) { 
      cookieJar.getCookie({}); 
     } 
    } 

    bool isHungry(); 
}; 

これの変形は、非公共のヘッダに定義され、無friendと、非テンプレートダミーです。どのメソッドが公開されているのかはまだ細かいですが、CookieMonsterだけでなく、ライブラリ全体に公開されるようになります。

class PrivateAPI; 
class Cookie; 

class CookieJar { 
public: 
    Cookie getCookie(PrivateAPI); 
}; 

class CookieMonster { 
public: 
    void feed(CookieJar cookieJar); 

    bool isHungry(); 
}; 

class PrivateAPI {}; 

void CookieMonster::feed(CookieJar cookieJar) { 
    while (isHungry()) { 
     cookieJar.getCookie({}); 
    } 
} 
+0

私はこれが私が尋ねたものとは異なる問題を解決すると感じます。ユーザーが 'getCookie()'を呼び出すことを制限する必要はありません。私が望むのは、APIを単純化するために、そのパブリックメソッドを完全に隠すことです。 – Quinchilion

+0

アイデア1について、私はその批判を受け入れず、 'CookieJarPrivate'は隠されています。アイデア2については、「プライベート」メンバーは隠されていませんが、ユーザーは「プライベート」メンバーを無視できるという慣習を理解しています。アイデア2では、これらのダミーパラメータでマークされたメンバーを無視できるという新しい規則が導入されています。 「プライベート」と同じように、重要なことは、人間がコードを読んでいることを意味します。構文的制約は単なるボーナスにすぎません。 – Oktalist

+1

あなたの最初のアイデアはややうまくいく、はい。パブリック 'getCookie()'メソッドをパブリック 'getPrivate()'メソッドで置き換えます。これは、Pimplイディオムで取得したものと同じです。私の批判は他の2つに向かっていました。彼らについては、そうですね。しかしその場合、私は 'private_getCookie()'のような命名規則を簡単に導入することができます。これは間違いなく簡単に気付くことができ、コード補完によって簡単には示唆されません。 – Quinchilion

関連する問題