単体テストを実装できるように、会社の既存のコードの一部を更新することを検討しています。これを実行できるようにするために、DIを許可するためにすべてのリポジトリーがインターフェースされています。ただし、既存のコードでは、データベース内のテーブルごとに1つのリポジトリクラスを作成する傾向があります。つまり、ビジネスロジックの1つのセクションで5つの異なるリポジトリクラスをクエリする必要がある場合があります。
DIを使用して5つ以上のインターフェースを渡すのは面倒なようで、これを再設計する方法についてのヘルプを探しています。
これはすべてMVC 5 C#サイト用であり、その例を以下に示します(コードサイズを小さくして読みやすくしました)。
アカウントのコントローラーがあり、このシナリオではユーザーセッションを管理します。
public class AccountController : Controller
{
private AccountLogic _accountLogic { get; set; }
public AccountController(
ISessionRepository sessionRepository,
ILoginRepository loginRepository)
{
_accountLogic = new AccountLogic(sessionRepository, loginRepository);
}
public IActionResult Index()
{
return View();
}
public IActionResult Login()
{
return View();
}
public IActionResult Login(LoginModel loginModel)
{
if (ModelState.IsValid)
{
if (_accountLogic.DoesUserExist(loginModel.Username))
{
var existingHash= _accountLogic.GetPassword(loginModel.Username);
if (Hash.VerifyPassword(loginModel.Password, existingHash))
{
// Set Auth Cookies
return RedirectToAction("Index", "Home");
}
else
{
ModelState.AddModelError(
"Username",
"Sorry, we did not recognize " +
"either your username or password");
}
}
}
return View(loginModel);
}
}
チームは、ビジネスクラスにほとんどのアクションを実行させることにより、コントローラーのサイズを削減しようとします。これが実際に意味することは、ビジネスクラスは非常に大きく、クエリする必要があるすべてのリポジトリのインターフェイスを渡す必要があるため、多くの責任があるということです。示されている例では、SessionリポジトリとLoginリポジトリの両方を渡します。これらはそれぞれ、データベース内の対応するテーブルに対して単独で責任があります。
public class AccountLogic
{
private ISessionRepository _sessionRepository { get; set; }
private ILoginRepository _loginRepository { get; set; }
public AccountLogic(
ISessionRepository sessionRepository,
ILoginRepository loginRepository)
{
_sessionRepository = sessionRepository;
_loginRepository = loginRepository;
}
public Guid CreateSession(int loginID, string userAgent, string locale)
{
return _sessionRepository.CreateSession(loginID, userAgent, locale);
}
public Session GetSession(Guid sessionKey)
{
return _sessionRepository.GetSession(sessionKey);
}
public void EndSession(Guid sessionKey)
{
_sessionRepository.EndSession(sessionKey);
}
public bool DoesUserExist(string username)
{
return _loginRepository.DoesUserExist(username);
}
public string GetPassword(string username)
{
return _loginRepository.GetPassword(username);
}
}
次に、すべてのビジネスロジックを処理するAccountLogicクラスがあります。与えられた例では、実行する複雑なロジックがないため、純粋にインターフェースメソッドへのチェーン呼び出しになります。
私の質問は、これをより簡単に処理できるようにするにはどうすれば、大規模でもテストできるようになるのでしょうか。 10個の異なるリポジトリをクエリする必要のあるコントローラーがある場合、コントローラーに15個のインターフェイスを渡し、次に関連するビジネスクラスに渡すことは確かに管理できませんか?テーブルごとに1つのクラスが存在しないように、リポジトリの処理方法を変更する必要がありますか?
完全にC#のスタイルではありませんが、ここでは free monad API が役立ちます。これが C#の例 です。
無料のモナドAPIでは、インターフェースはオブジェクトとしてモデル化されます。
// Static DSL for composing DbOp
public static class DB {
public static class Session {
public static DbOp<Guid> Create(int loginID, string userAgent, string locale) => new DbOp<Guid>.Session.Create(loginID, userAgent, locale, Return);
}
public static class Login {
public static DbOp<Boolean> DoesUserExist(string username) => new DbOp<Boolean>.Login.DoesUserExist(username, Return);
}
public static DbOp<A> Return<A>(A value) => new DbOp<A>.Return(value);
}
// The classes that are created by the DSL. Holds the data of each operation
public abstract class DbOp<A> {
public static class Session {
public class Create : DbOp<A> {
public readonly int LoginID;
public readonly string UserAgent;
public readonly string Locale;
public readonly Func<Unit, DbOp<A>> Next;
public Create(int loginID, string userAgent, string locale, Func<Unit, DbOp<A>> next) =>
(LoginID, UserAgent, Locale, Next) = (loginID, userAgent, locale, next);
}
}
public static class Login {
public class DoesUserExist {
public readonly string Username;
public readonly Func<Unit, DbOp<A>> Next;
public DoesUserExist(string username, Func<Unit, DbOp<A>> next) =>
(Username, Next) = (username, next);
}
}
public class Return : DbOp<A> {
public readonly A Value;
public Return(A value) => Value = value;
}
}
これにより、インターフェイスの実装を完全に分離しながら、データインターフェイス全体を単一のファイルで均一に表すことができます。インターフェースオブジェクトを無料のモナドで構成することにより、プログラムを記述します。
public class BusinessLogic {
private readonly IDbInterpreter dbInterpreter;
public BusinessLogic(IDbInterpreter dbInterpreter) {
this.dbInterpreter = dbInterpreter;
}
public Guid login(username) = {
// Compose our program of DbOp
DbOp<Guid> program =
from userExists in DB.Login.DoesUserExist(username)
from guid in DB.Session.Create(...)
select guid;
// Interpret the program to get the result
dbInterpreter.Interpret(program);
}
}
インタープリターは大きなswitchステートメント(疑似コード)のように見えます。
interface IDbInterpreter {
A Interpret<A>(DbOp<A> ma);
}
public class DbInterpreter : IDbInterpreter {
private readonly LoginRepository loginRepo;
private readonly SessionRepository sessionRepo;
DbInterpreter(LoginRepository loginRepo,
SessionRepository sessionRepo) {
this.loginRepo = loginRepo;
this.sessionRepo = sessionRepo;
}
A IDbInterpreter.Interpret<A>(DbOp<A> ma) =>
ma is DbOp<A>.Return r ? r.Value
: ma is DbOp<A>.Session.Create c ? Interpret(c.Next(sessionRepo.Create(c.LoginID, c.UserAgent, c.Locale)))
: ma is DbOp<A>.Login.DoesUserExist d ? Interpret(d.Next(loginRepo.DoesUserExist(d.Username)))
: throw new NotSupportedException();
}
このパターンを使用する利点の1つは、システム内でDBテーブルを個別に分離できることですが、それらすべてを1つのDSLとして解釈できます。つまり、テストでは、予想されるすべてのDB呼び出しを処理する単一のモックインタープリターを提供できます(予期しない呼び出しが行われた場合は例外をスローします)(疑似コード)。
class MockInterpreter : IDbInterpreter {
A IDbInterpreter.Interpret(DbOp<A> ma) =>
ma is DbOp<A>.Return r ? r.Value
: ma is DbOp<A>.Session.Create c ? throw new Exception("") // simulate exception being thrown on session create
// we only need to implement the operations we expect to be called in the mock interpreter
: throw new NotSupportedException();
}
BusinessLogic businessLogic = new BusinessLogic(mockInterpreter);
assert businessLogic.login(username) ...
多数のインターフェースモックを提供する代わりに。
具体的なクラス「AccountLogic」を「AccountController」に挿入してみましたか? IOCコンテナは、追加の構成なしで具象クラスを構築できるはずです。
public class AccountController : Controller
{
private readonly AccountLogic _accountLogic;
public AccountController(AccountLogic accountLogic)
{
_accountLogic = accountLogic;
}
...
}
空のコンストラクターを追加し、メソッドを「仮想」に変更することで、テスト用の具象クラスを模擬できます。
public class AccountLogic
{
private readonly ISessionRepository _sessionRepository;
private readonly ILoginRepository _loginRepository;
public AccountLogic() { }
public AccountLogic(
ISessionRepository sessionRepository,
ILoginRepository loginRepository)
{
_sessionRepository = sessionRepository;
_loginRepository = loginRepository;
}
public virtual Guid CreateSession(int loginID, string userAgent, string locale)
{
return _sessionRepository.CreateSession(loginID, userAgent, locale);
}
...
}
+1を使用して、コンストラクターパラメーターを水平方向ではなく垂直方向に配置します。
また、
private readonly ISessionRepository _sessionRepository;
の代わりに
private ISessionRepository _sessionRepository { get; set; }
注入されたオブジェクト用。