Come gestire troppe interfacce

6

Sto cercando di aggiornare alcune delle mie società con codice esistente per consentire l'implementazione dei test unitari. Per essere in grado di farlo, tutti i repository sono interfacciati per consentire DI. Tuttavia, il codice esistente ha l'abitudine di creare 1 classe repository per tabella nel database, il che significa che una sezione della logica aziendale potrebbe dover interrogare 5 diverse classi di repository.

Passare in 5+ interfacce usando DI sembra complicato e sto cercando aiuto su come ridisegnarlo.

Questo è tutto per un sito MVC 5 C #, e un esempio è riportato di seguito (ho ridotto le dimensioni del codice in modo che sia più facile da leggere).

Abbiamo il nostro controller per Account, che in questo scenario gestisce le sessioni utente:

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);
    }
}

Il team cerca di ridurre le dimensioni del controller facendo in modo che una classe business esegua la maggior parte delle azioni. Ciò che questo significa in realtà è che la business class è molto grande e ha molte responsabilità in quanto deve essere passata un'interfaccia per tutti i repository che deve interrogare. Nell'esempio mostrato, passiamo sia nei repository Session che Login, ognuno dei quali è l'unico responsabile delle rispettive tabelle nel database.

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);
    }
}

Abbiamo quindi la classe AccountLogic, che gestisce tutte le logiche di business. Nell'esempio fornito, non vi è alcuna logica complicata da eseguire, quindi diventa puramente una chiamata a catena ai metodi di interfaccia.

La mia domanda è: come possiamo renderlo più facile da maneggiare, ma anche testabile su una scala più ampia? Se avessi un controller che ha bisogno di interrogare 10 diversi repository, sicuramente non è gestibile passare 15 interfacce al controller e quindi a una business class rilevante? Abbiamo bisogno di cambiare il modo in cui i repository sono gestiti in modo che non ci sia una classe per tabella?

    
posta user1677922 09.11.2017 - 22:10
fonte

2 risposte

6

Sebbene non sia interamente nello stile di C #, API monad gratis può aiutarti qui. Ecco un esempio C # .

In un'API monad gratuita, la tua interfaccia è modellata come oggetti:

// 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;
  }
}

Ciò consente di rappresentare l'intera interfaccia dati in modo uniforme in un singolo file, consentendo al tempo stesso una completa separazione dell'implementazione dell'interfaccia. Scrivi programmi componendo i tuoi oggetti di interfaccia in una monade gratuita:

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);
  }
}

Gli interpreti assomigliano a grandi istruzioni switch (pseudocodice):

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();
}

Uno dei vantaggi dell'utilizzo di questo modello è che è possibile separare le tabelle DB separatamente nel sistema, ma è possibile interpretarle tutte come un unico DSL. Ciò significa che nel test è possibile fornire un singolo interprete simulato per gestire tutte le chiamate DB previste (e generare un'eccezione se viene effettuata una chiamata inattesa) (pseudocodice):

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) ...

Invece di fornire numerosi simulatori di interfacce.

    
risposta data 09.11.2017 - 23:16
fonte
2

Hai provato a iniettare la classe concreta "AccountLogic" in "AccountController". Il tuo contenitore IOC dovrebbe essere in grado di creare una classe concreta senza alcuna configurazione aggiunta.

public class AccountController : Controller
{
    private readonly AccountLogic _accountLogic;

    public AccountController(AccountLogic accountLogic)
    {
        _accountLogic = accountLogic;
    }
    ...
}

Puoi prendere in giro una classe concreta per il test aggiungendo un costruttore vuoto e cambiando i metodi in "virtuale".

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 per allineare i parametri del costruttore verticalmente anziché orizzontalmente

Inoltre, usa

private readonly ISessionRepository _sessionRepository;

invece di

private ISessionRepository _sessionRepository { get; set; }

per gli oggetti iniettati.

    
risposta data 16.02.2018 - 17:33
fonte

Leggi altre domande sui tag