web-dev-qa-db-ja.com

抽象クラスを適切に設計する:データを取得する方法?

私は独学で学んだ経歴を持っており、OOPの弱点、特にC#とクラス設計の弱点を補強しようとしています。私はコードコンプリート2を読んでいて、優れたクラス設計の原則に従っていないことが明らかになりました。現在、ハンドラーがインスタンス化されるときに設定されるReportType列挙に基づいてレポートを生成するReportHandlerクラスがあります。

だから今私はコードをリファクタリングしていて、レポートとそれらの違いを検討することから始めます:

それらはすべて次のプロパティを共有しますが、これらはすべて同じタイプになります。

  • レポートID(ガイド)
  • 開始日(DateTime)
  • 終了日(DateTime)
  • 合計結果(int)

また、データベース(現在は同じデータベースで、将来変更される可能性があります)からデータを取得し、データをオブジェクトに解析して、そのオブジェクトをJSONに変換できる必要もあります。データオブジェクトと解析は、レポートごとに異なります。

だから、これに最適なデザインは何だろうと思います。抽象クラスを作成した場合、次のようにできます。

public abstract class Report
{
    public abstract void GetData();
    public abstract void ParseData();
    public abstract string ToJson();

    public Guid ReportId { get; set; }
    public DateTime StartDate { get; set; }
    public DateTime EndDate { get; set; }
    public int TotalResults { get; set; }

    //Not sure if this is correct, I want to make sure we can't instantiate a derived class without these values.
    protected Report(Guid reportId, DateTime startDate, DateTime endDate)
    {
        if (startDate > endDate)
            throw new ArgumentException("Start date cannot be after end date.");
        ReportId = reportId;
        StartDate = startDate;
        StartDate = endDate;
    }
}

これはクラスを整理するのに役立つと思います。私はこのようなものを作成することができます:

public class SpecificReport : Report
{
    //The raw data type can change between reports.
    private List<string> _rawData;
    public SpecificReportObject ParsedReport { get; private set; }

    public SpecificReport(Guid reportId, DateTime startDate, DateTime endDate)
        : base(reportId, startDate, endDate)
    {

    }

    public override void GetData()
    {
        var rawData = new List<string>();
        //Connect to a database and assign the results to rawData
        _rawData = rawData;
    }

    public override void ParseData()
    {
        var parsedReport = new SpecificReportObject();
        foreach (var result in _rawData)
        {
            //Parse RawData into a specific report object
        }
        ParsedReport = parsedReport;
    }

    public override string ToJson()
    {
        return JsonConvert.SerializeObject(ParsedReport);
    }

    public class SpecificReportObject
    {
        //Specific specific properties and logic
    }
}

今では、それは私にはもっと整理されているように見え、論理的にはもっと理にかなっているようです。レポートをインスタンス化し、GetData()、ParseData()、最後にToJson()を呼び出して、レポートを移植可能な形式で取得できます。

しかし、派生レポートにこの機能が含まれるべきかどうか疑問に思っています。 GetData()とParseData()を呼び出すことを覚えておかなければならないのは嫌ですが、これらの関数をコンストラクターに配置することは悪い考えのようです。失敗する可能性のあるコンストラクター、特にデータベースのクエリのように時間がかかる可能性のある操作にコンストラクターを配置したくありません。

GetDataをクラスから分離する方が良いでしょうか?次に、最初にデータを取得し、それを引数としてParseData関数にロードします。

7
Brian Ge

私はあなたの見方を広げるために、別の提案をもっとします。私が一般的に言うことの背後にある概念をお勧めしますが、それがあなたの状況に最も適しているかどうかを確認するために、より多くのコンテキストが必要になります(おそらく他のコードへの変更を提案するでしょう)。

メソッドを呼び出すことを忘れないようにする必要があることについての懸念に言及するときは、絶対に頭に釘を打ちます。状況はさらに悪化している可能性があります。メソッドを呼び出すことを覚えている場合でも、後でもう一度GetDataを呼び出すとどうなりますか?あなたのコードは現在書かれているので、一貫性のないデータを持つオブジェクトになってしまいます。 (余談ですが、おそらく、クラスからデータをフェッチする場所などの詳細を移動するには、依存性注入(以前は「パラメーター化」と呼ばれていた概念)を参照する必要があります。これについては以下では説明しません。)

この問題の1つの解決策は型システムを使用を使用して不変条件を強化することです。それが目的です。この例をもう少しインパクトのあるものにするために、レポートを解析するタイミングを明示的に制御する必要がある差し迫った必要があると仮定します。 (実際のコードでは、GetDataを公開する理由が明確ではありません。コンストラクタで実行したくない場合は、ParseDataによってオンデマンドで実行してください。)重要なアイデアは次のとおりです。

処理の段階ごとに異なるタイプを作成します。

次のようなものを検討してください:

public interface IReportDataProvider {
    IUnparsedReport Fetch();
}

public interface IUnparsedReport {
    // ...
    IParsedReport Parse();
}

public interface IParsedReport {
    // ...
    string ToJsonString();
}

このアプローチでは、順不同で実行したり、ステップを実行することを忘れたり、ステップを複製して一貫性のないデータを生成したりすることができなくなります。この手法を適用するには、実際には型システムは必要ありません。それは単なるデータの抽象化です。これで型システムが重要になる可能性がある場所の1つは、一部のコンテキストでのParseの可能な実装の1つはreturn this;です。このシナリオでは、型システムを使用して不変式を強制しています。IUnparsedReportを要求しただけでは、取得する特定のインスタンスがIParsedReportも実装していることがわからないためです(ただし、ダウンキャストやリフレクションのようなグロッキーなもの)実際、このアプローチをコードにそのまま適用できます。これにより、ラッパーが必要になったり、データをコピーしたり、コードを複製したりする必要がなくなります。ミューテーションを意味し、隠された共有が混乱を招く結果をもたらす可能性があるため、デフォルトでこれを行うことはお勧めしません。

このアプローチをOOD用語でキャストするには、各ステージを次のステージを生成するファクトリーと見なすことができます。 DataProviderについて議論されているのはBobDalgleishの回答と同様の用語を使用しました。 (それがどれほど一般的であるかによっては、そのように考えるのではなく、IUnparsedReportの特定の実装へのパラメーターとしてそれを提供する方がよいかもしれません。)クラスの説明に同意しませんただし、DataProviderの問題を要約しているだけです。 「接続」を忘れた場合はどうなりますか?この回答で同じテクニックを使用して、DataProviderの説明が示すステージングの問題を解決できます。

より広いマントラがあり、これはインスタンスであり、型付き関数型プログラミングコミュニティで人気がありますが、FPまたは型付きプログラミングコミュニティにさえ限定されません。つまり、

無効な状態を表現できないものにします。

GetDataをクラスから分離する方が良いでしょうか?次に、最初にデータを取得し、それを引数としてParseData関数にロードします。

これが方法です。 DataProviderクラスインスタンスをReportサブクラスのコンストラクターに提供します。 DataProviderインターフェースは、データの接続/切断およびクエリのためのメソッドを提供します。おまけとして、これにより、テストのニーズを満たす特定のDataProviderインスタンスを使用してレポートをテストするためのテストスキャフォールドを構築できます。

3
BobDalgleish

それらはすべて次のプロパティを共有しますが、これらはすべて同じタイプになります

これは、抽象クラスを定義するための基準であってはなりません。このアプローチを使用すると、カプセル化を解除する 構造継承 は壊れやすく、すべて手続き型です。

また、データベース(現在は同じデータベースで、将来変更される可能性があります)からデータを取得し、データをオブジェクトに解析して、そのオブジェクトをJSONに変換できる必要もあります。

それはずっと良いです!あなたはあなたの抽象を特定しました:あなたのレポートがすべきか.

_interface Report
{
    public function initialize();

    public function parse();

    public function toJson();
}
_

ただし、ご指摘のとおり、レポートの使用方法には 時間結合 があります:I have to remember to call GetData() and ParseData()。私も好きではありません。そして、コンストラクターは軽量でなければならないことに同意します。

これらの問題を特定した後、私は自分の抽象化を詳しく調べます。それは何についてですか?データの取得と提示についてです。これはレポートの本質的な動作です。したがって、おそらくこれらの操作を抽象化することができます。お気づきのように、データソースが異なる可能性があり、json表現以外にもあると思います。したがって、データの取得とデータの表現を表す2つの別個の階層があります。

_interface Report
{
    public function display();
}

interface DataStorage
{
    /**
     * @return array
     */
    public function get();
}
_

また、実装は次のようになります。

_class ArrayedReport implements Report
{
    private $reportId;
    private $startDate;
    private $finishDate;
    private $storage;

    public function __construct(ReportId $reportId, DateTime $startDate, DateTime $finishDate, DataStorage $storage)
    {
        $this->reportId = $reportId;
        $this->startDate = $startDate;
        $this->finishDate = $finishDate;
        $this->storage = $storage;
    }

    public function display()
    {
        var_export(
            $this->storage->get(
                $this->reportId,
                $this->startDate,
                $this->reportId
            )
        );
    }
}

class JsonReport implements Report
{
    private $reportId;
    private $startDate;
    private $finishDate;
    private $storage;

    public function __construct(ReportId $reportId, DateTime $startDate, DateTime $finishDate, DataStorage $storage)
    {
        $this->reportId = $reportId;
        $this->startDate = $startDate;
        $this->finishDate = $finishDate;
        $this->storage = $storage;
    }

    public function display()
    {
        echo
            json_encode(
                $this->storage->get(
                    $this->reportId,
                    $this->startDate,
                    $this->reportId
                )
            );
    }
}
_

ここで、データを取得する基準を抽象化することもできます。これにより、3つ目の抽象化が可能になります。

_interface Criteria
{
    /**
     * @return array
     */
    public function value();
}

interface DataStorage
{
    /**
     * @param Criteria $criteria
     * @return array
     */
    public function get(Criteria $criteria);
}

interface ReportRepresentation
{
    public function display();
}
_

これは次のように実装できます。

_class JsonReport implements ReportRepresentation
{
    private $criteria;
    private $storage;

    public function __construct(Criteria $criteria, DataStorage $storage)
    {
        $this->criteria = $criteria;
        $this->storage = $storage;
    }

    public function display()
    {
        echo json_encode($this->storage->get($this->criteria));
    }
}
_

意図的にReportDataと呼ばれる抽象化を導入していないことに注意してください。それは単に基準とデータストレージを保持するためのデータ構造であり、間違いなく単一の実装になります。したがって、それは役に立たない抽象化でしょう。

そして今、私は簡単な単体テストを含むさまざまな目的のために無数の方法で構成できる、複数の実装を持つ3つの抽象化を持っています。それが 良い領域分解 の私の基準であり、設計を真に--- 固体 にするものです。

0
Zapadlo