- PR -

クラスライブラリ、アセンブリ、名前空間の使い分け

投稿者投稿内容
ひろし
ぬし
会議室デビュー日: 2002/09/16
投稿数: 390
お住まい・勤務地: 兵庫県
投稿日時: 2008-03-16 16:02
ご回答ありがとうございます。
Jittaさんが指摘された通り、リファクタリングの途中で、あるべき姿を模索中の状態です。

名前空間の分割による可視性の制御についても検討中ですが、
とりあえず、以下ではアセンブリの分割による可視性の制御を前提に検討しています。

ソースコードを例にあげて説明させていただきます。

ソースコードでは、リングバッファを管理するクラスライブラリを作成しています。
リングバッファはデータの形式が異なる複数の種類が存在します。
現在はFrameRingBufferとDigitalRingBufferの2種類です。
両者に共通する実装はBaseRingBufferにまとめます。

FrameRingBufferとDigitalRingBufferはリングバッファからデータを取り出すための独自のGetData()メソッドを実装しています。
GetDataメソッドは内部で必要なパラメータをCreateGetDataParams()から取得します。

BaseRingBufferのCreateDataParams()は派生先のFrameRingBufferとDigitalRingBufferのGetData()でしか使う予定が無いメソッドなのでprotectedにします。

説明に登場したクラスを一覧にまとめます。

[現状]
クラス名 アクセサ 備考
DigitalRingBuffer public BaseRingBufferから派生、GetData()を含む。
FrameRingBuffer public BaseRingBufferから派生、GetData()を含む。
BaseRingBuffer public GetDataParamsを返すprotectedなCreateGetData()を含む
GetDataParams public GetData()の内部でしか使われない。

クラスライブラリを使う立場に立つと、4つのクラスが丸見えなのは問題です。
できればBaseRingBufferとGetDataParamsを隠蔽したいと考えます。
つまり、あるべき姿は下記の通りと考えています。

[あるべき姿]
クラス名 アクセサ 外部に対する本来あるべき見え方
DigitalRingBuffer public 公開
FrameRingBuffer public 公開
BaseRingBuffer internal 隠蔽(protected AND internalなCreateGetData()を含む)
GetDataParams internal 隠蔽

まず、BaseRingBufferについてですが、使う立場からすると、BaseRingBufferの存在を意識する必要が無いので、隠蔽できたほうが自然な気がします。しかし、現実は継承の関係で隠蔽できていません。
継承では無く包合にするべきなのでしょうか?FrameRingBufferおよびDigitalRingBufferとBaseRingBufferとの関係は"A is a B"の関係と見ています。この見方が正しければ、やはり継承が正しい選択に思えます。
それでは、継承では無く、インターフェースを使うべきなのでしょうか?BaseRingBufferは実装を共有したいので、継承が正しい選択に思えます。

次に、GetDataParamsについてですが、これは、GetData()専用に設計されたクラスなので、是非とも隠蔽したいところです。上述のBaseRingBufferが隠蔽できない問題は小さな問題だと思いますが、こちらは非常に気になります。
CreateGetData()をprotectedからinternalに変更するという妥協案はあります。しかし、BaseRingBufferはabstractクラスなのでprotectedなメンバーが他にも多く存在します。これらをinternalにするとクラスライブラリ内とはいえBaseRingBufferの内部構造を公開しすぎであるような気がします。
また、GetDataParamsをBaseRingBufferの中で定義するという妥協案もあります。しかし、クラスの中で別のクラスを定義することは、クラスの肥大化という代償が伴います。こちらも避けたいところです。

現状の構造をそのままに保ちつつBaseRingBufferとGetDataParamsを隠蔽するには、プロジェクトの参照の助けを借りるしか無いように思えます。しかし、このような考えも本末転倒のような違和感があります。

私なりに、4つのクラスについてあるべき姿を検討してみましたが、すっきりした解答が見つけられません。アドバイスよろしくお願いします。



ソースコードの一部

// *** FrameRingBuffer.csの中身
using System;
...
namespace AbcCorp.RingBuffer;
// 公開クラス
public sealed class FrameRingBuffer : BaseRingBuffer, IDisposable
{
...
public byte[][] GetData(...)
{

GetDataParams params = CreateGetDataParams(...);
...
}
...
}


// *** DigitalRingBuffer.csの中身
using System;
...
namespace AbcCorp.RingBuffer;
// 公開クラス
public sealed class DigitalRingBuffer : BaseRingBuffer, IDisposable
{
...
public int[] GetData(...)
{

GetDataParams params = CreateGetDataParams(...);
...
}
...
}


// BaseRingBuffer.cs の中身
using System;
...
namespace AbcCorp.RingBuffer;
// 本当は隠蔽したいクラス
public abstract class BaseRingBuffer : IDisposable
{
protected GetDataParams CreateGetDataParams(...) {...}
...
// 他にもprotectedなメンバーが存在する。
...
}


// GetDataParams.cs の中身
using System;
...
namespace AbcCorp.RingBuffer;
// 本当は隠蔽したいクラス
public class GetDataParams
{
...
}

ひろし
ぬし
会議室デビュー日: 2002/09/16
投稿数: 390
お住まい・勤務地: 兵庫県
投稿日時: 2008-03-16 17:13
ご回答ありがとうございます。

indigo-xさんのご指摘には一貫性があって説得力があるような気がします。

> @名前空間(論理構成)
> Aアセンブリ(物理配置)
> Bクラス設計
の順番した方がよいです。

まず、クラスを公開するか隠蔽するかは論理設計の一部と私は理解しています。
この理解が正しいとして、indigo-xさんの主張に従うなら、クラスの公開および隠蔽は名前空間の分割によって制御すべきということになりますが、いかがでしょうか。

名前空間の分割についての考察を書き込みます。

「アセンブリの分割」を検討する前は「名前空間による分割」を検討していました。

前述と同じ例なので重複する説明は省かせていただきます。
リングバッファに関連するクラスを公開クラス群と隠蔽クラス群に分けました。

公開クラス → AbcCorp.RingBuffer.MyLib 名前空間
隠蔽クラス → AbcCorp.RingBuffer 名前空間

他の名前空間からは、"using AbcCorp.RingBuffer.MyLib;"だけを参照することに決めれば、AbcCorp.RingBuffer名前空間にあるクラスを隠蔽できます。そして、隠蔽クラスを公開クラスのルート寄りの名前空間に配置すれば、公開クラスからは、隠蔽クラスを見ることもできます。このように階層づけされた配置スタイルは良くないスタイルでしょうか?あるいは、公開クラスの名前空間と隠蔽クラスの名前空間は階層関係を持たせず、同一の階層に並列にするほうが良いのでしょうか?あるいは、別のもっと良いルールがあるのでしょうか。

namespace AbcCorp.RingBuffer.MyLib;
{
public class DigitalRingBuffer : BaseRingBuffer {...}
public class FrameRingBuffer : BaseRingBuffer {...}
}

namespace AbcCorp.RingBuffer
{
public class GetDataParams {...}
public class BaseRingBuffer {...}
public class UnmanagedBuffer {...}
}

さて、ここに隠蔽クラスの中にUnmanagedBufferクラスがあったとして、
ある時、このUnmanagedBufferを公開クラスに格上げすることになったとします。
UnmanagedBufferクラスの名前空間の移動に伴って関連する呼び出しを全て書き換える必要があります。あるいは、AbcCorp.RingBuffer名前空間に"using AbcCorp.RingBuffer.MyLib;"句を追加すれば良いのでしょうか?
公開クラスへの格上げ時の名前空間の移動についての問題にはどう対処すべきなのでしょうか。
indigo-x
大ベテラン
会議室デビュー日: 2008/02/21
投稿数: 207
お住まい・勤務地: 太陽の塔近く
投稿日時: 2008-03-16 18:02
3点ほど

@隠蔽はおまけ機能です。見えても問題ありません。
(インテリセンスがあるからメソッドがある事がすぐわかりますが
 なければpublicでもわかりません。
 で、最初に書いたEditorBrowsableを使用して消してください)

A名前空間のつけ方は簡単ではありません。
 (抽象度と粒度と集約度を適切にするのが非常に難しいです)

Bリファクタリングが進めばアセンブリは自然と割れます。
 (依存関係と物理的層と論理的層がはっきりして割れます)

で、ひろしさんの名前空間は小さすぎます。(隠ぺいする為に使用しているので。。)
あと、隠ぺいに固執しすぎてます。(隠ぺいしなくてもちゃんと使用してくれます。。)

参考になればと思います。
Azulean
大ベテラン
会議室デビュー日: 2008/01/04
投稿数: 123
お住まい・勤務地: 大阪府
投稿日時: 2008-03-16 19:01
解決法ではないのですが、名前空間に関してはFxCop(Visual Studio Team Editionには別の名前で統合されている?)という静的コード解析で次のようなルールがあります。
http://msdn2.microsoft.com/ja-jp/library/ms182130.aspx

ルール名:少ない型数の名前空間を作成しないでください

※隠すためだけに名前空間を作るのはデザイン的におかしいという主張です。

otf
ベテラン
会議室デビュー日: 2006/08/04
投稿数: 91
投稿日時: 2008-03-16 19:48
引用:

ひろしさんの書き込み (2008-03-16 16:02) より:
まず、BaseRingBufferについてですが、使う立場からすると、BaseRingBufferの存在を意識する必要が無いので、隠蔽できたほうが自然な気がします。しかし、現実は継承の関係で隠蔽できていません。
継承では無く包合にするべきなのでしょうか?FrameRingBufferおよびDigitalRingBufferとBaseRingBufferとの関係は"A is a B"の関係と見ています。この見方が正しければ、やはり継承が正しい選択に思えます。
それでは、継承では無く、インターフェースを使うべきなのでしょうか?BaseRingBufferは実装を共有したいので、継承が正しい選択に思えます。


振る舞いではなく実装の再利用であれば継承ではなく包合が適切だと思います。
クライアントからBaseRingBufferを隠ぺいしたいならなおさらです。
包合にすればBaseRingBufferとGetDataParamsを隠ぺいすることができるようできますね。
その際BaseRingBufferはRingBufferImplなどのクラス名に変更するのがよいと思います。

コード:
    internal class GetDataParams { }

    internal class RingBufferImpl
    {
        public GetDataParams CreateGetDataParams() { throw new NotImplementedException(); }
    }

    public sealed class FrameRingBuffer
    {
        RingBufferImpl _impl;

        public FrameRingBuffer()
        {
            _impl = new RingBufferImpl();
        }

        public byte[][] GetData()
        {
            GetDataParams p = _impl.CreateGetDataParams();
            throw new NotImplementedException();
        }
    }

    public sealed class DigitalRingBuffer
    {
        RingBufferImpl _impl;

        public DigitalRingBuffer()
        {
            _impl = new RingBufferImpl();
        }

        public int[] GetData()
        {
            GetDataParams p = _impl.CreateGetDataParams();
            throw new NotImplementedException();
        }
    }


そしてFrameRingBufferとDigitalRingBufferの間に振る舞いの共通性があればインターフェースを、
または、振る舞いとImplを使用している実装の共通性があれば抽象クラスを新しく導入すればよいかと思います。
これで可視性の問題はなくなったのですべて同一名前空間でよいはずです。
ひろし
ぬし
会議室デビュー日: 2002/09/16
投稿数: 390
お住まい・勤務地: 兵庫県
投稿日時: 2008-03-17 20:58
ご回答ありがとうございます。

継承か?包合か?という点について、私なりに調べ直しました。
ファウラーの本に今回のケースに対する回答が記述されていました。

参考書籍
「リファクタリング・プログラミングの体質改善テクニック」
マーチン・ファウラー著
ピアソンエディション
第11章継承の取り扱い
委譲による継承の置き換え p352
継承による委譲の置き換え p355

「コードコンプリート」下
スティーブ・マコネル著
日経BPソフトプレス
6.3.2 継承[is a]の関係

解決法は、明確になりました。otfさんがご指摘になった通りだと思います。

振る舞いを共有する場合(A is a Bの関係) → 継承
実装を共有する場合(A is a Bで無い関係) → 包合(委譲)

今回の問題は解決したかもしれませんが、
依然として自分自身が「振る舞い」と「実装」の違いを明確に区別できているか心配です
問い クラスAとBの違いは何ですか?
クラスA:バブルソートで文字列を昇順に並べる。
クラスB:シェルソートで文字列を昇順に並べる。
答え 振る舞いは同じだが、実装が違う。

上記のような場合は簡単です。しかし、見分けにくいケースも多々あると思います。
この件については別スレで再度質問させていただきます。

スキルアップ/キャリアアップ(JOB@IT)