質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

87.49%

オブジェクト指向での組み方

解決済

回答 3

投稿

  • 評価
  • クリップ 1
  • VIEW 1,587

score 3312

C/C++で趣味でやっています。

PCが一度ダメになったので C++用ライブラリを最初から作りなおしています。

Windows API を実装したときに LoadLibrary関数とかで DLLを動的リンクすることありますよね。

その場合の ライブラリを作ろうとしています。

今までは UML風に書くと

[ CDynamicLinkLibrary ]
+ CONSTRUCTOR( path : const std::string ) throw(例外) // LoadLibraryでリンクし、失敗すれば例外を投げる
+ DESTRUCTOR()  // FreeLibraryで破棄
+ Call( funcname : const std::string ) : FARPROC throw(例外) // DLL内の 関数ポインタを取得
+ CallCusror( cursorId : int ) : HCURSOR // リソースDLLから 指定IDのカーソルを取得
+ CallBitmap( bitmapId : int ) : HBITMAP // リソースDLLから 指定ビットマップを取得
+ Play( soundId : int ) : bool // リソースDLLから指定IDの音声を再生する
...
- hModule  : HMODULE
- filename : std::string

としていました。

( つまり、DLLからの取得等は この CDynamicLinkLibrary に任せるっていう... )

ですがPCがダメになったのでその中にあったデータもダメになったため、再度作ることにしました。
これを機に、namespaceに包んだりして もうちょっと C++らしくしようとしています。

で、今イメージしているのは2種類です。

方法1:
namespace LibraryLoader内に CDynamicLinkLibraryクラス ( ただし、DLLのリンクと 破棄, getHandle のみ。構造体にコンストラクタ・デストラクタがついたようなクラス )を追加し、

CFunctionLinkerクラス, CCursorLinkerクラス... という風に処理ごとに分割。( 関数オブジェクト )
それぞれ CDynamicLinkLibraryクラスをコンポジションする。

// throw(例外) とある部分は std::exception を継承した例外クラスだとします。(例なので)
namespace LibraryLoader{

          // cppファイルに書いて隠ぺいする (外部に触らせないために)
          class CDynamicLinkLibrary{
                public:
                           CDynamicLinkLibrary( const std::string filename ) throw(例外);
                           ~CDynamicLinkLibrary();

                           getHandle(void){ return this->hModule; }
                private:
                           HMODULE hModule;
                           ...
          };

          // 以降はヘッダファイルにクラス定義をして外部へ公開する ( 前方宣言付き )
          class CFunctionLinker{
                public:
                           CFunctionLinker( const std::string filename ) throw(例外){
                                   dll = new CDynamicLinkLibrary( filename ); 
                           }

                           ~CFunctionLinker(){ delete dll; }

                           FARPROC operator()( const std::string funcname ){
                                   HMODULE module = dll->getHandle();
                                   // ここで HMODULE から 関数ポインタを取得する
                           }
                private:
                           CDynamicLinkLibrary* dll; 
          };

          class CCursorLinker{
                public:
                           CCursorLinker( const std::string filename ) throw(例外){
                                   dll = new CDynamicLinkLibrary( filename ); 
                           }

                           ~CCursorLinker(){ delete dll; }

                           FARPROC operator()( const std::string funcname ){
                                   HMODULE module = dll->getHandle();
                                   // ここで HMODULE から カーソルを取得する
                           }
                private:
                           CDynamicLinkLibrary* dll; 
          };
          ...
}

方法2:
従来のやり方 ( 上記のUML風のやつ ) を namespace LibraryLoader で包んだだけ。

どれが C++ として正しいのかがわかりません。

動く・動かない ( または コンパイルが通る・通らない ) は実際に試せばいいかもしれませんが、

こういうのは慣れとかだと思うので。

自分なりに デザインパターン ( といっても ちょっとだけ。 Factory, TemplateMethod, Strategy ... となんとなく使えそうなやつだけ ) やったり、

UML を解説してるサイトをなるべく読んだり,

自分なりに組んだりしているのですが...

なぜ 方法1 のように考えたかというと、

2chだったか、何でだったか忘れましたが、「それ オブジェクト指向の原則からしたらダメじゃねーか!」みたいな文章があったので、

「ああ、オブジェクト指向に『原則』ってあるんだな」と思い、

検索したところ、

単一責任の原則 ( SRP原則 ) というらしいですね。

まだ詳しくはチェックしていませんが、

「クラスを変更する理由が2つ以上存在してはならない」というやつらしいです。

それで、そういうことを紹介している記事にちょいちょい「リファクタリング」というものが出てきます。

主に Javaでの説明が多いですが、

リファクタリングしてみよう2の「フィールドの引き上げ(320)」で

    フィールドの名前が違う場合、スーパークラスのフィールドとして使いたい名前に変更する
    コンパイルしてテストする
    スーパークラスに新しいフィールドを作成する
     =>フィールドがprivateならば、サブクラスから参照できるようにprotectedに変更する
    サブクラスのフィールドを削除する
    コンパイルしてテストする
    そのフィールドに対して「自己カプセル化フィールド(171)」の適用を検討する


(上記ページより)

とあったので方法1を思いつきました。

ですが こういうのは条件 ( 状況とか ) によって変わりますよね。

やたらめったら Singletonパターンを適用するのはダメだし... とか。

今回の場合はどうやれば(オブジェクト指向的に)正解なのでしょうか。

やるからには出来る限り そういう原則とかを守りたいので。

言語は上記の通り C++ でお願いします。

宜しくお願い致します。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 3

+3

こんにちは。

私なら「方法2」を使うと思います。

一部の「意識高い系」の人たちの中には、滅多矢鱈に細かく分割することを好む人達がいます。確かに無制限に巨大なクラスや関数は無駄な依存を増やすのでよくないです。その一点のみに着目しているのだと思います。

クラスや関数を分割すると、当たり前ですがそのクラス宣言や関数宣言、呼び出しI/Fが増加します。これはプログラム全体の複雑さを増しますから、無闇矢鱈に細かく分割するのは、確実に悪手です。私の目には「方法1」はこれに該当しているように見えます。

では、どんな粒度がベストなのか悩ましいですが、ほとんど関連性のない機能を1つのクラスに入れるとか、関数内のネストが深くなりすぎて、見落としが悪くなった等を契機に分割すれば良いように感じます。
最初から予想される機能分割はしておくとよいですが、分割が必要かどうか分からない部分まで「分割しておいた方が安心だから」の理由で分割して細かくしすぎるのは良くないです。

要はバランスです。バランス感覚は経験によって育ちますので、最初から経験豊富な人並みの設計を目指すのは難しいです。
自分なりにベストと感じる粒度で設計していけば良いのでは無いでしょうか?

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

checkベストアンサー

0

何を目的にしているかによると思います。

再利用できればとりあえずいいんじゃないのかなという気がします。
あとは、リファクタリングについても触れているので、単体テスト可能なように抽象クラスを適宜いれるとかになります。

上記のような目的を考慮すると、
HMODULEとかHANDLEとかの資源管理用のクラスとかはWindows APIを使う上ではよく使います。
ですので、CDynamicLinkLibraryみたいなクラスは用意しておいていいと思います。
CFunctionLinkerは要らない気がします。FARPROCという扱いにくいものを返されても使いにくいです。
ある程度種類ごとにAPIをまとめるという感じじゃないでしょうか?

namespace myapplication{

//この階層は具体的なアプリケーションの処理を行うクラスとかを置く。

class MusicPlayer
public:
    void Play(int no);
};

class ImageViewer{
public:
    void Show(int no);
};

}//namespace myapplication

namespace myapplication{
namespace mylib{

//例えばここに共通で使用するような内部実装を置くとか。
//規模によって階層と分類を増やす
//上位クラスをテストしたいのであればAPI呼び出しの抽象クラスを作る

//HMODULEの資源管理クラス
class HModule{
};

//サウンド関係のAPIとかのラッパー
class MusicApi{
};


}//namespace mylib
}//namespace myapplication

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

0

実装したい物について概要しか把握できないので完全に妥当かどうかはわかりませんが、STLのstd::fstreamを参考にクラスを設計するのがいいかと思います。

fstreamのようなクラスの特徴はオブジェクトの生成と消滅がファイル(今回はDLL)の読込と解放に同期することです。二重解放を防止するために、コピーコンストラクタとコピー代入演算子はdeleteしておく必要がありますが、正しくオブジェクトの寿命を管理すれば、DLLの解放忘れもありません。

また、ビットマップ用とカーソル用はそれぞれ継承して作成します。fstreamやifstreamとofstreamにわかれていくのと同じような感じです。そのほうが、親のクラスはDLLの読込の処理に集中できて、子のクラスは必要な差分だけ実装ということができると思います。

STLはC++におけるクラス設計のスタンダードの一つですので、設計の参考にして間違いは無いと思います。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 87.49%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る