2015-05-25 13 views
12

Attualmente sto creando un'applicazione Web e sto tentando di progettarla seguendo il buon MVC e l'architettura orientata ai servizi.Separazione di validatore e servizio con chiamate API esterne

Ho, tuttavia, colpito un po 'di un muro nel collegare il livello di presentazione (cioè i miei controller) e i servizi di back-end, pur mantenendo un buon rapporto di errore/convalida di nuovo all'utente.

Ho letto un SO post davvero valido here su come separare la logica di convalida dal livello di servizio e per la maggior parte tutto aveva senso. Tuttavia c'era un "difetto", se così si può chiamare, in questo modello che mi ha ridestato: come evitare di duplicare gli sforzi quando si cercano oggetti richiesti sia dal validatore che dal servizio?

penso che sarebbe stato più facile da spiegare con un ragionevolmente semplice esempio:

Diciamo Ho un'applicazione che consente agli utenti di condividere frammenti di codice in giro. Ora, ho deciso di aggiungere una nuova funzionalità che consente a un utente di collegare il proprio account GitHub al proprio account sul mio sito (ad esempio per creare un profilo). Ai fini di questo esempio assumerò semplicemente che tutti i miei utenti sono affidabili e tenterebbero solo di aggiungere i propri account GitHub, non quelli di altri :)

In seguito al suddetto articolo SO che ho impostato un servizio GitHub di base per il recupero delle informazioni utente GitHub.

interface IGitHubUserService { 
    GitHubUser FindByUserName(string username); 
} 

L'attuazione concreta di GitHubUserService effettua una chiamata costoso per https://api.github.com/users/{0} per tirare le informazioni dell'utente. Anche in questo caso, seguendo il modello del articolo che ho implementato il seguente comando per collegare un account utente a un utente GitHub:

// Command for linking a GitHub account to an internal user account 
public class GitHubLinkCommand { 
    public int UserId { get; set; } 
    public string GitHubUsername { get; set } 
}; 

mio validatore ha bisogno di verificare che il nome utente immesso dall'utente è un account GitHub valido. Questo è molto semplice: chiamare FindByUserName sulla GitHubUserService e assicurarsi che il risultato non è nullo:

public sealed class GitHubLinkCommandValidator : Validator<GitHubLinkCommand> { 
    private readonly IGitHubUserService _userService; 

    public GitHubLinkCommandValidator(IGitHubUserService userService) { 
     this._userService = userService; 
    } 

    protected override IEnumerable<ValidationResult> Validate(GitHubLinkCommand command) { 
     try { 
      var user = this._userService.FindByUserName(command.GitHubUsername); 
      if (user == null) 
       yield return new ValidationResult("Username", string.Format("No user with the name '{0}' found on GitHub's servers.")); 
     } 
     catch(Exception e) { 
      yield return new ValidationResult("Username", "There was an error contacting GitHub's API."); 
     } 
    } 
} 

Va bene che è grande! Il validatore è davvero semplice e sensato. Ora è il momento di fare il GitHubLinkCommandHandler:

public class GitHubLinkCommandHandler : ICommandHandler<GitHubLinkCommand> 
{ 
    private readonly IGitHubUserService _userService; 

    public GitHubLinkCommandHandler(IGitHubUserService userService) 
    { 
     this._userService = userService; 
    } 

    public void Handle(GitHubLinkCommand command) 
    { 
     // Get the user details from GitHub: 
     var user = this._userService.FindByUserName(command.GitHubUsername); 

     // implementation of this entity isn't really relevant, just assume it's a persistent entity to be stored in a backing database 
     var entity = new GitHubUserEntity 
     { 
      Name = user.Login, 
      AvatarUrl = user.AvatarUrl 
      // etc. 
     }; 

     // store the entity: 
     this._someRepository.Save(entity); 
    } 
} 

Ancora una volta, questo sembra veramente pulito e lineare. Tuttavia c'è un problema lampante: le chiamate duplicate a IGitHubUserService::FindByUserName, una dal validatore e una dal servizio. In una giornata negativa, una chiamata del genere può richiedere 1-2 secondi senza il caching sul lato server, rendendo la duplicazione troppo costosa per utilizzare questo modello architettonico.

Qualcun altro ha riscontrato un tale problema durante la scrittura di validatori/servizi intorno a API esterne e in che modo è stata ridotta la duplicazione degli sforzi al di fuori dell'implementazione di una cache nella classe concreta?

+2

Di solito disaccordo 'IGitHubUserService' dalla mia applicazione. Lì metterei una cache e anche se spesso si comporta come un ** Proxy ** (con qualche _decoration_) potrebbe anche diventare un ** Adapter ** (se l'interfaccia di GitHub cambia) o anche un ** Bridge ** (in caso vuoi renderlo abbastanza generico da essere usato anche con CodePlex, Google Code ...) –

+0

@AdrianoRepetti Questa è praticamente l'unica soluzione che ho trovato, ma non è davvero una gran "soluzione". Potrei vederlo funzionare per un grande progetto, ma sembra un enorme investimento per una funzionalità così banalmente implementata come quella nell'esempio. –

+2

Proxy con cache per IGitHubService non è molto più linee di codice di GitHubLinkCommandHandler (supponendo che sia necessario esporre solo FindUserByName) ma sì, sono d'accordo sarebbe bello se ci fosse _qualcosa_ per automatizzare questo codice boilerplate (come faremmo con AOP). –

risposta

1

Dal mio punto di vista, il problema è che né il LinkCommandHandler né il LinkCommandValidator devono recuperare l'utente GitHub in primo luogo. Se si pensa in termini di Principio di Responsabilità Unica, il Validatore ha un singolo lavoro, per validare l'esistenza di un utente, e LinkCommandHanlder ha un singolo lavoro per caricare l'entità nel repository. Nessuno dei due dovrebbe avere il compito di estrarre Entità/Utente da GitHub.

Mi piace strutturare il mio codice nel seguente schema, ciascuno rappresentante un livello attribuibile. Ogni livello può parlare con lo strato sopra e il livello sottostante, ma non può saltare su un livello.

  1. Livello dati: rappresenta un'origine dati come un database o un servizio in genere non si scrive codice per questo, lo si consuma.
  2. Access Layer - questo rappresenta il codice per interagire con il dataLayer
  3. Peristence Strato - questo rappresenta il codice per ottenere elementi pronti per la chiamata al livello di accesso come ad esempio traduzioni di dati, la costruzione di entità dai dati, o raggruppare più chiamate al livello di accesso in un'unica richiesta per recuperare dati o memorizzare dati. Inoltre, la decisione di memorizzare nella cache e i meccanismi per la memorizzazione nella cache e la cancellazione della cache risiederanno in questo livello.
  4. Livello processore: rappresenta il codice che esegue la logica aziendale. E 'anche il luogo in cui faresti uso di validatori, altri processori, parser, ecc.

E poi tengo tutto ciò che è stato separato dal mio livello di presentazione. Il concetto è che il codice principale e la funzionalità non dovrebbero sapere se viene utilizzato da un sito Web o da un'app desktop o da un servizio WCF.

Quindi nel vostro esempio, avrei un oggetto GitHubLinkProcessor un metodo chiamato LinkUser (nome utente stringa). All'interno di questa classe, istanzerei la mia classe GitHubPeristenceLayer e chiamerei il suo metodo FindUserByName (string username). Successivamente, procederemo all'istanziazione di una classe GitHubUserValidator per convalidare che l'utente non sia nullo e che siano presenti tutti i dati necessari. Una convalida viene passata, un oggetto LinkRepositoryPersistence viene istanziato e passato GitHubUser per la persistenza in AccessLayer.

Ma voglio sottolineare con forza che questo è solo il modo in cui lo farei, e in nessun modo voglio implicare che altre metodologie siano meno validi.

EDIT:

Stavo per una semplice risposta perché avevo paura la mia risposta era già troppo lungo e noioso. =) Ho intenzione di dividere i capelli qui per un momento, quindi per favore portami con me. Per me, non stai convalidando l'utente chiamando Git. Stai verificando l'esistenza di una risorsa remota, che può o non può fallire. Un'analogia può essere che è possibile verificare che (800) 555-1212 sia un formato valido per un numero di telefono degli Stati Uniti, ma non che il numero di telefono esista e appartenga alla persona corretta. Questo è un processo separato. Come ho detto, sta dividendo i capelli, ma così facendo consente lo schema generale del codice che descrivo.

Supponiamo quindi che l'oggetto utente locale disponga di una proprietà per UserName e Email che non può essere nullo. Verificheresti per quelli e passerai alla verifica della risorsa solo se quella convalida era corretta.

public class User 
{ 
    public string UserName { get; set; } 
    public string Email { get; set; } 

    //git related properties 
    public string Login { get; set; } 
    public string AvataUrl { get; set; } 
} 

//A processor class to model the process of linking a local system user 
//to a remote GitHub User 
public class GitHubLinkProcessor() 
{ 
    public int LinkUser(string userName, string email, string gitLogin) 
    { 
      //first create our local user instance 
      var myUser = new LocalNamespace.User { UserName = userName, Email = email }; 

     var validator = new UserValidator(myUser); 
     if (!validator.Validate()) 
      throw new Exception("Invalid or missing user data!"); 

     var GitPersistence = new GitHubPersistence(); 

     var myGitUser = GitPersistence.FindByUserName(gitLogin); 
     if (myGitUser == null) 
      throw new Exception("User doesnt exist in Git!"); 

     myUser.Login = myGitUser.Login; 
     myUser.AvatorUrl = myGitUser.AvatarUrl; 

     //assuming your persistence layer is returning the Identity 
     //for this user added to the database 
     var userPersistence = new UserPersistence(); 
     return userPersistence.SaveLocalUser(myUser); 

     } 
} 

public class UserValidator 
{ 
    private LocalNamespace.User _user; 

    public UserValidator(User user) 
    { 
     this._user = user; 
    } 

    public bool Validate() 
    { 
     if (String.IsNullOrEmpty(this._user.UserName) || 
      String.IsNullOrEmpty(this._user.Email)) 
     { 
      return false; 
     } 
    } 
} 
+0

La teoria è valida, ma ho già provato una soluzione del genere e presenta uno svantaggio serio: stai * sempre * recuperando l'utente GitHub, anche prima di eseguire la convalida di base (cioè tutti i campi del modulo sono presenti?). Ignorando un livello di memorizzazione nella cache, questo è un lavoro ridondante. C'è un sacco di convalida "a buon mercato" che può fallire prima ancora di dover effettuare una chiamata API a GitHub e sarebbe molto più efficiente eseguirla per prima. –

+0

L'unico modo che posso vedere intorno ad esso (usando la tua architettura) sarebbe quello di interrompere il processo di convalida in più metodi, in modo che il 'GitHubLinkProcessor' possa ancora essere responsabile del flusso, pur riuscendo a fallire all'inizio della convalida processi. –

+0

Ciao Jason, hai ragione in quell'ipotesi. Ecco perché ho aggiunto un po 'di più alla mia risposta per chiarire un po'. –

Problemi correlati