Come fare la dipendenza Iniezione e creazione di oggetti condizionali in base al tipo?

4

Ho un endpoint di servizio inizializzato usando DI. È del seguente stile. Questo punto finale viene utilizzato in tutta l'app.

 public class CustomerService : ICustomerService
{
    private IValidationService ValidationService { get; set; }
    private ICustomerRepository Repository { get; set; }

    public CustomerService(IValidationService validationService,ICustomerRepository repository)
    {
        ValidationService = validationService;
        Repository = repository;
    }


    public void Save(CustomerDTO customer)
    {
        if (ValidationService.Valid(customer))
            Repository.Save(customer);
    }

Ora, con i mutevoli requisiti, ci saranno diversi tipi di clienti (Legacy / Regular). Il requisito si basa sul tipo di cliente che devo convalidare e perseguire il cliente in un modo diverso (ad esempio se il cliente legacy persiste su LegacyRepository).

Il modo sbagliato per fare ciò sarà di rompere DI e fare qualcosa come

  public void Save(CustomerDTO customer)
    {
        if(customer.Type == CustomerTypes.Legacy)
        {
            if (LegacyValidationService.Valid(customer))
                LegacyRepository.Save(customer);
        }
        else
        {
            if (ValidationService.Valid(customer))
                Repository.Save(customer);
        }
    }

Le mie opzioni mi sembrano

  1. DI tutto possibile IValidationService e ICustomerRepository e cambia in base al tipo, che sembra sbagliato.
  2. L'altro è di cambiare la firma del servizio in Save(IValidationService validation, ICustomerRepository repository, CustomerDTO customer) , che è una modifica invasiva.
  3. Break DI. Usa l'approccio del modello di strategia per ogni tipo e fai qualcosa del tipo:

    validation= CustomerValidationServiceFactory.GetStratedgy(customer.Type);
    validation.Valid(customer)
    

    ma ora ho un metodo statico che deve sapere come inizializzare diversi servizi.

Sono sicuro che questo è un problema molto comune, qual è il modo giusto per risolvere questo problema senza modificare le firme di servizio o interrompere DI?

    
posta Pradeep 19.11.2012 - 14:52
fonte

6 risposte

1

Se hai solo due tipi di clienti, userei la versione brutta per lasciare che CustomerService.Save(CustomerDTO customer) decida quale repository usare. Ovviamente questo viola il principio aperto-chiuso, ma obbedisce al keepItSimpleStupid (bacio).

Se hai davvero diversi tipi di clienti, implementerei un servizio clienti per ogni tipo di cliente e un servizio clienti in cima che decide quale servizio spezializzato usare usando un modello di strategia.

 > but now I have a static method which needs to know how to initialize different services.

Non è necessario che la strategia sia un metodo statico.

Invece di lasciare che la strategia conosca ogni servizio puoi invertire la dipendenza e lasciare che ogni servizio specializzato registrato si dimostri così straziato come questo

 public class LegacyCustomerService : ICustomerService
 {
    LegacyCustomerService(IStrategy strategy)
    {
       strategy.register(this, typeof(LegacyCustomer));
    }
 }

La strategia utilizza un hash / dizionario dal tipo di cliente al servizio.

Questa è una soluzione open-closed-pripiple che viola il principio keepItSimpleStupid (bacio).

    
risposta data 19.11.2012 - 16:17
fonte
1

Non utilizzerei un metodo statico, ma sembra che un contenitore IoC (o DI semplice a seconda del modello di utilizzo) per il tuo ICustomerService sia appropriato. Dopotutto, se il cliente impone che la convalida e il pronti contro termine cambino entrambi insieme, che è l'asse di cambiamento, non ciascuno dei membri in modo indipendente.

Dovrebbe anche, si spera, ridurre il numero di combinazioni, rendendo il codice meno complesso.

    
risposta data 19.11.2012 - 15:18
fonte
1

CustomerService è accoppiato a tre interfacce (ValidationService, Repository e CustomerDTO) e fondamentalmente orchestra una convalida prima di salvare.

L'opzione migliore è fare un passo indietro e considerare diverse istanze di CustomerService, una per ogni tipo di cliente e già iniettate con i servizi appropriati. È possibile utilizzare il modello di strategia per passare tra le istanze di CustomerService, modificando il chiamante delle istanze CS.

Utilizzando questo approccio, non è necessario modificare CustomerService, ma solo il relativo chiamante. Tale impatto è coerente con il cambiamento: i nuovi requisiti suggeriscono nuovi tipi di clienti, ma non cambiano il modo in cui sono convalidati e mantenuti.

    
risposta data 19.11.2012 - 17:18
fonte
1

Aggiungi un livello o due di astrazione.

Aggiungi un'interfaccia per racchiudere le implementazioni vincolate; qualcosa come:

public interface IValidatingRepository 
{
    public string Key {get; }
    public IValidationService ValidationService {get; }
    public ICustomerRepository Repository {get; }

    //personally, I would prefer to have this operation 
    // return bool or bool? or give some kind of feedback
    //Also, this could be genericized to TDto dto vs CustomerDTO customer
    void Save(CustomerDTO customer);
}

Implementa il repository IValidating, ad esempio:

//this could alternately be an abstract base class, 
// if you wanted to require custom subclasses for each strategy 
public class ValidatingRepository : IValidatingRepository 
{  
    //these could be backed by readonly fields vs private setters if you prefer.
    public string Key {get; private set;}
    public IValidationService ValidationService {get; private set;}
    public ICustomerRepository Repository {get; private set;}

    public ValidatingRepository(
        string key, 
        IValidationService validationService, 
        ICustomerRepository repository
    )  
    { /* set properties / fields */}      

    public virtual void Save(CustomerDTO customer)
    {
        var validationService = ValidationService;
        var repository = Repository;
        if(customer == null || validationService == null || repository == null)
            return; //false? throw? some kind of feedback?

        //optional
        //if(!Preconditions())
        //  return; //false?

        if (validationService.Valid(customer))
            repository.Save(customer); //any return value from repository.Save(...)?

        //optional
        //return //Postconditions(customer);?
    }

    //optional
    protected virtual bool Preconditions(CustomerDTO customer)
    {
        if(customer.Type != Key)
            return false;  
        /* any other preconditions */
        return true;
    }

    //optional
    protected virtual bool Postconditions(CustomerDTO customer)
    {
        /* any postconditions */
    }
}

Modificare l'implementazione del servizio in modo da avere un'istanza di IValidatingRepository [] (o IEnumerable o altra raccolta di propria scelta) anziché le istanze di IValidationService e ICustomerRepository.

Cambia il ctor in modo che corrisponda a [1] .

public class CustomerService : ICustomerService
{
    private readonly IValidatingRepository[] ValidatingRepositorySet;

    public CustomerService(IValidatingRepository[] validatingRepositorySet)
    {
        ValidatingRepositorySet = validatingRepositorySet;
    }


    public void Save(CustomerDTO customer)
    {
        if(customer == null)
           return;

        var validatingRepositorySet = ValidatingRepositorySet;
        if(validatingRepositorySet == null)
           return;

        var key = customer.Type;
        var validatingRepository = validatingRepositorySet[key];
        if(validatingRepository == null)
            return;

        validatingRepository.Save(customer);

    }

Ovunque tu stia costruendo l'istanza di CustomerService, componi insieme le coppie di servizio e di deposito corrette in una raccolta di istanze di IValidatingRepository e inseriscile nel servizio immesso da Customer.Type o qualsiasi altra cosa sia applicabile o funzioni per il tuo utilizzo. Se si aggiungono più coppie servizio / repository in seguito, nessun grosso problema. Se vuoi inserire diversi set / mock per i test, puoi farlo. Ecc.

EXTENDED

Se il tuo meccanismo di iniezione non è in grado di gestirlo o preferisci non inserirlo direttamente nelle raccolte per qualche motivo, puoi aggiungere un altro livello di astrazione creando un'altra interfaccia per raggruppare l'idea di più repository e un servizio di convalida corrispondente specifico ( e potenzialmente altri concetti specifici di implementazione in seguito); qualcosa di simile:

//you could also make this generic for TDto, and generalize 
// away from CustomerDto to make this a more general purpose solution
public interface IValidatingRepositorySet 
{
   //IEnumerable<>, or whatever collection you like
   IValidatingRepository[] RepositorySet { get; set; } 

   //optional; you could move the strategy detection out of the service
   // and into the IValidatingRepositorySet implementation. 
   void Save(CustomerDTO customer);

}

E naturalmente modificare l'implementazione del servizio per avere un'istanza di IValidatingRepositorySet, e il ctor per prendere un'istanza di esso, e un'implementazione predefinita da qualche parte, ecc. Personalmente, non lo farei se non ne avessi uno o più altre cose che si applicavano all'insieme nel suo complesso o appartenevano a un livello peer al set ... come lo spostamento della logica di selezione della strategia dal servizio (il metodo di salvataggio opzionale in alto).

NOTA: ho scritto il codice nella finestra dei commenti al volo; è solo a tratti ampi.

[1] In alternativa, se per ragioni di supporto legacy è necessario conservare il ctor originale, è sufficiente racchiudere il passato in IValidationService & ICustomerRepository in un concreto ValidatingRepository predefinito (dovrai decidere su una chiave jolly, o avere un'implementazione caso speciale di ValidatingRepository che ignora la chiave o supporta una chiave alternativa) che fornisce un'implementazione semplice identica alla funzionalità corrente e imposta il campo IValidatingRepositorySet / proprietà a una nuova raccolta contenente questa istanza. Eviterei questo se puoi, ma è un'opzione.

    
risposta data 19.11.2012 - 23:21
fonte
0

Inietti i tuoi IValidationServices in CustomerValidationServiceFactory. GetStrategy non dovrebbe essere statico. Inject CustomerValidationServiceFactory nel tuo CustomerService. Usa come approproiate.

    
risposta data 19.11.2012 - 15:15
fonte
0

Se sono privati e non c'è alcuna logica dietro i getter / setter (come appare nell'esempio di codice), i membri della tua classe non dovrebbero avere alcuna ragione:

private IValidationService ValidationService { get; set; }
private ICustomerRepository Repository { get; set; }

sarebbe meglio servito come:

private IValidationService ValidationService;
private ICustomerRepository Repository;

o ancora meglio, se sono solo impostati nel costruttore (di nuovo, come mostra il codice), rendili readonly per dichiarare l'intento e mantenerli invariabili per tutta la durata della classe:

private readonly IValidationService ValidationService;
private readonly ICustomerRepository Repository;
    
risposta data 19.11.2012 - 15:52
fonte

Leggi altre domande sui tag