繰り返し #4 – アプリケーションを疎結合にする (C#)

提供元: Microsoft

コードのダウンロード

この 4 番目のイテレーションでは、いくつかのソフトウェア デザイン パターンを利用して、連絡先マネージャー アプリケーションを簡単に維持し、変更できるようにします。 たとえば、Repository パターンと Dependency Injection パターンを使用するようにアプリケーションをリファクターします。

連絡先管理の ASP.NET MVC アプリケーションをビルドする (C#)

この一連のチュートリアルでは、連絡先管理アプリケーション全体を最初から最後までビルドします。 連絡先マネージャー アプリケーションを使用すると、人物の一覧に連絡先情報 (名前、電話番号、電子メール アドレス) を保存できます。

複数のイテレーションを通してアプリケーションをビルドします。 イテレーションのたびに、アプリケーションを徐々に改善します。 複数のイテレーションからなるこのアプローチの目的は、お客様が各変更の理由を理解できるようにすることです。

  • イテレーション #1 - アプリケーションを作成します。 最初のイテレーションでは、可能な限り簡単な方法で連絡先マネージャーを作成します。 基本的なデータベース操作 (生成、読み取り、更新、削除 (CRUD)) のサポートを追加します。

  • イテレーション #2 - アプリケーションの外観を良くします。 このイテレーションでは、既定の ASP.NET MVC ビュー マスター ページとカスケード スタイル シートを変更することで、アプリケーションの外観を向上させます。

  • イテレーション #3 - フォームの検証を追加します。 3 番目のイテレーションでは、基本的なフォームの検証を追加します。 必須のフォーム フィールドを入力しないとユーザーがフォームを送信できないようにします。 また、メール アドレスと電話番号も検証します。

  • イテレーション #4 - アプリケーションを疎結合します。 この 4 番目のイテレーションでは、いくつかのソフトウェア デザイン パターンを利用して、連絡先マネージャー アプリケーションを簡単に維持し、変更できるようにします。 たとえば、Repository パターンと Dependency Injection パターンを使用するようにアプリケーションをリファクターします。

  • イテレーション #5 - 単体テストを作成します。 5 番目のイテレーションでは、単体テストを追加することで、アプリケーションを簡単に維持し、変更できるようにします。 データ モデル クラスをモックし、コントローラーと検証ロジックの単体テストをビルドします。

  • イテレーション #6 - テスト駆動開発を使用します。 この 6 番目のイテレーションでは、最初に単体テストを記述し、この単体テストに対してコードを記述することにより、新しい機能をアプリケーションに追加します。 このイテレーションでは、連絡先グループを追加します。

  • イテレーション #7 - Ajax 機能を追加します。 7 番目のイテレーションでは、Ajax のサポートを追加することで、アプリケーションの応答性とパフォーマンスを向上させます。

このイテレーション

連絡先マネージャー アプリケーションのこの 4 回目のイテレーションでは、アプリケーションをより疎結合にするためにアプリケーションをリファクターします。 アプリケーションが疎結合されていると、アプリケーションの 1 つの部分でコードを変更でき、アプリケーションの他の部分のコードを変更する必要がありません。 疎結合アプリケーションは、変更に対する回復性が高くなります。

現在、連絡先マネージャー アプリケーションで使用されているすべてのデータ アクセス ロジックと検証ロジックは、コントローラー クラスに含まれています。 これは適切ではありません。 アプリケーションの 1 つの部分を変更する必要があるときは、常にアプリケーションの別の部分にバグが発生するリスクがあります。 たとえば、検証ロジックを変更すると、データ アクセス ロジックまたはコントローラー ロジックに新しいバグが発生するリスクがあります。

Note

(SRP) に基づき、クラスに変更する理由が複数あってはなりません。 コントローラー ロジック、検証ロジック、データベース ロジックの混在は、単一責任の原則に対する大規模な違反です。

アプリケーションを変更する必要がある理由はいくつかあります。 新しい機能をアプリケーションに追加する必要がある場合や、アプリケーションのバグを修正する必要がある場合があります。アプリケーションの機能の実装方法を変更する必要がある場合もあります。 アプリケーションが静的になることはほとんどありません。 時間の経過とともに成長し、変異する傾向があります。

たとえば、データ アクセス層の実装方法を変更するとします。 現時点では、連絡先マネージャー アプリケーションは Microsoft Entity Framework を使用してデータベースにアクセスします。 ただし、ADO.NET Data Services や NHibernate などの新しいデータ アクセス テクノロジまたは代替データ アクセス テクノロジに移行することにするかもしれません。 ただし、データ アクセス コードが検証コードとコントローラー コードから分離されていないため、データ アクセスに直接関連しない他のコードを変更せずに、アプリケーションのデータ アクセス コードを変更することはできません。

一方、アプリケーションが疎結合されていると、アプリケーションの他の部分に触れることなく、アプリケーションの 1 つの部分に変更を加えることができます。 たとえば、検証ロジックまたはコントローラー ロジックを変更せずに、データ アクセス テクノロジを切り替えることができます。

このイテレーションでは、いくつかのソフトウェア設計パターンを利用して、連絡先マネージャー アプリケーションをさらに疎結合されたアプリケーションにリファクターできます。 完了したら、連絡先マネージャーは以前に行わなかったことを行いません。 ただし、今後、アプリケーションをより簡単に変更できるようになります。

Note

リファクタリングは、既存の機能を失わない方法でアプリケーションを書き換えるプロセスです。

リポジトリ ソフトウェア設計パターンを使用する

最初の変更は、リポジトリ パターンと呼ばれるソフトウェア設計パターンを利用することです。 リポジトリ パターンを使用して、アプリケーションの残りの部分からデータ アクセス コードを分離します。

リポジトリ パターンを実装するには、次の 2 つの手順を完了する必要があります。

  1. インターフェイスの作成
  2. インターフェイスを実装するクラスを作成する

まず、実行する必要があるすべてのデータ アクセス方法を記述するインターフェイスを作成する必要があります。 IContactManagerRepository インターフェイスは、リスト 1 に含まれています。 このインターフェイスでは、CreateContact()、DeleteContact()、EditContact()、GetContact、ListContacts() の 5 つのメソッドが記述されます。

リスト 1 - Models\IContactManagerRepository.cs

using System;
using System.Collections.Generic;

namespace ContactManager.Models
{
    public interface IContactRepository
    {
        Contact CreateContact(Contact contactToCreate);
        void DeleteContact(Contact contactToDelete);
        Contact EditContact(Contact contactToUpdate);
        Contact GetContact(int id);
        IEnumerable<Contact> ListContacts();

    }
}

次に、IContactManagerRepository インターフェイスを実装する具象クラスを作成する必要があります。 Microsoft Entity Framework を使用してデータベースにアクセスするため、EntityContactManagerRepository という名前の新しいクラスを作成します。 このクラスは、リスト 2 に含まれています。

リスト 2 - Models\EntityContactManagerRepository.cs

using System.Collections.Generic;
using System.Linq;

namespace ContactManager.Models
{
    public class EntityContactManagerRepository : ContactManager.Models.IContactManagerRepository
    {
        private ContactManagerDBEntities _entities = new ContactManagerDBEntities();

        public Contact GetContact(int id)
        {
            return (from c in _entities.ContactSet
                    where c.Id == id
                    select c).FirstOrDefault();
        }

        public IEnumerable ListContacts()
        {
            return _entities.ContactSet.ToList();
        }

        public Contact CreateContact(Contact contactToCreate)
        {
            _entities.AddToContactSet(contactToCreate);
            _entities.SaveChanges();
            return contactToCreate;
        }

        public Contact EditContact(Contact contactToEdit)
        {
            var originalContact = GetContact(contactToEdit.Id);
            _entities.ApplyPropertyChanges(originalContact.EntityKey.EntitySetName, contactToEdit);
            _entities.SaveChanges();
            return contactToEdit;
        }

        public void DeleteContact(Contact contactToDelete)
        {
            var originalContact = GetContact(contactToDelete.Id);
            _entities.DeleteObject(originalContact);
            _entities.SaveChanges();
        }

    }
}

EntityContactManagerRepository クラスが IContactManagerRepository インターフェイスを実装していることにご注意ください。 このクラスは、そのインターフェイスで記述されている 5 つのメソッドをすべて実装します。

なぜインターフェイスを気にする必要があるのか疑問に思うかもしれません。 インターフェイスとそれを実装するクラスの両方を作成する必要があるのはなぜでしょうか?

1 つの例外を除き、アプリケーションの残りの部分は、具象クラスではなくインターフェイスと対話します。 EntityContactManagerRepository クラスによって公開されるメソッドを呼び出す代わりに、IContactManagerRepository インターフェイスによって公開されるメソッドを呼び出します。

そうすることで、アプリケーションの残りの部分を変更しなくても、新しいクラスとのインターフェイスを実装できます。 たとえば、今後、IContactManagerRepository インターフェイスを実装する DataServicesContactManagerRepository クラスを実装する必要があるかもしれません。 DataServicesContactManagerRepository クラスは、ADO.NET Data Services を使用して、Microsoft Entity Framework の代わりにデータベースにアクセスできます。

アプリケーション コードが具象 EntityContactManagerRepository クラスではなく IContactManagerRepository インターフェイスに対してプログラムされている場合、コードの残りの部分を変更せずに具象クラスを切り替えることができます。 たとえば、データ アクセス ロジックや検証ロジックを変更することなく、EntityContactManagerRepository クラスから DataServicesContactManagerRepository クラスに切り替えることができます。

具象クラスではなくインターフェイス (抽象化) に対するプログラミングにより、アプリケーションの変更に対する回復性が高まります。

Note

具象クラスからインターフェイスをすばやく作成できます。Visual Studio でメニュー オプション [リファクター]、[インターフェイスの抽出] の順に選びます。 たとえば、最初に EntityContactManagerRepository クラスを作成してから、Extract Interface を使用して IContactManagerRepository インターフェイスを自動的に生成できます。

依存関係の挿入ソフトウェア設計パターンを使用する

データ アクセス コードを別の Repository クラスに移行したので、このクラスを使用するように Contact コントローラーを変更する必要があります。 依存関係の挿入と呼ばれるソフトウェア設計パターンを利用して、コントローラーで Repository クラスを使用します。

変更された Contact コントローラー クラスはリスト 3 の中に含まれています。

リスト 3 - Controllers\ContactController.cs

using System.Text.RegularExpressions;
using System.Web.Mvc;
using ContactManager.Models;

namespace ContactManager.Controllers
{
    public class ContactController : Controller
    {
        private IContactManagerRepository _repository;

        public ContactController()
            : this(new EntityContactManagerRepository())
        {}

        public ContactController(IContactManagerRepository repository)
        {
            _repository = repository;
        }

        protected void ValidateContact(Contact contactToValidate)
        {
            if (contactToValidate.FirstName.Trim().Length == 0)
                ModelState.AddModelError("FirstName", "First name is required.");
            if (contactToValidate.LastName.Trim().Length == 0)
                ModelState.AddModelError("LastName", "Last name is required.");
            if (contactToValidate.Phone.Length > 0 && !Regex.IsMatch(contactToValidate.Phone, @"((\(\d{3}\) ?)|(\d{3}-))?\d{3}-\d{4}"))
                ModelState.AddModelError("Phone", "Invalid phone number.");
            if (contactToValidate.Email.Length > 0 && !Regex.IsMatch(contactToValidate.Email, @"^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$"))
                ModelState.AddModelError("Email", "Invalid email address.");
        }

        public ActionResult Index()
        {
            return View(_repository.ListContacts());
        }

        public ActionResult Create()
        {
            return View();
        } 

        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Create([Bind(Exclude = "Id")] Contact contactToCreate)
        {
            // Validation logic
            ValidateContact(contactToCreate);
            if (!ModelState.IsValid)
                return View();

            // Database logic
            try
            {
                _repository.CreateContact(contactToCreate);
                return RedirectToAction("Index");
            }
            catch
            {
                return View();
            }
        }

        public ActionResult Edit(int id)
        {
            return View(_repository.GetContact(id));
        }

        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Edit(Contact contactToEdit)
        {
            // Validation logic
            ValidateContact(contactToEdit);
            if (!ModelState.IsValid)
                return View();

            // Database logic
            try
            {
                _repository.EditContact(contactToEdit);
                return RedirectToAction("Index");
            }
            catch
            {
                return View();
            }
        }

        public ActionResult Delete(int id)
        {
            return View(_repository.GetContact(id));
        }

        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Delete(Contact contactToDelete)
        {
            try
            {
                _repository.DeleteContact(contactToDelete);
                return RedirectToAction("Index");
            }
            catch
            {
                return View();
            }
        }

    }
}

リスト 3 の Contact コントローラーに 2 つのコンストラクターがあることにご注目ください。 最初のコンストラクターは、IContactManagerRepository インターフェイスの具象インスタンスを 2 番目のコンストラクターに渡します。 Contact コントローラー クラスは、コンストラクターの依存関係の挿入を使用します。

EntityContactManagerRepository クラスが使用される唯一の場所は、最初のコンストラクターにあります。 クラスの残りの部分は、具象 EntityContactManagerRepository クラスの代わりに IContactManagerRepository インターフェイスを使用します。

これにより、将来的に IContactManagerRepository クラスの実装を簡単に切り替えることができます。 EntityContactManagerRepository クラスの代わりに DataServicesContactRepository クラスを使用する場合は、最初のコンストラクターを変更するだけです。

コンストラクターの依存関係の挿入により、Contact コントローラー クラスも簡単にテストできるようになります。 単体テストで、IContactManagerRepository クラスのモック実装を渡すことで、Contact コントローラーをインスタンス化できます。 この依存関係の挿入機能は、次のイテレーションで連絡先マネージャー アプリケーションの単体テストをビルドするときに非常に重要になります。

Note

IContactManagerRepository インターフェイスの特定の実装から Contact コントローラー クラスを完全に分離する場合は、StructureMap や Microsoft Entity Framework (MEF) などの依存関係の挿入をサポートするフレームワークを利用できます。 依存関係の挿入フレームワークを利用すると、コード内で具象クラスを参照する必要がありません。

サービス レイヤーの作成

検証ロジックが、リスト 3 の変更されたコントローラー クラスのコントローラー ロジックとまだ混在していることにお気付きかもしれません。 データ アクセス ロジックを分離することをお勧めするのと同じ理由から、検証ロジックを分離することをお勧めします。

この問題を解決するには、別のサービス レイヤーを作成します。 サービス レイヤーは、コントローラー クラスとリポジトリ クラスの間に挿入できる別のレイヤーです。 サービス レイヤーには、すべての検証ロジックを含むビジネス ロジックが含まれています。

ContactManagerService はリスト 4 に含まれています。 これには、Contact コントローラー クラスの検証ロジックが含まれています。

リスト 4 - Models\ContactManagerService.cs

using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Web.Mvc;
using ContactManager.Models.Validation;

namespace ContactManager.Models
{
    public class ContactManagerService : IContactManagerService
    {
        private IValidationDictionary _validationDictionary;
        private IContactManagerRepository _repository;

        public ContactManagerService(IValidationDictionary validationDictionary) 
            : this(validationDictionary, new EntityContactManagerRepository())
        {}

        public ContactManagerService(IValidationDictionary validationDictionary, IContactManagerRepository repository)
        {
            _validationDictionary = validationDictionary;
            _repository = repository;
        }

        public bool ValidateContact(Contact contactToValidate)
        {
            if (contactToValidate.FirstName.Trim().Length == 0)
                _validationDictionary.AddError("FirstName", "First name is required.");
            if (contactToValidate.LastName.Trim().Length == 0)
                _validationDictionary.AddError("LastName", "Last name is required.");
            if (contactToValidate.Phone.Length > 0 && !Regex.IsMatch(contactToValidate.Phone, @"((\(\d{3}\) ?)|(\d{3}-))?\d{3}-\d{4}"))
                _validationDictionary.AddError("Phone", "Invalid phone number.");
            if (contactToValidate.Email.Length > 0 && !Regex.IsMatch(contactToValidate.Email, @"^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$"))
                _validationDictionary.AddError("Email", "Invalid email address.");
            return _validationDictionary.IsValid;
        }

        #region IContactManagerService Members

        public bool CreateContact(Contact contactToCreate)
        {
            // Validation logic
            if (!ValidateContact(contactToCreate))
                return false;

            // Database logic
            try
            {
                _repository.CreateContact(contactToCreate);
            }
            catch
            {
                return false;
            }
            return true;
        }

        public bool EditContact(Contact contactToEdit)
        {
            // Validation logic
            if (!ValidateContact(contactToEdit))
                return false;

            // Database logic
            try
            {
                _repository.EditContact(contactToEdit);
            }
            catch
            {
                return false;
            }
            return true;
        }

        public bool DeleteContact(Contact contactToDelete)
        {
            try
            {
                _repository.DeleteContact(contactToDelete);
            }
            catch
            {
                return false;
            }
            return true;
        }

        public Contact GetContact(int id)
        {
            return _repository.GetContact(id);
        }

        public IEnumerable<Contact> ListContacts()
        {
            return _repository.ListContacts();
        }

        #endregion
    }
}

ContactManagerService のコンストラクターには ValidationDictionary が必要であることにご注意ください。 サービス レイヤーは、この ValidationDictionary を介してコントローラー レイヤーと通信します。 ValidationDictionary については、次のセクションでデコレータ パターンと一緒に詳しく説明します。

さらに、ContactManagerService が IContactManagerService インターフェイスを実装していることにご注意ください。 具象クラスではなく、インターフェイスに対するプログラミングを行うよう常に努める必要があります。 連絡先マネージャー アプリケーションの他のクラスは、ContactManagerService クラスと直接対話しません。 代わりに、1 つの例外を除き、連絡先マネージャー アプリケーションの残りの部分は IContactManagerService インターフェイスに対してプログラムされます。

IContactManagerService インターフェイスは、リスト 5 に含まれています。

リスト 5 - Models\IContactManagerService.cs

using System.Collections.Generic;

namespace ContactManager.Models
{
    public interface IContactManagerService
    {
        bool CreateContact(Contact contactToCreate);
        bool DeleteContact(Contact contactToDelete);
        bool EditContact(Contact contactToEdit);
        Contact GetContact(int id);
        IEnumerable ListContacts();
    }
}

変更された Contact コントローラー クラスはリスト 6 の中に含まれています。 Contact コントローラーが ContactManager リポジトリと対話しなくなったことにご注意ください。 代わりに、Contact コントローラーは ContactManager サービスと対話します。 各レイヤーは、他のレイヤーから可能な限り分離されます。

リスト 6 - Controllers\ContactController.cs

using System.Web.Mvc;
using ContactManager.Models;

namespace ContactManager.Controllers
{
    public class ContactController : Controller
    {
        private IContactManagerService _service;

        public ContactController()
        {
            _service = new ContactManagerService(new ModelStateWrapper(this.ModelState));

        }

        public ContactController(IContactManagerService service)
        {
            _service = service;
        }
        
        public ActionResult Index()
        {
            return View(_service.ListContacts());
        }

        public ActionResult Create()
        {
            return View();
        }

        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Create([Bind(Exclude = "Id")] Contact contactToCreate)
        {
            if (_service.CreateContact(contactToCreate))
                return RedirectToAction("Index");
            return View();
        }

        public ActionResult Edit(int id)
        {
            return View(_service.GetContact(id));
        }

        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Edit(Contact contactToEdit)
        {
            if (_service.EditContact(contactToEdit))
                return RedirectToAction("Index");
            return View();
        }

        public ActionResult Delete(int id)
        {
            return View(_service.GetContact(id));
        }

        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Delete(Contact contactToDelete)
        {
            if (_service.DeleteContact(contactToDelete))
                return RedirectToAction("Index");
            return View();
        }

    }
}

このアプリケーションは、単一責任の原則 (SRP) の違反を実行しなくなりました。 リスト 6 の Contact コントローラーは、アプリケーション実行のフローを制御する以外のすべての責任を取り除かれました。 すべての検証ロジックが Contact コントローラーから削除され、サービス レイヤーにプッシュされました。 すべてのデータベース ロジックがリポジトリ レイヤーにプッシュされました。

デコレータ パターンを使用する

サービス レイヤーをコントローラー レイヤーから完全に分離できるようにします。 原則として、MVC アプリケーションへの参照を追加しなくても、コントローラー レイヤーとは別のアセンブリでサービス レイヤーをコンパイルできる必要があります。

ただし、サービス レイヤーが検証エラー メッセージをコントローラー レイヤーに渡すことができるようにする必要があります。 コントローラーとサービス レイヤーを結合せずに、サービス層が検証エラー メッセージを通信できるようにするにはどうすればよいでしょうか? デコレータ パターンというソフトウェア デザイン パターンを利用できます。

コントローラーは、ModelState という ModelStateDictionary を使用して検証エラーを表します。 そのため、ModelState をコントローラー レイヤーからサービス レイヤーに渡そうとお考えになるかもしれません。 ただし、サービス レイヤーで ModelState を使用すると、サービス レイヤーは ASP.NET MVC フレームワークの機能に依存することになります。 場合によっては、ASP.NET MVC アプリケーションの代わりに WPF アプリケーションでサービス レイヤーを使用する場合があるため、これは適切ではありません。 その場合、ModelStateDictionary クラスを使用するために、ASP.NET MVC フレームワークを参照する必要はありません。

デコレータ パターンを使用すると、インターフェイスを実装するために、既存のクラスを新しいクラスでラップできます。 連絡先マネージャー プロジェクトには、リスト 7 に含まれる ModelStateWrapper クラスが含まれています。 ModelStateWrapper クラスは、リスト 8 のインターフェイスを実装します。

リスト 7 - Models\Validation\ModelStateWrapper.cs

using System.Web.Mvc;

namespace ContactManager.Models.Validation
{
    public class ModelStateWrapper : IValidationDictionary
    {
        private ModelStateDictionary _modelState;

        public ModelStateWrapper(ModelStateDictionary modelState)
        {
            _modelState = modelState;
        }

        public void AddError(string key, string errorMessage)
        {
            _modelState.AddModelError(key, errorMessage);
        }

        public bool IsValid
        {
            get { return _modelState.IsValid; }
        }
    }
}

リスト 8 - Models\Validation\IValidationDictionary.cs

namespace ContactManager.Models.Validation
{
    public interface IValidationDictionary
    {
        void AddError(string key, string errorMessage);
        bool IsValid {get;}
    }
}

リスト 5 を詳しく見ると、ContactManager サービス レイヤーが IValidationDictionary インターフェイスのみを使用していることがわかります。 ContactManager サービスは ModelStateDictionary クラスに依存していません。 Contact コントローラーが ContactManager サービスを作成すると、コントローラーはその ModelState を次のようにラップします。

_service = new ContactManagerService(new ModelStateWrapper(this.ModelState));

まとめ

このイテレーションでは、連絡先マネージャー アプリケーションに新しい機能を追加しませんでした。 このイテレーションの目的は、連絡先マネージャー アプリケーションをリファクターすることで、維持と変更を容易にすることでした。

最初に、リポジトリ ソフトウェアの設計パターンを実装しました。 すべてのデータ アクセス コードを別の ContactManager リポジトリ クラスに移行しました。

また、検証ロジックをコントローラー ロジックから分離しました。 すべての検証コードを含む別のサービス レイヤーを作成しました。 コントローラー レイヤーはサービス レイヤーと対話し、サービス レイヤーはリポジトリ レイヤーと対話します。

サービス レイヤーを作成したときに、デコレータ パターンを利用して ModelState をサービス レイヤーから分離しました。 サービス レイヤーでは、ModelState の代わりに IValidationDictionary インターフェイスに対してプログラミングしました。

最後に、依存関係の挿入パターンというソフトウェア設計パターンを利用しました。 このパターンにより、具象クラスではなくインターフェイス (抽象化) に対してプログラムを実行できます。 依存関係の挿入の設計パターンを実装すると、コードのテストも容易になります。 次のイテレーションでは、単体テストをプロジェクトに追加します。