緊密結合コードの作成

第一章で述べたように、ホルダンソースは卵黄とバターで作られるエマルジョンソースですが、これは魔法のようにホルダンソースを作る能力を与えてくれません。学習の最良の方法は実践ですが、一つの例が理論と実践の間のギャップを埋めることができます。自分で試す前に、プロのシェフがソースを作るのを見るのは非常に役立ちます。

前章で依存性注入(DI)を紹介したとき、その目的と一般的な原則を理解するための高度なチュートリアルを紹介しました。しかし、この簡単な説明はDIに対して不公平です。DIは**疎結合(Loose Coupling)**を実現する方法の一つであり、**疎結合(Loose Coupling)**はまず複雑さを処理する効果的な方法です。

ほとんどのソフトウェアは複雑です。なぜなら、多くの問題を同時に処理しなければならないからです。ビジネス関心事自体が複雑である可能性があるだけでなく、ソフトウェアはセキュリティ、診断、操作、パフォーマンス、スケーラビリティに関連する問題も解決しなければなりません。**疎結合(Loose Coupling)**は、すべての問題を大きな泥団子の中で解決するのではなく、それぞれの問題を個別に解決することを奨励します。それぞれの問題を個別に解決することは簡単ですが、最終的には、この複雑な問題のセットを単一のアプリケーションに組み合わせる必要があります。

この章では、より複雑な例を見ていきます。緊密結合されたコードを書くのがいかに簡単かを見ていきます。また、**緊密結合(tightly coupled)されたコードが保守性の観点からなぜ問題であるかを一緒に分析します。第3章では、この緊密結合(tightly coupled)されたコードベースを完全に疎結合(Loose Coupling)されたコードベースに書き直します。すぐに疎結合(Loose Coupling)**されたコードを見たい場合は、この章をスキップするかもしれません。そうでなければ、この章を読み終えた後、**緊密結合(tightly coupled)**されたコードがなぜこれほど問題であるかを理解し始めるはずです。

緊密結合されたアプリケーションの構築(Building a tightly coupled application)

**疎結合(Loose Coupling)コードを構築するという考えは特に論争の的ではありませんが、理論と実践の間には大きなギャップがあります。次章でDIを使用して疎結合(Loose Coupling)**されたアプリケーションを構築する方法を示す前に、どれほど簡単に間違えることができるかを示したいと思います。**疎結合(Loose Coupling)**コードの一般的な試みは、階層型アプリケーションを構築することです。誰でも3層アプリケーションの図を描くことができますし、図2.1もそれを証明しています。

3層の図を描くことは簡単に見えますが、図を描く行為は、あなたのステーキにホルダンソースをかけたいという意図の宣言に似ています:これは最終結果に対する保証は何もありません。他の問題に遭遇する可能性があり、すぐに見ていきます。

柔軟で保守可能な複雑なアプリケーションを設計および表示する方法は多种多様ですが、n層アプリケーションアーキテクチャは、よく知られており、テスト済みの方法を構成します。課題は、どのように正しく実装するかです。図2.1に示すような3層図を使用して、アプリケーションを構築し始めることができます。

図2.1 標準的な3層アプリケーションアーキテクチャ。これはn層アプリケーションアーキテクチャの最も単純で一般的な変種であり、アプリケーションはn個の異なる層で構成されます。

メアリー・ローナン(Mary Rowan)を紹介します

メアリー・ローナンは、主にWebアプリケーションを開発する地元の認定Microsoftパートナーで働くプロの.NET開発者です。彼女は34歳で、ソフトウェア業界で11年間働いています。これにより、彼女は同社の経験豊富な開発者の一人になります。上級開発者としての通常の職務をこなすだけでなく、彼女は頻繁にジュニア開発者のメンターも務めています。全体的に、メアリーは自分の仕事に満足していますが、マイルストーンが頻繁に逃し、彼女と同僚たちが最終期限前に仕事を完了するために残業や週末作業を余儀なくされるため、彼女は非常にイライラしています。

彼女は、より効率的なソフトウェアを構築するより良い方法があると疑っています。効率を学ぶために、彼女は多くのプログラミングの本を購入しましたが、彼女の余暇のほとんどが夫と2人の娘との過ごす時間に費やされるため、読む時間はほとんどありません。メアリーは山でハイキングするのが好きです。彼女は情熱的なシェフでもあり、本物のホルダンソースを作る方法を絶対に知っています。

メアリーは、SQL Serverをデータストアとして使用して、ASP.NET Core MVCEntity Framework Coreで新しい電子商取引アプリケーションを作成するよう求められました。モジュール性を最大化するために、それは3層アプリケーションでなければなりません。

実装する最初の機能は、データベーステーブルから特色商品を抽出し、Webページに表示する単純なリスト(図2.2の例のように)です。そして、リストを表示しているユーザーが優先顧客である場合、すべての商品の価格は5%割引されるべきです。

図2.2 メアリーが開発する電子商取引Webアプリケーションのスクリーンショット。特色商品とその価格リストを提供します。

最初の機能を完了するために、メアリーは次の機能を実装する必要があります:

  • データ層(A data layer)—データベース内のProductsテーブル(すべてのデータベース行を表す)とProductクラス(単一のデータベース行を表す)を含む
  • 領域層(A domain layer)—特色商品を取得するロジックを含む
  • MVCコントローラーを持つUI層(A UI Layer with an MVC controller)—入ってくるリクエストを処理し、領域層から関連データを取得し、それをRazorビューに送信し、最終的に特色商品リストを表示する

メアリーがアプリケーションの最初の機能を実装するプロセスを確認しましょう。

データ層の作成(Creating the data layer)

メアリーはデータベーステーブルからデータを抽出する必要があるため、データ層の実装から始めました。最初のステップは、データベーステーブル自体を定義することです。メアリーはSQL Server Management Studioを使用して、表2.1に示すテーブルを作成しました。

表2.1 メアリーが作成したProductsテーブルには、以下の列が含まれています。

列名 データ型 NULL許可 主キー
Id uniqueidentifier No Yes
Name nvarchar(50) No No
Description nvarchar(max) No No
UnitPrice money No No
IsFeatured bit No No

データアクセス層を実装するために、メアリーはソリューションに新しいライブラリを追加しました。以下のリストは、彼女の商品クラスを示しています。

リスト2.1 メアリーの商品クラス

public class 商品
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public decimal UnitPrice { get; set; }
    public bool IsFeatured { get; set; }
}

メアリーは、データアクセスのニーズを満たすためにEntity Frameworkを使用しました。彼女はMicrosoft.EntityFrameworkCore.SqlServerファイルNuGetパッケージへの依存関係を追加し、アプリケーションがProductsテーブルにアクセスできるようにする特定のアプリケーション固有のDbContextクラスを実装しました。このクラスはCommerceContextクラスとして知られています。以下のリストは、彼女のCommerceContextクラスを示しています。

リスト2.2 メアリーのCommerceContextクラス

public class 商業コンテキスト : Microsoft.EntityFrameworkCore.DbContext
{
    public DbSet<商品> 商品 { get; set; }  <--- 基礎データベースのProductテーブルでクエリを有効にする
        
    protected override void OnConfiguring(
        DbContextOptionsBuilder builder)  <-- 作成されたコンテキストの各インスタンスを呼び出して構成する
    {
        var config = new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile("appsettings.json")
            .Build();   <--- 設定ファイルを読み込む(リスト1.2のような内容)
        string connectionString = config.GetConnectionString( "CommerceConnectionString");
        builder.UseSqlServer(connectionString);  
        <-- 設定ファイルから接続文字列を読み取り、DbContextOptionsBuilderに適用します。これにより、アプリケーションのCommerceContextを効果的に構成できます。
    }
}

CommerceContextが設定ファイルから接続文字列を読み込むため、そのファイルを作成する必要があります。メアリーは、appsettings.jsonという名前のファイルをWebプロジェクトに追加し、以下の内容を含めました:

{
    "ConnectionStrings": {
        "CommerceConnectionString":
        "Server=.;Database=MaryCommerce;Trusted_Connection=True;"
    }
}

Entity Framework Core クイックスタート

Entity Framework CoreはMicrosoftのオブジェクト/リレーショナルマッパー、略してORMです。これはリレーショナルデータベースモデルとオブジェクト指向のコード(C#のような)の間のギャップを埋めます。開発者はより高い抽象レベルで作業できます。なぜなら、私たちは自分でSQLクエリを書いていないからです:Entity Framework CoreがC#からSQLへの変換を私たちのために行ってくれるからです。

エンティティフレームワークの中心クラスはDbContextです。DbContextクラスはユニットオブワークです。1つのユニットオブワークは単一のビジネストランザクションに必要なオブジェクトのローカルキャッシュで構成されます。DbContextは、例えばProductsテーブルのようなデータベース内のデータにアクセスできます。

もしMicrosoft Entity Frameworkに慣れていない場合は、心配しないでください。この場合、データアクセス実装の詳細はそれほど重要ではなく、他のデータアクセス技術に精通していても、この例に従うことができるはずです。

警告 CommerceContextが設定ファイルから接続文字列を読み込む—これは罠です。各新しいCommerceContextが設定ファイルを読み込むようにしますが、アプリケーションが実行されている間、設定ファイルは通常変更されません。CommerceContextはハードコードされた接続文字列を含むべきではありませんが、設定システムから設定値を読み込むべきでもありません。第2.3.3節でこれについて説明します。

CommerceContextProductは同じアセンブリ内の公開型です。メアリーは、後でアプリケーションにさらに多くの機能を追加する必要があることを知っていますが、最初の機能を実装するために必要なデータアクセスコンポーネントはすでに完成しています(図2.3)。

図2.3 メアリーが図2.1で想定した階層型アーキテクチャを実装するのにどれほど進んだか。

データアクセス層が実装されたので、次の論理的なステップは領域層(domain layer)です。領域層は、ドメイン層、ビジネス層、またはビジネスロジック層とも呼ばれます。ドメインロジックは、アプリケーションが構築されたドメインに固有のすべての動作です。

領域層の作成(Creating the domain layer)

純粋なデータレポートアプリケーションを除いて、常にドメインロジックが存在します。最初は気づいていないかもしれませんが、ドメインを理解するにつれて、その内蔵された暗黙のルールと仮定が浮き彫りになります。ドメインロジックがない場合、技術的にUI層からCommerceContextによって公開されている商品リストを直接使用できます。

警告 UIまたはデータアクセス層でドメインロジックを実装すると、苦痛が生じます。自分のために、最初から領域層を作成してください。

メアリーの申請要件では、優先顧客に表示される商品の表示価格が5%割引されるべきであると規定されています。メアリーは優先顧客を特定する方法をまだ理解していないので、同僚のJensにアドバイスを求めました:

メアリー:このビジネスロジックを実装する必要があります。そうすれば、優先顧客は5%の割引を受けることができます。

Jens: 聞こえます。0.95を掛けます。

メアリー:ありがとう、ですが、それは私が尋ねたいことではありません。優先顧客を特定するにはどうすればよいですか?

Jens: 分かりました。これはWebアプリケーションですか、それともデスクトップアプリケーションですか?

メアリー:これはWebアプリケーションです。

Jens: 好的、その場合、HttpContextUserプロパティを使用して、現在のユーザーがPreferredCustomerロールにあるかどうかを確認できます。

メアリー:待って、Jens。このコードは領域層に属する必要があります。これはライブラリです。HttpContextはありません。

Jens: ああ。[少し考えて]私はまだあなたがASP.NETを使用してユーザーの値を検索するべきだと思います。その値はブール値としてドメインロジックに渡すことができます。

メアリー:分かりません。。。

Jens: これはまた、セキュリティの問題を処理する必要がないため、優れた関心事の分離を保証します。ご存知のように、単一責任の原則!これはアジャイルなアプローチです!

メアリー:あなたが言う通りだと思います。

Jensのアドバイスは彼のASP.NETの技術知識に基づいています。議論が彼の快適ゾーンから離れたため、彼はメアリーを3つの文で組み合わせて、彼女を上昇させました。Jensが何を言っているのか知らないことに注意してください:

  • 彼は関心事の分離の概念を誤用しています。セキュリティの問題をドメインロジックから分離することは重要ですが、それをプレゼンテーション層に移動しても問題を分離しません。
  • 彼がアジャイルを言及したのは、最近誰かが熱心に話していたからです。
  • 彼は単一責任の原則の要点を完全に見逃しています。アジャイルメソッドが提供する迅速なフィードバックサイクルは、ソフトウェア設計をそれに応じて改善するのに役立ちますが、ソフトウェア設計原則としての単一責任の原則(SRP)自体は選択されたソフトウェア開発メソッドから独立しています。

単一責任の原則(Single Responsibility Principle)

第一章で議論したように、単一責任の原則(SRP)は、各クラスが単一の責任を持つべきである、またはより正確に言えば、クラスは単一の変更理由を持つべきであると規定しています。

もし私たちがSQLステートメントをHTMLマークアップを含むビューに直接配置した場合、私たちはすぐに同意するでしょう。SQLステートメントを変更するのとマークアップを変更するのでは、異なる時間、異なる率、そして異なる理由で発生します。私たちがデータモデルを変更したり、パフォーマンスを調整する必要があるとき、私たちのSQLステートメントも変更されます。一方、私たちがWebアプリケーションの外観を変更する必要があるとき、私たちのマークアップも変更されます。これらは、異なる理由で変更される異なる関心事です。したがって、SQLステートメントを直接ビューに配置することは、SRPに違反します。

しかし、クラスに複数の変更理由があるかどうかを検出することは、通常、より挑戦的です。助けになるのは、コードの凝集性からSRPを考えることです。凝集性は、クラスまたはモジュールの要素の機能的な関連性として定義されます。関連性が低いほど、凝集性が低く、クラスがSRPに違反するリスクが高いほどです。

SRP違反を検出できることは一回事ですが、それを解決すべきかどうかを判断することは別の問題です。症状がない場合は、SRPを適用するのは不適切です。不必要にクラスを分割すると、保守性の問題を引き起こさない追加の複雑さが増加します。ソフトウェア設計の秘訣は、複雑さを管理することです。

Jensの悪いアドバイスを利用して、メアリーは新しいC#ライブラリプロジェクトを作成し、リスト2.3に示すようにProductServiceという名前のクラスを追加しました。ProductServiceクラスをコンパイルするには、CommerceContextクラスがそこで定義されているため、データアクセス層への参照を追加する必要がありました。

リスト2.3 メアリーのProductServiceクラス

public class 商品サービス
{
    private readonly 商業コンテキスト dbコンテキスト;
    public 商品サービス()
    {
        this.dbコンテキスト = new 商業コンテキスト();  <--- 後で使用するために新しいCommerceContextインスタンスを作成する
    }
    
    public IEnumerable<商品> 特色商品を取得する(
        bool isCustomerPreferred)
    {
        decimal discount = isCustomerPreferred ? .95m : 1;
        
        var featuredProducts =
            from product in this.dbコンテキスト.商品
            where product.IsFeatured
            select product;  <----- データベースからすべての商品を取得し、特色商品でフィルタリングする
                
        return
            from product in
            featuredProducts.AsEnumerable()
            select new 商品
        {
            Id = product.Id,
            Name = product.Name,
            Description = product.Description,
            IsFeatured = product.IsFeatured,
            UnitPrice =
                product.UnitPrice * discount
        };  <---- 指定された顧客の割引率に基づいて割引商品リストを作成する
    }
}

メアリーは、ProductServiceクラスでデータアクセス技術(Entity Framework Core)、設定、ドメインロジックをカプセル化しました。彼女はisCustomerPreferredパラメータを渡すことでユーザーの知識を呼び出者に委任し、この値を使用してすべての商品の割引を計算しました。

さらなる改善には、ハードコードされた割引値(.95)を構成可能な数値に置き換えることが含まれるかもしれませんが、当面は、この実装で十分です。メアリーはほとんど完成しました。残っているのはUI層だけです。メアリーは明日まで待つことができると判断しました。図2.4は、メアリーが図2.1で想定したアーキテクチャを実装するのにどれほど進んだかを示しています。

図2.4 図2.3と比較して、メアリーは現在、データアクセス層と領域層を実装しています。UI層はまだ実装されていません。

メアリーが気づいていないのは、ProductServiceがデータアクセス層のCommerceContextクラスに依存するようにすることで、彼女が領域層をデータアクセス層に緊密に結合したことです。第2.2節でこれがなぜ問題であるかを説明します。

UI層の作成(Creating the UI layer)

次の日に、メアリーは電子商取引アプリケーションを続行し、ソリューションに新しいASP.NET Core MVCアプリケーションを追加しました。ASP.NET Core MVCフレームワークに慣れていない場合は、心配しないでください。MVCフレームワークがどのように機能するかの複雑な詳細は、この議論の焦点ではありません。重要な部分は依存関係の使用方法であり、これは比較的プラットフォームに依存しないテーマです。

ASP.NET Core MVC クイックスタート

ASP.NET Core MVCの名前はモデル-ビュー-コントローラー設計パターンに由来します。この場合、最も重要なのは、Webリクエストが到着したとき、コントローラーがリクエストを処理し、それを(ドメイン)モデルを使用して処理し、最終的にビューによって表示されるレスポンスを形成することを理解することです。

コントローラーは通常、抽象Controllerクラスから派生したクラスです。それは1つまたは複数のリクエストを処理するアクションメソッドを持っています;例えば、HomeControllerクラスは通常、デフォルトページへのリクエストを処理するためのIndexという名前のメソッドを持っています。アクションメソッドが戻ると、それはViewResultインスタンスを介して結果モデルをビューに渡します。

次のリストは、メアリーがHomeControllerクラスのIndexメソッドを実装する方法を示しています。データベースから特色商品を抽出し、それをビューに渡す方法です。このコードをコンパイルするには、データアクセス層と領域層の両方に参照を追加する必要があります。これは、ProductServiceクラスが領域層で定義され、Productクラスがデータアクセス層で定義されているためです。

リスト2.4 デフォルトコントローラークラスのIndexメソッド

public ViewResult Index()
{
    bool isPreferredCustomer = this.User.IsInRole("PreferredCustomer");   <---- 顧客が優先顧客かどうかを判断する
        
    var service = new 商品サービス();  <--- 領域層からProductServiceを作成する
        
    var products = service.特色商品を取得する(isPreferredCustomer);    <--- 商品サービスから特色商品リストを取得する(データアクセス層で定義)
        
    this.ViewData["Products"] = products;  <----- 商品リストをコントローラーの汎用ViewDataディクショナリに保存し、後でビューで使用する
        
    return this.View();
}

ASP.NET Core MVCライフサイクルの一部として、HomeControllerクラスのUserプロパティは正しいユーザーオブジェクトで自動的に埋められるため、メアリーは現在のユーザーが優先顧客であるかどうかを判断するためにそれを使用します。この情報を使用して、彼女はドメインロジックを呼び出して特色商品リストを取得できます。

注意 メアリーが領域層を作成したとき、再び緊密結合されたコードを作成しました。この場合、HomeControllerProductServiceに緊密に結合されています。ProductServiceが安定した依存関係であった場合、これはそれほど悪くありませんが、第1章で学んだように、ProductServiceは不安定な依存関係です。それは不安定です。なぜなら、関係データベースのセットアップと構成の要件を導入するからです。

メアリーのアプリケーションでは、商品リストはIndexビューによって表示する必要があります。以下のリストは、ビューのマークアップを示しています。

リスト2.5 Index ビューマークアップ

<h2>特色商品</h2>
<div>
    @{
    	var products =
    		(IEnumerable<商品>)this.ViewData["Products"];  <-- コントローラーが設定した商品を取得する
    	foreach (商品 product in products)  <--- 商品をループし、単一価格をフォーマットし、HTMLとして表示する
    	{
    		<div>@product.Name (@product.UnitPrice.ToString("C"))</div>
    	}
    }
</div>

ASP.NET Core MVCは、作成したビューのコントローラーによって作成および割り当てられたオブジェクトにアクセスするための命令型コードを埋め込んだ標準のHTMLを記述できるようにします。この場合、HomeControllerIndexメソッドは特色商品リストをMaryという名前のキーに割り当て、Maryのビューで商品リストを表示するために使用されます。図2.5は、メアリーが図2.1で想定したアーキテクチャを実装するのにどれほど進んだかを示しています。

図2.5 メアリーは現在、アプリケーションのすべての3つの層を実装しています。

これらの3つの層で、アプリケーションは理論的には機能するはずです。しかし、アプリケーションを実行して検証できるのはメアリーだけです。

緊密結合されたアプリケーションの評価(Evaluating the tightly coupled application)

メアリーはすべての3つの層を実装したので、このアプリケーションが機能するかどうかを見てみましょう。彼女はF5キーを押し、図2.2に示すWebページが表示されます。特色商品機能は現在完成しており、メアリーは自信を持ってアプリケーションで次の機能を実装する準備ができています。毕竟、彼女は確立されたベストプラクティスに従い、3層アプリケーションを作成しました……またはそうでしょうか?

メアリーは適切な階層型アプリケーションを開発しましたか?いいえ、彼女はしませんでした。ただし、もちろん、最善の意図がありました。彼女は計画されたアーキテクチャに対応する3つのVisual Studioプロジェクトを作成しました。不注意な観察者にとって、これは魅力的な階層型アーキテクチャのように見えますが、ご覧のとおり、コードは緊密に結合されています。

Visual Studioでソリューションを使用すると、重要な依存関係を忘れがちです。なぜなら、Visual Studioは、.NET基本クラスライブラリ(BCL)のアセンブリを指す可能性のある他のすべてのプロジェクト参照と一緒にそれらを表示するからです。メアリーのアプリケーション内のモジュール間の相互関係を理解するために、依存関係図を描くことができます(図2.6を参照)。

図2.6から得られる最も顕著な洞察は、UI層が同時に領域層とデータアクセス層の両方に依存していることです。一部のケースでは、UIが領域層をバイパスできるように見えます。これはさらに調査が必要です。

図2.6 メアリーのアプリケーションの依存関係図。モジュール間の相互依存関係を示しています。矢印はモジュールの依存関係を指します。

可組合性の評価(Evaluating composability)

3層アプリケーションを構築する主な目標は**関心事の分離(Separation Of Concerns)**です。私たちはドメインモデルをデータアクセス層とUI層から分離したいので、問題がドメインモデルを汚染しないようにします。大規模なアプリケーションでは、アプリケーションの各領域を個別に処理できる必要があります。メアリーの実装を評価するために、私たちは単純な質問をすることができます:各モジュールを個別に使用できますか?

理論的には、私たちはモジュールを好きなように組み合わせるべきです。私たちは、既存のモジュールを新しいおよび予期せぬ方法でバインドするために新しいモジュールを書く必要があるかもしれませんが、理想的には、既存のモジュールを変更せずにそれができるべきです。私たちはメアリーのアプリケーション内のモジュールを新しいおよび興味深い方法で使用できますか?いくつかの可能なシナリオを見てみましょう。

注意 以下の分析は、モジュールを置き換えることができるかどうかを議論していますが、これは可組合性を評価するために使用する技術であることに注意してください。私たちがモジュールを交換したくない場合でも、この分析は、潜在的な結合の問題を明らかにします。私たちがコードが緊密結合であると判断した場合、**疎結合(Loose Coupling)**のすべての利点が失われます。

新しいUIの構築(Building a new UI)

メアリーのアプリケーションが成功した場合、プロジェクトの利害関係者は、彼女にWindows Presentation Foundation(WPF)でリッチクライアントバージョンを開発するよう求めるかもしれません。領域層とデータアクセス層を再利用しながら、これが可能でしょうか?

図2.6の依存関係図を確認すると、Web UIに依存するモジュールがないことがすぐにわかります。したがって、それを削除してWPF UIに置き換えることができます。WPFに基づいてリッチクライアントを作成するのは新しいアプリケーションであり、元のWebアプリケーションとその実装の大部分を共有します。図2.7は、WPFアプリケーションがどのように元のWebアプリケーションと同じ依存関係を持つ必要があるかを示しています。元のWebアプリケーションは変更なしで保持できます。

図2.7 Web UIWPF UIで置き換えることは可能です。なぜなら、Web UIに依存するモジュールがないからです。破線のボックスは、私たちが交換したい部分を示します。

メアリーの実装はもちろんUI層を置き換えることができます。別の興味深い分解を研究しましょう。

新しいデータアクセス層の構築(Building a new data access layer)

メアリーの市場アナリストは、彼女のアプリケーションが利益を最大化するために、Microsoft Azureでホストされているクラウドアプリケーションになるべきだと考えています。Azureでは、データは高可用性のAzure Table Storage Serviceに保存できます。このストレージメカニズムは、制約のないデータを含む柔軟なデータコンテナに基づいており、特定のデータベーススキーマを強制しませんし、参照整合性もありません。

.NET上で最も一般的なデータアクセス技術はADO.NETデータサービスに基づいていますが、Table Storage Serviceと通信するためのプロトコルはHTTPです。このタイプのデータベースは、Entity Framework Coreを介してアクセスされるリレーショナルデータベースとは異なるキーバリューデータベースと呼ばれることがあります。

電子商取引アプリケーションをクラウドアプリケーションにするには、Table Storage Serviceを使用するモジュールでデータアクセス層を置き換える必要があります。これが可能でしょうか?

図2.6の依存関係図から、私たちはすでにUI層と領域層がEntity Frameworkに基づくデータアクセス層に依存していることを知っています。元のデータアクセス層を削除しようとすると、ソリューションは他のすべてのプロジェクトをリファクタリングしない限りコンパイルされなくなります。数十のモジュールを持つ大規模なアプリケーションでは、コンパイルされないモジュールを削除して残りを見ることもできます。メアリーのアプリケーションでは、元のデータアクセス層を削除すると、残りは何もありません(図2.8のように)。

元のデータアクセス層が公開するAPIを模倣するAzure Tableデータアクセス層を開発することは可能ですが、アプリケーションの他の部分を触れずにそれを適用することはできません。アプリケーションの可組合性は、プロジェクトの利害関係者が望んでいたほどではありません。利益を最大化するクラウド機能を有効にするには、アプリケーションの主要な書き直しが必要です。なぜなら、既存のモジュールのいずれも再利用できないからです。

図2.8 リレーショナルデータアクセス層を置き換えようとする試み

翻訳

The domain layer depends on the relational data access layer.

領域層はリレーショナルデータアクセス層に依存しています。

Attempting to remove the relational data access layer leaves nothing left because all other layers depend on it. You'll have to replace all three layers together. There’s no place where you can instruct the domain layer to use the new Azure Table data access layer instead of the original

リレーショナルデータアクセス層を削除しようとすると、他のすべての層が依存しているため、何も残りません。3つの層を一緒に置き換える必要があります。領域層に、新しいAzure Tableデータアクセス層を元のものの代わりに使用するように指示できる場所はありません。

他の組み合わせの評価(Evaluating other combinations)

アプリケーションに対して他のモジュールの組み合わせ分析を行うことができますが、これは議論の的になるでしょう。なぜなら、私たちはすでに重要なシナリオをサポートできないことがわかっているからです。さらに、すべての組み合わせが意味を持つわけではありません。

たとえば、ドメインモデルを他の実装で置き換えることが可能かどうかを尋ねるかもしれません。しかし、ほとんどの場合、これは奇妙な質問です。なぜなら、ドメインモデルはアプリケーションの核心をカプセル化するからです。ドメインモデルがなければ、ほとんどのアプリケーションは存在する理由がありません。

可組合性の欠如の分析(Analysis of missing composability)


なぜメアリーの実装は期待される可組合性の程度に達しなかったのでしょうか?UIがデータアクセスライブラリに直接依存しているからでしょうか?この可能性をより詳細に調べてみましょう。

依存関係図の分析(Dependency graph analysis)

なぜUIがデータアクセスライブラリに依存するのでしょうか?罪はこのドメインモデルのメソッドシグネチャにあります:

ProductServiceクラスのGetFeaturedProductsメソッドは一連の商品を返しますが、Productクラスはデータアクセス層で定義されています。GetFeaturedProductsメソッドを使用するクライアントは、コンパイルするためにデータアクセス層への参照を持つ必要があります。メソッドのシグネチャを変更して、ドメインモデルで定義された型を返すようにすることができます。それはより正確ですが、問題を解決しません。

UIとデータアクセスライブラリの間の依存関係を削除したと仮定した場合の依存関係図を図2.9に示します。

この変更は、Azure Tableサービスへのアクセスをカプセル化する層でリレーショナルデータアクセス層を置き換えることができるようにメアリーにしますか?残念ながら、領域層はまだデータアクセス層に依存しています。逆に、UIはまだドメインモデルに依存しています。元のデータアクセス層を削除しようとすると、アプリケーションは何も残りません。問題の根本原因は他の場所にあります。

図2.9 UIがデータアクセス層への依存関係を削除した仮定の状況の依存関係図

データアクセスインターフェースの分析(Data access interface analysis)

ドメインモデルはデータアクセス層に依存しています。なぜなら、データモデル全体がここで定義されているからです。Entity Frameworkを使用してデータアクセス層を実装するという決定は合理的かもしれません。しかし、**疎結合(Loose Coupling)**の観点からは、それを直接ドメインモデルで使用するのではありません。

問題のあるコードはProductServiceクラスに見つかります。コンストラクタはCommerceContextクラスの新しいインスタンスを作成し、それをプライベートメンバ変数に割り当てます:

this.dbContext = new CommerceContext();

これはProductServiceクラスをデータアクセス**層に緊密に結合(tightly coupled)**させます。このコードをインターセプトして他のコードで置き換える合理的な方法はありません。データアクセス層への参照がProductServiceクラスにハードコードされているのです!

GetFeaturedProductsメソッドの実装はCommerceContextを使用してデータベースから商品オブジェクトを抽出します:

var featuredProducts =
    from product in this.dbContext.Products
    where product.IsFeatured
    select product;

GetFeaturedProductsでのCommerceContextへの参照はハードコードされた依存関係を強化しますが、この時点で、すでに損害が発生しています。私たちが必要とするのは、この**緊密結合(tightly coupled)**なしでモジュールを組み合わせるためのより良い方法です。第一章で議論したDIの利点を思い出すと、メアリーのアプリケーションは次の機能を持っていません:

  • 遅延バインディング(Late binding)—領域層がデータアクセス層に緊密に結合されているため、同じアプリケーションの2つのバージョンをデプロイすることはできません。一方はローカルSQL Serverデータベースに接続し、もう一方はAzure Table Storageでホストされます。言い換えれば、正しいデータアクセス層を遅延バインディング(Late binding)でロードすることは不可能です。
  • 拡張性(Extensibility)—アプリケーションのすべてのクラスが互いに緊密に結合されているため、セキュリティ機能(第1章の横断的関心事など)を挿入するのは非常に高価になります。これを行うには、システム内の多くのクラスを変更する必要があります。したがって、この緊密結合(tightly coupled)された設計は特に拡張性がありません。
  • 並行開発(Maintainability)—前のアプリケーションで横断的関心事を使用した例を続けると、コードベース全体で徹底的な変更が必要になるため、複数の開発者が単一のアプリケーションで並行して作業する能力が妨げられます。私たちと同様に、過去にバージョン管理システムに作業をコミットしたとき、痛みの多いマージ競合を処理したことがあるかもしれません。設計が良好で疎結合(Loose Coupling)されたシステムは、マージ競合の数を減らします。メアリーのアプリケーションで開発を開始する開発者が増えるにつれて、互いに干渉せずに効果的に作業するのはますます難しくなります。
  • テスト性(Parallel development)—データアクセス層を交換できないことがわかっています。しかし、データベースなしでコードをテストすることは、単体テスト(Unit testing)の前提条件です。しかし、統合テストを使用しても、メアリーはコードの一部を置き換える必要があるかもしれません。現在の設計はそれを困難にします。したがって、メアリーの申請はテストできません。

この時点で、必要な依存関係図がどのようになるべきか自問するかもしれません。最高の再利用性のために、最少の依存関係が必要です。他方、依存関係が全くない場合、アプリケーションは無意味になります。

必要な依存関係とそれらが指す方向は要件に依存します。しかし、私たちはドメイン層を完全に置き換えるつもりがないと確信しているので、他の層が安全にそれに依存できると仮定できます。図2.10は、**疎結合(Loose Coupling)**されたアプリケーションに大きなヒントを提供します。次章でそれを書くでしょうが、必要な依存関係図を示しています。

図2.10 必要な状況の依存関係図

翻訳

The data access layer now depends on the domain layer instead of the other way around.

データアクセス層は現在、領域層に依存しており、その逆ではありません。

The UI layer only depends on the domain layer.

UI層は領域層にのみ依存しています。

この図は、領域層とデータアクセス層の間の依存関係をどのように反転するかを示しています。次章で詳細に説明します。

その他の雑多な問題(Miscellaneous other issues)

メアリーのコードで解決すべきいくつかのその他の問題を指摘したいと思います。

  • ほとんどのドメインモデルはデータアクセス層で実装されているようです(Most of the domain model seems to be implemented in the data access layer.)。領域層がデータアクセス層を参照するのは技術的な問題ですが、商品クラスとしてデータアクセス層がクラスを定義するのは概念的な問題です。公開商品クラスはドメインモデルに属します。
  • Jensのアドバイスに基づいて、メアリーはユーザーが優先顧客かどうかを判断するコードをUIで実装することに決めました。しかし、顧客を優先顧客として識別する方法はビジネスロジックの一部であるため、ドメインモデルで実装するべきです(On Jens'advice, Mary decided to implement in the UI the code that determines whether a user is a preferred customer.)。Jensの関心事の分離と単一責任の原則に関する議論は、コードを間違った場所に置くための言い訳ではありません。単一ライブラリ内で単一責任の原則に従うことは完全に可能です—それは期待される方法です。
  • メアリーは、CommerceContextクラスから設定ファイルから接続文字列を読み込みました(リスト2.2に示すように)(Mary loaded the connection string from the configuration file from within the CommerceContext class (shown in listing 2.2).)。消費者の観点から、この設定値への依存は完全に隠されています。リスト2.2を議論したときに言及したように、この含蓄には罠が含まれています。

​ 設定がコンパイルされたアプリケーションの能力を有効にすることが重要ですが、完成したアプリケーションのみが設定ファイルに依存するべきです。再利用可能なライブラリの場合、呼び出し元が設定を強制する方が柔軟であり、自分で設定ファイルを読み込むよりも優れています。最終的に、最終呼び出し元はアプリケーションのエントリポイントです。この時点で、すべての関連する設定データは、必要に応じて下位ライブラリに直接提供できるように、起動時に設定ファイルから直接読み込むことができます。私たちはCommerceContextが要求する設定が明示的であることを望みます。

  • ビュー(リスト2.5に示すように)は、多すぎる機能を含んでいるようです(The view (as shown in listing 2.5) seems to contain too much functionality.)。それはキャストと特定の文字列フォーマットを実行します。このような機能は下位モデルに移動するべきです。

タグ: .NET C# Entity Framework Core ASP.NET Core MVC 依存性注入

5月28日 12:39 投稿