クラス設計と断ったがテンプレートを含むコード設計についてまとめてみる.
最初に述べるが,検索エンジンでここに直接やってきたとしても,期待する答えはここにはない.
ここだけでなく何時間Web上で検索したとしても,あなたの作りたいアプリケーションの具体的な仕様など落ちているはずがないのである.
クラス設計がしたい人は無駄なWeb検索をやめて,データと機能を全てリストアップしどのようにクラス化するのが効率がよいか自分で検討するべきである.
最初に述べるが,検索エンジンでここに直接やってきたとしても,期待する答えはここにはない.
ここだけでなく何時間Web上で検索したとしても,あなたの作りたいアプリケーションの具体的な仕様など落ちているはずがないのである.
クラス設計がしたい人は無駄なWeb検索をやめて,データと機能を全てリストアップしどのようにクラス化するのが効率がよいか自分で検討するべきである.
クラス間の関係は外注に似ると考える.発注した部品の製造工程について元請けは関与しない.与えた指示に対して期待する結果が帰ってくればそれで良い.途中経過などについてはそれが必要な場合を除き,使う側に公開されるべきでないし,使われる側は使う側に依存するべきではない.
これが真の意味でのカプセル化と考える.すなわち,単に変数をprotectedやprivateにすることがカプセル化ではなく,必要十分な機能がそのクラスに備わっていることがカプセル化であろう.
異なる見方をしてみる.
これが真の意味でのカプセル化と考える.すなわち,単に変数をprotectedやprivateにすることがカプセル化ではなく,必要十分な機能がそのクラスに備わっていることがカプセル化であろう.
- 同じカテゴリに含まれるものは抽象化し基本クラスを作る.
- 派生の過程で特化する必要があるものは仮想関数を継承させる.非仮想関数をオーバーライドしてはならない.
- 異なるものが共通する機能・アルゴリズムをもつ場合はテンプレートで抽象化する.
異なる見方をしてみる.
- コンパイル時に決定しているものはテンプレート引数で決定する.
- 動的に決まるものは仮想継承,dynamic_castで決定する.
- (コンパイル前に決定しているものはプリプロセッサでも記述できる)
A is B の場合は派生(クラスBを派生させてAを作る),A has Bの場合は委譲(クラスBをメンバ変数としてもつ)を使うべきと言われる.しかし,どちらかはっきりしない場合も多い.
たとえば多角形クラスを考える.多角形クラスは点データをもつのか点データ自体であるのか議論のわかれるところである.
この実装のみを見た場合,どちらが最適か議論することは不毛であろう.実装方法の解は唯一ではない.
次にPolygonは描画できる.そこでこの描画という機能はPolygonが持つ機能とするべきかどうかについて考える.
これは開発するアプリケーションによって決めるべきであり,これも普遍的な解があるわけではない.
ここで言いたいことはこのように抽象化した部分だけで議論しても,その後のこのクラスの用途,最終的に実装したい機能が明確でない以上,どのような設計がベストかなどわかるわけがないということである.
図形ごとに特化させる必要があるのか,点データのコンテナはstd::vectorに限定してよいのか,そして最も重要なこのクラスでどういう機能を実装したのか.
それはいくらWeb上で検索を繰り返しても,自分が作りたいアプリケーションの設計図が公開されていることなどありえないのである.
デザインパターンについても同様である.どのデザインパターンを使うかについては自分のアプリケーションを細かく検討しなければわかるわけがない.
たとえば多角形クラスを考える.多角形クラスは点データをもつのか点データ自体であるのか議論のわかれるところである.
class Polygon1 { protected: std::vector<std::complex> pts; }; class Polygon2 : public std::vector<std::complex> {};
この実装のみを見た場合,どちらが最適か議論することは不毛であろう.実装方法の解は唯一ではない.
次にPolygonは描画できる.そこでこの描画という機能はPolygonが持つ機能とするべきかどうかについて考える.
これは開発するアプリケーションによって決めるべきであり,これも普遍的な解があるわけではない.
- virtual void Draw() = 0とし,Platformごとに派生させる.
- Drawはここで実装してしまい,特化した図形へと派生させる.
ここで言いたいことはこのように抽象化した部分だけで議論しても,その後のこのクラスの用途,最終的に実装したい機能が明確でない以上,どのような設計がベストかなどわかるわけがないということである.
図形ごとに特化させる必要があるのか,点データのコンテナはstd::vectorに限定してよいのか,そして最も重要なこのクラスでどういう機能を実装したのか.
それはいくらWeb上で検索を繰り返しても,自分が作りたいアプリケーションの設計図が公開されていることなどありえないのである.
デザインパターンについても同様である.どのデザインパターンを使うかについては自分のアプリケーションを細かく検討しなければわかるわけがない.
さて,ここで,上記例に戻る.
なるべく汎用性ということを考えて図形クラスの実装について考えてみると,
Drawする方法についてはコンパイル時におそらく決まっているであろう.
そのため下記のようなテンプレートとする.
このようにすれば,関数オブジェクトDrawerを与えれば自由に図形を描画することが可能であろう.
点のみを描画するのか,点を線でつなぐのか,面を塗りつぶすのか.
さらに点を描画するというアルゴリズムは自作したpolygonクラス以外でも可能とするべきであろう.
点データをもつコンテナでありイテレータでアクセスできればよいのである.もちろん点データが共通の基底クラスをもち,その基底クラスが点へのアクセスのためのインターフェイスを定義しているならば,テンプレートでなくその基底クラスを受ける形式でも構わない.しかしながら,全ての点データを派生させるよりも,PointsType::has_iteratorなどのコンセプトチェックをするほうがコストが安く柔軟性があるだろう.
次に,必要なことはコンテナである.最初の例ではデータはstd::vector<std::complex>に限定していた.
点は三次元になるかもしれないし,他のデータを表現するクラスになるかもしれない.そのような場合は以下のように抽象化できるであろう.
N次元に対して対応したものとなる.
最後に図形ごとの特化について考える.このpolygonクラスに円を表すデータを入れて描画させたいとする.
ではCircleクラスはPolygonを継承すべきだろうか.それともPolygonを持つべきだろうか.それとも点データのみを持つべきだろうか.
円とはR^2 = (x-x0)^2 + (y-y0)^2を満たす式である.これはひとつの実装としてpolygonを持つと考えるべきだろうか,Polygon自体であると考えるべきか.
極限まで抽象化するならば,円は上記式でありそれ以上でもそれ以下でもなかろう.
式という特性で定義してみたCircleクラスである.
式という特性で定義してみたCircleクラスである.
大事なことなので二度書いておいた.当然,Polygonクラスを継承し,点データの外部からの変更を許さない仕様としてもよい.
この場合はCircleクラスをPolygonクラス以外の機能,式としての機能を重視しているということである.
ある点が円の式を満たすか,内部にあるのか外部にあるのかについて実装している.他の幾何図形も実装するならば,Equationクラスでも作って継承してもよいだろう.
このCircleクラスはイテレータでResolutionで指定されたデータを返す.円を半径や中心などの定数パラメータ以外で修正することは,許されないとし,const_iteratorのみとした.
ここでcircleとpolygonを効率よくつなぐためにpolygonにイテレータを受けるコンストラクタを作る.
これにより下記のようにpolygonを初期化できる.
他の図形が必要な場合でもこのような実装であれば簡単に追加することができるであろう.
前述したとおりこの実装は一例であり,本来アプリケーションが要求する仕様に応じて設計すべきものである.
なるべく汎用性ということを考えて図形クラスの実装について考えてみると,
- Draw関数はPolygonクラスに実装すべきなのか.
- 図形ごとに特化すべきなのか
Drawする方法についてはコンパイル時におそらく決まっているであろう.
そのため下記のようなテンプレートとする.
class Polygon { template<class drawer> void Draw() ( drawer d ) { d(*this); } };
このようにすれば,関数オブジェクトDrawerを与えれば自由に図形を描画することが可能であろう.
点のみを描画するのか,点を線でつなぐのか,面を塗りつぶすのか.
struct DrawPoint { void operator()(const Polygon& p); }; struct DrawLine { void operator()(const Polygon& p); }; struct DrawFill { void operator()(const Polygon& p); };
さらに点を描画するというアルゴリズムは自作したpolygonクラス以外でも可能とするべきであろう.
点データをもつコンテナでありイテレータでアクセスできればよいのである.もちろん点データが共通の基底クラスをもち,その基底クラスが点へのアクセスのためのインターフェイスを定義しているならば,テンプレートでなくその基底クラスを受ける形式でも構わない.しかしながら,全ての点データを派生させるよりも,PointsType::has_iteratorなどのコンセプトチェックをするほうがコストが安く柔軟性があるだろう.
template<class PointsType> struct DrawPoint { void operator()(const PointsType& p); }; template<class PointsType> struct DrawLine { void operator()(const PointsType& p); }; template<class PointsType> struct DrawFill { void operator()(const PointsType& p); };
次に,必要なことはコンテナである.最初の例ではデータはstd::vector<std::complex>に限定していた.
点は三次元になるかもしれないし,他のデータを表現するクラスになるかもしれない.そのような場合は以下のように抽象化できるであろう.
N次元に対して対応したものとなる.
template<class Ty, size_t N> class Polygon { public: template<class drawer> void Draw(drawer d) { d(*this); } protected: std::vector< std::array<Ty, N> > data; };
最後に図形ごとの特化について考える.このpolygonクラスに円を表すデータを入れて描画させたいとする.
ではCircleクラスはPolygonを継承すべきだろうか.それともPolygonを持つべきだろうか.それとも点データのみを持つべきだろうか.
円とはR^2 = (x-x0)^2 + (y-y0)^2を満たす式である.これはひとつの実装としてpolygonを持つと考えるべきだろうか,Polygon自体であると考えるべきか.
極限まで抽象化するならば,円は上記式でありそれ以上でもそれ以下でもなかろう.
template <typename Ty, size_t N > class circle { public: void SetRadius(Ty arg); void SetCenter(const std::array<Ty, N>& arg); void SetResolution(size_t arg); bool equal( const std::array<Ty, N>& p) const; bool greater_than( const std::array<Ty, N>& p) const; bool less_than( const std::array<Ty, N>& p) const; std::vector< std::array<Ty, N> >::const_iterator begin(); std::vector< std::array<Ty, N> >::const_iterator end(); protected: void Make(); std::vector< std::array<Ty, N> > data; std::array<Ty, N> center; Ty radius; size_t resolution; };
式という特性で定義してみたCircleクラスである.
式という特性で定義してみたCircleクラスである.
大事なことなので二度書いておいた.当然,Polygonクラスを継承し,点データの外部からの変更を許さない仕様としてもよい.
この場合はCircleクラスをPolygonクラス以外の機能,式としての機能を重視しているということである.
ある点が円の式を満たすか,内部にあるのか外部にあるのかについて実装している.他の幾何図形も実装するならば,Equationクラスでも作って継承してもよいだろう.
このCircleクラスはイテレータでResolutionで指定されたデータを返す.円を半径や中心などの定数パラメータ以外で修正することは,許されないとし,const_iteratorのみとした.
ここでcircleとpolygonを効率よくつなぐためにpolygonにイテレータを受けるコンストラクタを作る.
template<typename InputIterator> polygon(const InputIterator first, const InputIterator last) : container(first, last) {}
これにより下記のようにpolygonを初期化できる.
double radius = 10.0; std::array<double, 2> center = { 3.0, 4.0 }; size_t resolution = 24; circle<double, 2> c; c.SetRadius( radius ); c.SetCenter( center ); c.SetResolution( resolution ); polygon polyCircle( c.begin(), c.end() ); polyCircle.Draw( DrawLine() ); //not implemented
他の図形が必要な場合でもこのような実装であれば簡単に追加することができるであろう.
前述したとおりこの実装は一例であり,本来アプリケーションが要求する仕様に応じて設計すべきものである.
コメントをかく