QA@IT

データ型は異なるが、同一名称のメソッドをデータクラス(機能はクラスにより違う)を実装したいです。

5159 PV

初めて質問させて頂きます。
C# .NET 2010 で開発しています。

継承とインターフェイスとジェネリックスの使い分けというか、
機能差分が分からず、こんがらがっている状態です。

やりたいこと。

1.基本となるクラスにプロパティとメソッドの口だけを作り、
2.実クラスには独自のプロパティを追加した上で、各クラスに併せたメソッドを実装したいです。

(例)
すべてのテーブルには、共通したカラム( ID / 登録日 /修正日)が存在し、
商品(Product)には、共通カラム+独自カラム( 商品コード /商品名/価格/顧客名)が、
人(Personnel)には、共通カラム+独自カラム( 氏名 /役職/会社名/年齢)があるとします。
※ 太字は後述のコードに利用したフィールド

DBには、受注商品(OrderProduct)、パッケージ商品(PackageProduct)、
社員(Staff)、客先担当者(ChargePerson)の各テーブルがあり、
受注商品とパッケージ商品のテーブルカラムは、商品と同一、
社員と客先担当者のテーブルカラムは、人と同一とします。

その際、すべてのデータ型に「Clear」「Copy」のメソッドを実装したいのです。
そして、それらメソッドの実装忘れを防ぐために、実クラス内で宣言するのではなく、
継承かインターフェイスを使って、必ず実装したいのです。

ただ、Copyのように、戻り値のデータ型が異なる場合、
単純に戻り値をジェネリックスのTypeにしてもうまくいかず、悩んでいます。
※サンプルコードでは、object型を使用していますが、
 コピーの戻り値がObject型というのは違う気がしています。

現在のクラス構成は、このような形です。

※ 長くなるので今回の質問に影響がない範囲でプロパティを省略させていただきました。

インターフェイスおよび継承元クラス(スーパークラス)

public interface ITableBaseData
{
    void Clear();

    //Type Copy(); //これだと、継承先(実クラス)の戻り値がType型ではないためにコンパイルエラーになりました。
    object Copy(); 
    //TableBaseData Copy(); //TableBaseDataでは動作しますが、ProductDataでは動作しません。

    // 可能であれば、Add, Change, Remove といったメソッドも定義したいと考えています。
}

public class TableBaseData : ITableBaseData{
    private int __id;
    private DateTime _dtCreate;

    public int _Id{
        get { return __id; }
        set { __id = value; }
    }

    public DateTime DtCreate{
        get { return _dtCreate; }
        set { _dtCreate = value; }
    }

    public void Clear(){
        this.__id = 0;
        this._dtCreate = DateTime.MinValue;
    }

    public object Copy(){
        TableBaseData data = new TableBaseData();

        data.__id = this.__id;
        data._dtCreate = this._dtCreate;

        return data; // コンパイルエラー (Objectにキャストすればコンパイルは通るが望ましくない)
    }
}

public interface IProductData
{
    /// <summary>
    /// 著作権の所有を取得します。(実クラスによって動作を分岐したいメソッドのサンプル)
    /// </summary>
    /// <returns></returns>
    string GetCopyright();
}

public class ProductData : TableBaseData
{
    private string _cd;

    public string Cd{
        get { return _cd; }
        set { _cd = value; }
    }

    public new void Clear(){
    // newを使うと、格納データ型によってはTableBaseData.Clear()が呼ばれてしまうので、できれば避けたいです。
        base.Clear();
        this._cd= "";
    }

    public new object Copy(){
    // newを使うと、格納データ型によってはTableBaseData.Copy()が呼ばれてしまうので、できれば避けたいです。
        ProductData data = new ProductData();

        // ↓↓↓出来れば、TableBaseData.Copy()を適用したいです。
        data._Id = this._Id;
        data.DtCreate = this.DtCreate;
        // ↑↑↑出来れば、TableBaseData.Copy()を適用したいです。

        data._cd = this._cd;
        return data;
    }
}

interface IPersonnelData{
    /// <summary>
    /// 署名を取得します。(実クラスによって動作を分岐したいメソッドのサンプル)
    /// </summary>
    /// <returns></returns>
    string GetPersonnelSign();
}

public class PersonnelData : TableBaseData
{
    private string _name;
    public string Name{
        get { return _name; }
        set { _name = value; }
    }


    public new void Clear(){
    // newを使うと、格納データ型によってはTableBaseData.Clear()が呼ばれてしまうので、できれば避けたいです。
        base.Clear();
        this._name = "";
    }

    public new object Copy(){
    // newを使うと、格納データ型によってはTableBaseData.Copy()が呼ばれてしまうので、できれば避けたいです。
        PersonnelData data = new PersonnelData();

        // ↓↓↓出来れば、TableBaseData.Copy()を適用したいです。
        data._Id = this._Id;
        data.DtCreate = this.DtCreate;
        // ↑↑↑出来れば、TableBaseData.Copy()を適用したいです。

        data._name = this._name;
        return data;
    }
}

派生クラス(サブクラス)

public class OrderProductData : ProductData, IProductData
{
    public string GetCopyright(){
        return this.Customer;
    }
}

public class PackageProductData : ProductData, IProductData
{
    public string GetCopyright(){
        return this.Name;
    }
}

public class StaffData : PersonnelData, IPersonnelData
{
    public string GetPersonnelSign(){
        return this.Name + " です。";
    }
}

public class ChargeParsonData : PersonnelData, IPersonnelData
{
    public string GetPersonnelSign(){
        return this.Name + " 様";
    }
}

また、可能であればすべてのデータ型のメソッドに、
「Add」「Change」「Remove」も追加したいと考えています。

ご意見よろしくお願い致します。

  • もう少し小さいサンプルからスタートした方が良いでしょう(列の数を減らしてみるなど)。あとは機能だけでなく意味的な違いも考えてみてください。 -
  • 現状では「DBにいる」から共通する部分、「人」だから共通する部分、「商品」だから共通する部分、アプリの都合で持たせたい共通機能、がありますが、それぞれどれ(継承とかI/Fとか)で実装すれば良いと思いますか? -
  • すみません、DBやテーブルは例えばの話です。
    -「人」や「商品」のどちらにも共通するもの、
    -「人」に共通するもの、
    -「商品」に共通するもの、
    -アプリの都合でいるもの、
    との分類で良いでしょうか。

    プロパティに関しては、
    すべてに共通するものをBaseクラスとして、
    Baseクラスを継承した「人」や「商品」クラスを派生させれば良いと考えました。
    -
  • しかし、CopyメソッドをBaseクラスに宣言しても、
    「人」や「商品」クラスは別クラスなので、戻り値の型が違うと怒られます。

    今までは実クラスのメソッド群をごっそりコピペしたりしていたのですが、
    今回複数人での同時進行が必要となり、何かしらのテンプレートのようなものを準備した上で、実クラスを分担したいと考えています。
    -
  • サンプルであることは了解しました。それならなおさらプロパティを減らしてコードの量を減らしてほしいですね。ちょっとパッチを書いてその後一次回答を書きます。 -
  • ちなみにジェネリクスを利用する分にはわかるが継承なりインターフェースなりは自分では書いたことはないという認識でいいですか? -
  • 説明が下手で申し訳ないです。
    簡単な継承は作成経験がありますが、インターフェイスや
    ジェネリクスの使用経験はなく、さっぱり分かっておりません。
    また、今まで作った継承は、「人」から「社員」や「客先担当者」を作った程度です。
    (主にプロパティを継承し、メソッドは個別に記述していました。)
    -
  • 短くしてくださってありがとうございます。パッチの方はリジェクト(取り消し?どう書いてあるかわかりませんが、拒否する感じの操作を)してください。 -
  • 長くなりましたがひとまず最後まで書きました。通知が出るようコメントでお知らせ。 -

回答

継承関係が

<TableBaseData> ::= <ProductData> | <PersonnelData>

<ProductData>   ::= <OrderProductData> | <PackageProductData>
<PersonnelData> ::= <StaffData> | <ChargeParsonData>

なのは自然だと思います(BNFぽく書いたのは見やすいかなーと思っただけで気まぐれです)
あとはこれらにどう肉付けするかですね。

newがよくないのはお分かりのようですので特に説明はしません。

まず気になったのはインターフェースの名前です。
ITableBaseData、IProductData、IPersonnelData とオブジェクト名由来になっていますね。
インターフェースは通常 IEnumerableとか IObserverとか IList<T> など実現する機能や性質に由来することが多いと思います。

今回で言えば、GetCopyright が商品すべてに提供される機能ならProductDataにvirtualとして定義してあげれば、各子クラス( OrderProductData など)でoverride可能です。GetCopyright はProductDataだけのものではないのであれば、インターフェースの名前がおかしいという事になります。


追記1

まずジェネリクス以外の方法でCopyを実現してみます。

  1. インターフェース
    インターフェースでの実装は試されたやり方で合っています(理想はおいておいて動くという意味)
public interface ITableBaseCopyable{
    TableBaseData Copy();
}

public class TableBaseData : ITableBaseCopyable{
    public TableBaseData Copy(){
        return new TableBaseData { __id = this.__id };
    }
    /*そのほか省略*/
}
  1. 抽象メソッド

    または、Copyを抽象メソッドにしても同様の事が実現できます

    public abstract class TableBaseData {
    /* インターフェースを使っていないことにも注意↑ */
    
    public abstract TableBaseData Copy();
    

抽象メソッドにひきづられて TableBaseDataも 抽象クラスになり、単体ではインスタンスを作成できなくなりました。
しかし DBにStaffDataやPackageProductDataしかいないのであれば、TableBaseDataで newする必要はありませんので問題はありません。 少なくとも ProductDataとPersonnelDataのどっちなのかわからないという状態で newすることはなさそうですから大丈夫でしょう(正確には設計によりますけど、今はサンプルですので深く追求しません)。


長くなる都合上自動プロパティなども使って短くしていますが、
上のインターフェースを用いた例は以下の様になります。
ここでのポイントは、TableBaseDataの Copyの実装に virtualを用いて子クラスで上書きできるようにした点です。

public interface ITableBaseData
{
    TableBaseData Copy();
}

public class TableBaseData : ITableBaseData
{
    public int _Id{ get; set; }
    public virtual TableBaseData Copy()
    {
        return new TableBaseData() { _Id = this._Id };
    }
}

public class ProductData : TableBaseData
{
    public string Cd{ get; set; }
    public override TableBaseData Copy()
    {
        return (TableBaseData)new ProductData() { _Id = this._Id, Cd = this.Cd };
    }
}

以下の様に使うことができます。

var p = new ProductData() { _Id = 10, Cd = "abc" };
ProductData p2 = (ProductData)p.Copy()
Console.WriteLine((p2.Cd == p.Cd).ToString());

抽象クラスは以下の様になります。

public abstract class TableBaseData
{
    public int _Id { get; set; }
    public abstract TableBaseData Copy();
}

public class ProductData : TableBaseData
{
    public string Cd { get; set; }
    public override TableBaseData Copy()
    {
        return (TableBaseData)new ProductData() { _Id = this._Id, Cd = this.Cd };
    }
}

使い方は全く同じです。

var p = new ProductData() { _Id = 10, Cd = "abc" };
ProductData p2 = (ProductData)p.Copy()
Console.WriteLine((p2.Cd == p.Cd).ToString());

I/Fでやっても抽象クラスでやっても使い方(すなわち外からの見え方)が同じであれば、どちらを使えばいいか。
それはコメントに書いた意味的な話になります。

ちょっといろいろ細かく説明していくと膨大になるので端的にかつ私の独断と偏見で言えば、
ここではインターフェースの方が良いかと思います。
コピー可能なことって、「TableBaseData」の本質とは違うのかなと。
今回の「TableBaseData」はDBのテーブルに入ってるデータの基礎。という意味ですが、「Copy」メソッドが欲しいのはアプリケーション側の都合であってDBのテーブルと関係ないですよね? という理屈です。設計次第なので必ずしもこれが正解ではないです。今回のコレでは私はそう感じる。というだけです。


コメントされていたのと意図が違うかもしれませんが、Baseクラスに子クラスからアクセスできないメンバが存在する場合は、
以下のように外部に公開しないコンストラクタを用意してコピーを行うという手もあります。
コンストラクタではなくて、渡したオブジェクトの値を取り込んでもらうメソッドを親クラスに用意してもいいでしょう。

public abstract class TableBaseData
{
    public int _Id { get; set; }
    public DateTime Hidden { get { return hidden;  } }
    private DateTime hidden = DateTime.Now;

    public TableBaseData() { }

    protected TableBaseData(TableBaseData copyFrom){
        this._Id = copyFrom._Id;
        this.hidden = copyFrom.hidden;
    }

    /* またはこういうメソッドを、子クラスから呼び出すという手も。しかしこの場合は忘れる確率が高いかも。
    protected void CopyValues(TableBaseData copyFrom){
        this._Id = copyFrom._Id;
        this.hidden = copyFrom.hidden;
    }
    */

    public abstract TableBaseData Copy();
}

public class ProductData : TableBaseData
{
    public ProductData():base(){
    }

    protected ProductData(TableBaseData copyFrom): base(copyFrom){
    }

    public string Cd { get; set; }
    public override TableBaseData Copy()
    {
        // return (TableBaseData)new ProductData() { _Id = this._Id, Cd = this.Cd };
        return (TableBaseData)new ProductData(this) { Cd = this.Cd };
    }
}

プライベートメンバのコピー用ソースを用意しておきました。
子クラスから更新できない hiddenもコピーできています。


追記2

さて、昨日の Ext1-1および Ext1-2 (番号振りました)ですが、同様に
PersonnelDataを実装すると問題がおきます。
これはObjectを戻り値としたのを嫌がったのと似た問題です。

var prod = new ProductData() { _Id = 10, Cd = "abc" };
TableBaseData t = prod.Copy();
ProductData prod2 = (ProductData)prod;

// Runtime Error
// PersonnelData person = (PersonnelData)t;
var prod = new ProductData() { _Id = 10, Cd = "abc" };
ITableBaseData prod_i = prod;
var t2 = prod_i.Copy();

ProductData prod2 = (ProductData)prod_i;

// Runtime Error
// PersonnelData person = (PersonnelData)prod_i;
// PersonnelData person = (PersonnelData)prod_i.Copy();

この問題を解決するには、Copyが一体なにをコピーしたいのかをきちん設計する必要があります。

一見すると、ProductDataなのか PersonnelDataなのか区別がつかないからいけないように思えますが、
Copyという機能が、TableBaseDataの性質をコピーしたいだけであった場合、
受け取り側が他のクラスであることを期待しているのが悪いという考え方もあります。

インターフェースや抽象メソッドのタイミングを遅らせて、ProductDataやPersonnnelデータで抽象メソッド、インターフェースを適用する手もありますが、その子クラスでまた同じ問題が発生します。

追記3

Copyによって複製したいのはどういう情報でしょうか。
たとえばOrderProductData.Copy() の戻り値はどんな型であってほしいのか。

ここでクラスの関係をもう一度見てみますが

<TableBaseData> ::= <ProductData> | <PersonnelData>

<ProductData>   ::= <OrderProductData> | <PackageProductData>
<PersonnelData> ::= <StaffData> | <ChargeParsonData>

TableBaseData, ProductData, OrderProductData
あたりが自然でしょうか、このうちOrderProductDataを返してみることにします。

実装の強制を考えたとき普通の継承やインターフェースでは Objectを戻り値の型にしなければならなくなります。
かといって、一番末端のクラス(OrderProductData, PackageData)だけにメソッドを実装したり、
インターフェースを用意すると実装の強制もできないし、共通でもないし、インターフェースなら型の数だけ必要でいいことありません。

というわけでジェネリクスを使ってみます。

まずは抽象クラスです。

public abstract class TableBaseData<T> {
    public abstract T Copy();
}
public abstract class ProductData<T>: TableBaseData<T> {
}
public class OrderProductData : ProductData<OrderProductData> {
    public override OrderProductData Copy() { return new OrderProductData; }
}
public class PackageProductData : ProductData<PackageProductData> {
    // PackageProductData は PackageProductDataを返すことにします。
    public override PackageProductData Copy() { return new PackageProductData); }
}
/*こちらはサンプルで personもちょっと書いておきます*/
public abstract class PersonnelData<T>: TableBaseData<T> {
}
public class StaffData : ProductData<StaffData> {
    public override StaffData Copy() { return new StaffData; }
}

次にインターフェースです。

interface ICopyable<T> {
    T Copy();
}

public abstract class TableBaseData<T>: ICopyable<T> {
    public abstract T Copy();
}
public abstract class ProductData<T>:TableBaseData<T> {
}
public class OrderProductData : ProductData<OrderProductData> {
    public override OrderProductData Copy() { return new OrderProductData(); /*略*/ }
}
public class PackageProductData : ProductData<PackageProductData> {
    public override PackageProductData Copy() { return new PackageProductData(); /*略*/ }
}

これにより、実装を強制させつつ、定義を共通化し、戻りの型を各クラスで変更することが出来ました。

出来ましたが、これだと自分自身しかコピーできません。
ほとんどCloneであり、それならばIClonableインターフェースを使った方が良いでしょうしジェネリクスも必要なさそうです。

ここまで見ていかがでしょうか。

この状態が最初に思っていたものなのであれば、OrderProductDataからPackageProductData へのコピーにはコンバーターのようなものを用意した方がいいかもしれません。

OrderProductData o = new OrderProductData();
PackageProductData pack = o.ToPackage();

しかしこれではクラスが増えるたびに ToXXXXを作る必要がありますね。
コンストラクタのオーバーロードで対応するのも同じ問題があります。

public class OrderProductData : ProductData {
    public OrderProductData() { }
    public OrderProductData(ProductData createFrom) { }
    public OrderProductData(OrderProductData createFrom) : this((ProductData)createFrom) { }
    public OrderProductData(PackageProductData createFrom) : this((ProductData)createFrom) { }
}

ProductDataConverterのような独立した変換クラスを用意する手もあるでしょう。

OrderProductData.Copy()が ProductData部分だけのコピーで構わないのであればそういう実装もできますが、
手に入れたProductDataを拡張することはできませんのでうれしくないかもしれません。
そこから値を取り出すだけなら、たんにキャストするだけでもいいわけですし。

ここまで書いてみました、ジェネリクスのサンプルも提示だけしてみましたが、
Copyでどこまでコピーしたいか整理していろいろ考えてみてください。

実装にはいろいろな方法があります。同じことを別の実装方法で提供できたりもします。何が最適かはやりたいことによって変わってきます。

Copyメソッドに何もかも詰め込もうとしすぎていたのであれば複数のメソッドにわけてもいいでしょう。

コンバート処理が全部用意できないなら、デリゲートやラムダ式をつかって変換処理そのものは共通では用意せず、
アプリケーション実装者に必要な時に作らせるなんていうやり方もあります。

どれが最適なのか、こればっかりはこちらでは決められませんので整理しながらいい方法を探してみてください。

分解しながら説明している途中にピッタリくるのがあれば示そうかとおもったのですが、
CopyではなくConvertに近いのかなと思い始めてしまいました。
他のメソッドはここに示したもので実装できるものもあるかもしれませんね。
それも各メソッドでどんなことをしたいか次第です。

長々と失礼しました。あまり参考にはなっていないかもしれませんが一旦ここで締めさせていただきます。
なにかあればコメントください。

編集 履歴 (11)
  • なるほど、確かに私のつけたインターフェイス名はおかしいですね。
    MSDNを見ていたときに、独自クラスと独自インターフェイスの名称をプレフィクスのあるなしだけにしているサンプルがあったので、それに準拠していました。
    (分かりやすくするためだったのかもしれません。)
    -
  • GetCopyrightメソッドだけであれば、virtualとoverrideで可能なのですが、
    ClearやCopyのメソッドのように、(抜け漏れを防ぐために)出来れば共通プロパティへの操作はbase.Copy等を使いたいのです。

    overrideした場合は、元のメソッドを上書きすると解釈しているのですが、そこが間違いでしょうか?
    -
  • IListとかIDataReaderとかIDataAdapterとかいますので、I+クラス名がダメではないのですが、名前と実装するメソッドの関係性が重要ですね。 -
  • 親クラスでvirtualなメソッドをoverrideしなかった場合は親クラスのメソッドが呼ばれます。
    あとはCopyの場合は子クラスからアクセスできないメンバがコピーされないことを心配されているなら、コンストラクタで対応した方がいい気がしますね。それだけちょっと追記します。
    -
  • overrideしてもbase.Clearなどで親クラスのメソッドを呼ぶことはできますよ。

    class A { public virtual string s() { return "A"; } }
    class B : A { public override string s() { return base.s(); } }

    -
  • なるほど!
    Copyメソッド内でnewした型をキャストして返せば可能ですね。

    このサンプルだと怪しい感じになりますが、PackageProductDataをコピーして、
    OrderProductDataをnewする事も考えられるので、
    もしくはこの2つ以外のProductDataの派生クラスとのコピペも考えられますので、
    「商品」クラスを抽象クラスにはしたくなかったのです。
    -
  • パッケージ商品をカスタマイズして、受注商品を作成する。
    他の例としては、スケジュールをコピーして、履歴を作成する。
    といった使い方を想定しています。
    -
  • そうですね、抽象クラスはそれ自体をnewできませんが、子クラスからキャストすれば利用できます。仮想関数だった場合は、C#では子クラスのものが利用されます。
    -
  • OrderデータはProductDataとは関係なく持てた方がいいような気もしますし、製品(PackageProductData)を注文してOrderProductDataが出来上がるので、PackageProductDataがOrderProductDataの親という選択肢もありそうですが、そういった部分はここでは扱わないものとします。必要があればリファクタしてください。 -
  • 返信遅くなって申し訳ありません。

    > Copyによって複製したいのはどういう情報でしょうか。
    > たとえばOrderProductData.Copy() の戻り値はどんな型であってほしいのか。

    OrderProductData型であって欲しいと考えていたのですが、Copyに関してはご指摘のように「IClonableを使う」「Convertクラスを用意する」方が良い気がしてきました。
    -
  • 最終目標のAdd、Change、Removeは共通メソッドを各クラスに対して強制実装したい(機能は共通だが、動作はクラス毎に異なる)だったので、最後にご提示頂いたジェネリクスとI/Fの組み合わせが一番良さそうです。

    > 実装にはいろいろな方法があります。同じことを別の実装方法で提供できたりもします。何が最適かはやりたいことによって変わってきます。
    を心に刻んで、精進して参ります。
    -

仮に、Copy() のような、比較的単純なインフラ的なメソッドが間違いなく各派生クラスで実装されればいいだけなら、「コード生成してしまう」のも一つの解決策ではないでしょうか。

編集 履歴 (0)

ご質問をざっと拝見しましたが、デザインパターンのコマンドパターンでダウンキャストが必要になるケースと類似した問題かな、と感じました。
Copy の戻り値を object 型にするのはたしかにマズいです。戻り値の型を、「共通したカラム( ID / 登録日 /修正日)」に相当する抽象クラスやインターフェースにすることまでは可能であり、最低限そうしたほうが良いです。しかしそれよりさらに具象化した型を戻り値にすることもできません。やるとしたら上記のようなダウンキャストが必要になります。
このような場面でダウンキャストをなくせれば、オブジェクト指向プログラミングで大革命なことなので、たぶん不可能です。ですから選択肢としては、ダウンキャストを前提とした設計にするか、あるいは、独自カラムの構造を隠蔽したクラスにしてしまうかのどちらかしかないと思います。

編集 履歴 (0)
ウォッチ

この質問への回答やコメントをメールでお知らせします。