Limiti dell'iniezione di dipendenza

1

Uso l'iniezione delle dipendenze con Guice e la maggior parte delle volte sono piuttosto soddisfatto . Ma a volte, è difficile ottenere le dipendenze dove sono necessarie e, a volte, è impossibile.

Un esempio estremo è che MyResponse.toString() restituisce alcuni JSON, dove MyResponse contiene un elenco di DTO o un'eccezione. Ci sono dei requisiti, che lo stacktrace dovrebbe essere mostrato ai tester, il che significa che alcune informazioni sull'utente connesso devono essere fornite in qualche modo. Inoltre, quando eseguo il test localmente, ovviamente voglio sempre vedere lo stacktrace, anche quando disconnesso. E ci sono più tali regole.

Ovviamente, questa dipendenza può essere inclusa in MyResponse o fornita come argomenti (anche se non per il metodo toString() , ma sono d'accordo che toString() dovrebbe essere usato di solito solo per il debug). Questo diventa piuttosto brutto quando viene aggiunta una nuova dipendenza di questo tipo. Ad esempio, ho appena dovuto aggiungere un messaggio di utente finale localizzato alla rappresentazione JSON dell'eccezione. Il numero di tali dipendenze di seconda classe è cresciuto lentamente rispetto al numero di dipendenze rilevanti per la funzionalità di base.

A volte possono essere ben aggregati, ad esempio, posso fornire un Gson correttamente configurato, che si occupa del nascondimento dello stacktrace condizionale e della generazione di messaggi localizzati dell'utente finale. (*) Ma spesso non lo fa funzionano bene e finisco per passare un sacco di argomenti, o passare inutilmente grandi aggregati di dipendenza, o creare aggregati monouso. Alla fine, mi sembra di abusare di DI, poiché inizia a rendere il codice più brutto piuttosto che più bello.

Mi chiedo, se c'è un punto in cui dovrei semplicemente rinunciare e dire "questi dettagli noiosi saranno disponibili da un thread locale (o anche un singleton)"? Non credo ostacolerebbe il test in quanto queste dipendenze difficilmente hanno importanza e quando lo fanno, possono essere impostate secondo necessità.

(*) Questo può sembrare di attribuire troppe responsabilità alla povera istanza Gson , tuttavia, è la cosa responsabile della conversione di oggetti in JSON, quindi dovrebbe farlo esattamente come necessario. Il Gson è composto da un gruppo di adattatori e rende il ThrowableAdapter consapevole del nascondiglio dello stacktrace e la generazione di messaggi utente localizzati sembra una buona idea. L'unico problema è che le informazioni necessarie (1. Dovrebbe essere mostrato lo stacktrace? 2. Qual è la lingua per il messaggio?) Deve essere passato in qualche modo ad esso.

Pensandoci meglio, potrei avvolgere Throwable in MyThrowableWithUserInfo e scrivere un adattatore per questo (posso sempre fare questo wrapping prima di aver bisogno di Gson). In questo caso particolare, sembra essere la soluzione DI perfetta.

In generale, non è così bello come potrebbe esserci qualcosa da buttare da qualche parte nel profondo dell'oggetto da serializzare e aggiungere queste informazioni a loro potrebbe essere impossibile. Ho paura, sarebbe necessaria una modifica locale del thread, ma questo hack può essere limitato solo al processo di serializzazione, il che lo rende molto meno brutto.

    
posta maaartinus 20.10.2017 - 08:33
fonte

1 risposta

4

Analisi del problema:

... sometimes, it's difficult to get the dependencies to where they're needed and sometimes, it's plain impossible.

... An extreme example is making MyResponse.toString() return some JSON...

... stacktrace should only be shown to testers ...

... Obviously, this dependency can be included in MyResponse or provided as arguments...

Dal tuo contesto, mi sembra che Dependency Injection potrebbe non essere il problema principale. Invece, IMO forse il design delle tue classi sta diventando troppo complesso, essendo difficile gestire le dipendenze e le relazioni. Inoltre, credo che dovresti rivedere la struttura generale delle classi e verificare se la tua applicazione sta seguendo tutte le migliori pratiche e principi. Ad esempio:

I can provide a properly configured Gson, which takes care about the conditional stacktrace hiding and the localized end-user message generation.

Sembra che questo oggetto possa violare il principio di responsabilità singola. Facendo cose del genere, stai influenzando negativamente la progettazione dell'applicazione, rendendo difficile l'ulteriore manutenzione.

Quindi, dando un'occhiata al tuo problema:

But oftentimes, it doesn't work well and I end up passing lots of arguments, or passing needlessly big dependency aggregates, or creating single-use aggregates. In the end, it feels like I'm overusing DI as it starts to make the code more ugly rather then nicer.

Suggerisco di rivedere la progettazione dell'applicazione e provare a utilizzare schemi di progettazione che possono aiutare a gestire dipendenze o astrazioni. Ad es .: puoi provare a usare:

  • Pattern di facciata: il client chiama il servizio, che nasconde un Api più complesso, aggregando o gestendo parti della risposta;
  • Schema mediatore: oggetti che semplificano le relazioni tra molti oggetti che potrebbero funzionare insieme per alcune attività;
  • Motivo decoratore: aggiunge dinamicamente comportamento agli oggetti in fase di runtime;
  • Verifica tutte le classi e i moduli e assicurati che non violino anche altri principi di solidità;

Per ora posso pensare a questi modelli, se avrò più idee modificherai la mia risposta.

Facciata: link

Mediatore: link

Decoratore : link

Dopo aver letto le tue modifiche, ti suggerisco il modello Decorator per il tuo caso. Di seguito fornisco alcuni esempi di codice basati su esempi di risposta.

class MyResponse {
    //list of Dtos or exception
}

interface MyResponseProcessor {
    MyResponse processResponse(MyResponse originalResponse);
}

interface MyResponseProcessorDecorator implements MyResponseProcessor {
    private MyResponseProcessor processor;

    MyResponse processResponse(MyResponse originalResponse);
}

class MyResponseBaseProcessor implements MyResponseProcessor {

    MyResponse processResponse(MyResponse originalResponse) {
        //basic processing of your response information
    }
}

class MyResponseStackTraceProcessor implements MyResponseProcessorDecorator {
    private MyResponseProcessor processor;

    public MyResponseStackTraceProcessor(MyResponseProcessor processor) {
        this.processor = processor;
        //Maybe inject some object capable of retrieving current user's permissions,
        //in order to know if should hide stack trace or not
    }

    MyResponse processResponse(MyResponse originalResponse) {
        //transforms original response, by removing info
        //depending on user's role of access
        //Returns a new response with the transformed information
    }
}

class MyResponseLocalizedErrorMessageProcessor implements MyResponseProcessorDecorator {
    private MyResponseProcessor processor;

    public MyResponseStackTraceProcessor(MyResponseProcessor processor) {
        this.processor = processor;
        //Inject other objects capable of loading current localization info
    }

    MyResponse processResponse(MyResponse originalResponse) {
        //transforms original response, by loading appropriate
        //localized string according to the error message within the 
        //original response;
        //Returns new MyResponse with error message added;
    }
}

interface MyResponseSerializer {
    String serialize(MyResponse response);
}

class MyResponseJsonSerializer implements MyResponseSerializer {
    String serialize(MyResponse response) {
        //only gets the info and convert them to Json
    }
}

class MyResponseXmlSerializer implements MyResponseSerializer {
    String serialize(MyResponse response) {
        //only gets the info and convert them to Xml
    }
}


//Example of use

String getSerializedResponse(MyResponse response) {

    //Decorations for response processor
    MyResponseProcessor processor = new MyResponseBaseProcessor();
    processor = new MyResponseStackTraceProcessor(processor);
    processor = new MyResponseLocalizedErrorMessageProcessor(processor);

    MyResponse processedResponse = processor.processResponse(response);

    MyResponseSerializer serializer = new MyResponseJsonSerializer();

    return serializer.serialize(processedResponse);
}
    
risposta data 20.10.2017 - 11:58
fonte