Le cosiddette "preoccupazioni trasversali" sono una scusa valida per rompere SOLID / DI / IoC?

43

Ai miei colleghi piace dire "logging / caching / etc. è una preoccupazione trasversale" e quindi procedere usando il corrispondente singleton ovunque. Eppure amano IoC e DI.

È davvero una scusa valida per infrangere il principio SOLI D ?

    
posta Den 13.05.2016 - 13:22
fonte

11 risposte

42

No.

SOLID esiste come linee guida per spiegare il cambiamento inevitabile. davvero non cambierai mai la tua libreria di registrazione, né target, né filtro, né formattazione, o ...? davvero non cambierai la tua libreria di memorizzazione nella cache, né la tua destinazione, la tua strategia, il tuo ambito, o ...?

Certo che lo sei. Al minimo , vorrai prendere in giro queste cose in modo sano per isolarle per il test. E se vuoi isolarli per i test, probabilmente ti imbatterai in una ragione aziendale in cui li vuoi isolare per motivi di vita reale.

E otterrai quindi l'argomento che il logger stesso gestirà la modifica. "Oh, se il target / filtro / formattazione / strategia cambia, allora cambieremo semplicemente la configurazione!" Questa è spazzatura. Non solo ora hai un oggetto di Dio che gestisce tutte queste cose, stai scrivendo il tuo codice in XML (o simile) dove non ottieni l'analisi statica, non ricevi errori di compilazione nel tempo e non lo fai Ottieni davvero test unitari efficaci.

Ci sono casi in cui violare le linee guida SOLID? Assolutamente. A volte le cose non cambiano (senza richiedere comunque una riscrittura completa). A volte una leggera violazione di LSP è la soluzione più pulita. A volte creare un'interfaccia isolata non fornisce alcun valore.

Ma il logging e il caching (e altri problemi trasversali onnipresenti) non sono quei casi. Di solito sono ottimi esempi dei problemi di accoppiamento e progettazione che si verificano quando si ignorano le linee guida.

    
risposta data 13.05.2016 - 15:27
fonte
38

Questo è l'intero punto del termine "preoccupazione trasversale" - significa qualcosa che non si adatta perfettamente al principio SOLID.

È qui che l'idealismo si incontra con la realtà.

Le persone semi-nuove su SOLID e trasversali spesso si imbattono in questa sfida mentale. Va bene, non spaventare. Cerca di mettere tutto in termini di SOLID, ma ci sono alcuni posti come la registrazione e il caching in cui SOLID non ha senso. Il taglio trasversale è il fratello di SOLID, vanno di pari passo.

    
risposta data 13.05.2016 - 18:20
fonte
25

Per la registrazione penso che lo sia. La registrazione è pervasiva e generalmente non correlata alla funzionalità del servizio. È comune e ben compreso l'utilizzo dei pattern singleton framework di registrazione. Se non lo fai, stai creando e inserendo i logger ovunque e non lo vuoi.

Un problema di cui sopra è che qualcuno dirà "ma come posso testare la registrazione?" . I miei pensieri sono che normalmente non testare la registrazione, oltre a sostenere che posso effettivamente leggere i file di registro e capirli. Quando ho visto la registrazione testata, di solito è perché qualcuno ha bisogno di affermare che una classe ha effettivamente fatto qualcosa e stanno usando i messaggi di log per ottenere quel feedback. Preferirei piuttosto registrare un ascoltatore / osservatore in quella classe e affermare nei miei test che ciò viene chiamato. Puoi quindi inserire la registrazione degli eventi in quell'osservatore.

Penso che il caching sia uno scenario completamente diverso, comunque.

    
risposta data 13.05.2016 - 13:41
fonte
12

I miei 2 centesimi ...

Sì e no.

Non dovresti mai davvero violare i principi che adotti; ma i tuoi principi dovrebbero sempre essere sfumati e adottati al servizio di un obiettivo più alto. Quindi, con una comprensione propriamente condizionata, alcune violazioni apparenti potrebbero non essere effettive violazioni dello "spirito" o "corpo di principi nel suo complesso."

I principi SOLID in particolare, oltre a richiedere molte sfumature, sono in definitiva subordinati all'obiettivo di "fornire software funzionante e gestibile". Quindi, aderire a qualsiasi particolare principio SOLID è controproducente e auto-contraddittorio quando lo si fa in conflitto con gli obiettivi di SOLID. E qui, noto spesso che consegna trumps manutenibilità .

Quindi, che dire del D in SOLID ? , contribuisce ad aumentare la manutenibilità rendendo il modulo riusabile relativamente agnostico al suo contesto. E possiamo definire il "modulo riutilizzabile" come "una raccolta di codice che prevedi di utilizzare in un altro contesto distinto". E questo vale per singole funzioni, classi, gruppi di classi e programmi.

E sì, cambiare le implementazioni del logger probabilmente mette il tuo modulo in un "altro contesto distinto".

Quindi, permettimi di offrire i miei due importanti avvertimenti :

In primo luogo: Disegnare le linee attorno ai blocchi di codice che costituiscono "un modulo riutilizzabile" è una questione di giudizio professionale. E il tuo giudizio è necessariamente limitato alla tua esperienza .

Se non si pianifica attualmente sull'uso di un modulo in un altro contesto, è probabilmente OK perché possa dipendere da esso in modo impotente. Il caveat al caveat: i vostri piani sono probabilmente sbagliati - ma questo è anche OK. Quanto più si scrive modulo dopo modulo, tanto più intuitivo e accurato sarà il senso se "Avrò bisogno di questo di nuovo un giorno". Ma probabilmente mai sarà in grado di dire retrospettivamente, "Ho modularizzato e disaccoppiato tutto nella misura più ampia possibile, ma senza eccesso ."

Se ti senti in colpa per i tuoi errori di giudizio, vai alla confessione e vai avanti ...

In secondo luogo: Il controllo inverso non è uguale alle dipendenze per l'iniezione .

Questo è particolarmente vero quando si avvia iniettando dipendenze e nauseam . La dipendenza dall'iniezione è una tattica utile per la strategia globale IoC. Ma, direi che DI è di minore efficacia di altre tattiche - come l'uso di interfacce e adattatori - singoli punti di esposizione al contesto all'interno del modulo .

E concentriamoci su questo per un secondo. Perché, anche se si inietta un Logger ad nauseam , è necessario scrivere codice contro l'interfaccia Logger . Non è possibile iniziare a utilizzare un nuovo Logger da un altro fornitore che accetta parametri in un ordine diverso. Questa capacità deriva dalla codifica, all'interno del modulo, contro un'interfaccia che esiste all'interno del modulo e che ha un singolo submodulo (Adapter) al suo interno per gestire la dipendenza.

E se stai codificando contro un adattatore, se il Logger è iniettato in quell'adapter o scoperto dall'adapter è in genere dannatamente insignificante rispetto all'obiettivo generale di gestibilità. E, cosa più importante, se hai un adattatore a livello di modulo, è probabilmente assurdo iniettarlo in qualcosa. Ha scritto per il modulo.

tl; dr - Smetti di preoccuparti dei principi senza tener conto del perché stai usando i principi. E, più in pratica, crea solo un Adapter per ogni modulo. Usa il tuo giudizio quando decidi dove tracciare i confini del "modulo". Da ogni modulo, vai avanti e fai riferimento direttamente a Adapter . E sicuramente, inserisci il logger real in Adapter - ma non in ogni piccola cosa che potrebbe averne bisogno.

    
risposta data 13.05.2016 - 20:19
fonte
8

L'idea che la registrazione dovrebbe sempre essere implementata come singleton è una di quelle menzogne che è stata raccontata così spesso che ha guadagnato aderenza.

Fino a quando i moderni sistemi operativi sono stati su di esso è stato riconosciuto che si potrebbe desiderare di accedere a più posti a seconda della natura dell'output .

I progettisti di sistemi dovrebbero costantemente mettere in discussione l'efficacia delle soluzioni passate prima di includerle ciecamente in quelle nuove. Se non stanno portando avanti una tale diligenza, allora non stanno facendo il loro lavoro.

    
risposta data 13.05.2016 - 14:20
fonte
4

La registrazione autentica è un caso speciale.

@Telastyn scrive:

Are you really never going to change your logging library, or target, or filtering, or formatting, or...?

Se si prevede che potrebbe essere necessario modificare la libreria di registrazione, è necessario utilizzare una facciata; Ad esempio SLF4J se ti trovi nel mondo Java.

Per il resto, una libreria di registrazione decente si occupa di cambiare la posizione di registrazione, quali eventi vengono filtrati, come vengono formattati gli eventi di registro utilizzando i file di configurazione del logger e (se necessario) le classi di plug-in personalizzate. Ci sono un certo numero di alternative disponibili in commercio.

In breve, questi sono i problemi risolti ... per la registrazione ... e quindi non c'è bisogno di usare Dependency Injection per risolverli.

L'unico caso in cui DI potrebbe essere vantaggioso (rispetto agli approcci di registrazione standard) è se si desidera sottoporre la registrazione dell'applicazione al test dell'unità. Tuttavia, sospetto che la maggior parte degli sviluppatori direbbe che la registrazione non fa parte di una funzionalità di classi, e non qualcosa che ha bisogno di per essere testato.

@Telastyn scrive:

And you'll then get the argument that the logger itself will handle the change. "Oh, if the target/filtering/formatting/strategy changes, then we'll just change the config!" That is garbage. Not only do you now have a God Object that handles all of these things, you're writing your code in XML (or similar) where you don't get static analysis, you don't get compile time errors, and you don't really get effective unit tests.

Ho paura che sia un riassunto molto teorico. In pratica, la maggior parte degli sviluppatori e degli integratori di sistema piace il fatto che è possibile configurare la registrazione tramite un file di configurazione. E a loro piace il fatto che non ci si aspetti di testare il logging di un modulo.

Sicuramente, se si infastidiscono le configurazioni di registrazione, si possono avere problemi, ma si manifestano come l'errore dell'applicazione durante l'avvio o la registrazione troppo / troppo piccola. 1) Questi problemi sono facilmente risolvibili correggendo l'errore nel file di configurazione. 2) L'alternativa è un ciclo completo di compilazione / analisi / test / distribuzione ogni volta che si apportano modifiche ai livelli di registrazione. Non è accettabile.

    
risposta data 14.05.2016 - 03:59
fonte
3

e No !

Sì: penso sia ragionevole che diversi sottosistemi (o livelli semantici o librerie o altre nozioni di raggruppamento modulare) accetti ciascuno (lo stesso o) logger potenzialmente diverso durante l'inizializzazione piuttosto che tutti i sottosistemi facendo affidamento sullo stesso comune singleton condiviso .

Tuttavia,

No: allo stesso tempo è irragionevole parametrizzare la registrazione in ogni piccolo oggetto (per costruttore o metodo di istanza). Per evitare inutili e inutili gonfie, le entità più piccole dovrebbero utilizzare il logger Singleton del loro contesto di chiusura.

Questo è uno dei motivi per cui molti pensano alla modularità nei livelli: i metodi sono raggruppati in classi, mentre le classi sono raggruppate in sottosistemi e / o livelli semantici. Questi fasci più grandi sono preziosi strumenti di astrazione; dovremmo dare considerazioni diverse entro i limiti modulari di quando li incrociamo.

    
risposta data 13.05.2016 - 19:10
fonte
3

Per prima cosa inizia con una strong cache di singleton, le cose successive che vedi sono forti singleton per il livello di database che introduce lo stato globale, le API non descrittive di class es e il codice non testabile.

Se decidi di non avere un singleton per un database, probabilmente non è una buona idea avere un singleton per una cache, dopotutto rappresentano un concetto molto simile, l'archiviazione dei dati, usando solo meccanismi diversi.

L'uso di un singleton in una classe trasforma una classe che ha una quantità specifica di dipendenze in una classe che ha teoricamente , un numero infinito di esse, perché non si sa mai cosa è realmente nascosto dietro il metodo statico .

Nell'ultimo decennio ho trascorso la programmazione, c'è stato solo un caso in cui ho assistito a uno sforzo per modificare la logica di registrazione (che è stata poi scritta come singleton). Quindi, anche se adoro le iniezioni di dipendenza, la registrazione non è davvero una grande preoccupazione. Cache, d'altra parte, farei sicuramente sempre una dipendenza.

    
risposta data 13.05.2016 - 21:37
fonte
3

Sì e No, ma soprattutto No

Suppongo che la maggior parte della conversazione sia basata su istanze statiche o iniettate. Nessuno sta proponendo che la registrazione interrompa l'SRP presumo? Parliamo principalmente del "principio dell'inversione di dipendenza". Per lo più sono d'accordo con la risposta di Telastyn.

Quando va bene usare la statica? Perché chiaramente a volte va bene. Il sì risponde al punto dei benefici dell'astrazione e le risposte "no" indicano che sono qualcosa per cui paghi. Uno dei motivi per cui il tuo lavoro è difficile è che non esiste una sola risposta che puoi scrivere e applicare a tutte le situazioni.

Prendere: Convert.ToInt32("1")

Lo preferisco a:

private readonly IConverter _converter;

public MyClass(IConverter converter)
{
   Guard.NotNull(converter)
   _converter = conveter
}

.... 
var foo = _converter.ToInt32("1");

Perché? Accetto che avrò bisogno di rifattorizzare il codice se ho bisogno della flessibilità per scambiare il codice di conversione. Accetto che non sarò in grado di deriderlo. Credo che la semplicità e la durezza valgano questo scambio.

Guardando l'altra estremità dello spettro, se IConverter fosse un SqlConnection , sarei piuttosto inorridito nel vederlo come una chiamata statica. I motivi per cui sono falliti sono ovvi. Sottolineo che un SQLConnection può essere abbastanza "trasversale" in una applciation, quindi non avrei usato quelle parole esatte.

La registrazione è più simile a SQLConnection o Convert.ToInt32 ? Direi più come "SQLConnection".

Dovresti fare il ladro con la registrazione . Parla al mondo esterno. Quando scrivo un metodo usando Convert.ToIn32 , lo sto usando come uno strumento per calcolare un altro output separatamente testabile della classe. Non ho bisogno di controllare che Convert sia stato chiamato correttamente quando si verifica che "1" + "2" == "3". La registrazione è diversa, è un risultato completamente indipendente della classe. Suppongo che sia un risultato che ha valore per te, il team di supporto e il business. La tua classe non funziona se la registrazione non è corretta, quindi i test unitari non devono passare. Dovresti testare i log della tua lezione. Penso che questo sia l'argomento killer, potrei davvero fermarmi qui.

Penso anche che sia qualcosa che probabilmente cambierà. Una buona registrazione non si limita a stampare stringhe, è una vista di ciò che l'applicazione sta facendo (sono un grande fan della registrazione basata sugli eventi). Ho visto la registrazione di base trasformarsi in un'interfaccia utente di reporting piuttosto elaborata. Ovviamente è molto più facile andare in questa direzione se il tuo log sembra _logger.Log(new ApplicationStartingEvent()) e meno come Logger.Log("Application has started") . Si potrebbe obiettare che questo sta creando un inventario per un futuro che potrebbe non accadere mai, questo è un giudizio e io penso che ne valga la pena.

In effetti, in un mio progetto personale, ho creato un'interfaccia utente non di registrazione utilizzando esclusivamente _logger per capire cosa stava facendo l'applicazione. Ciò significava che non dovevo scrivere codice per capire cosa stava facendo l'applicazione, e ho finito con il logging solido. Mi sento come se la mia attitudine al disboscamento fosse che è semplice e immutabile, che l'idea non mi sarebbe venuta.

Quindi sono d'accordo con Telastyn per il caso di registrazione.

    
risposta data 14.05.2016 - 19:41
fonte
3

Le prime preoccupazioni trasversali non sono elementi costitutivi fondamentali e non devono essere considerate come dipendenze in un sistema. Un sistema dovrebbe funzionare se per es. Il logger non è inizializzato o la cache non funziona. Come farai sistema meno abbinato e coesivo? È qui che SOLID entra in scena nella progettazione di sistemi OO.

Mantenere l'oggetto come singleton non ha nulla a che fare con SOLID. Questo è il ciclo di vita dell'oggetto per quanto tempo vuoi che l'oggetto viva nella memoria.

Una classe che ha bisogno di una dipendenza da inizializzare non dovrebbe sapere se l'istanza di classe fornita è singleton o transitoria. Ma tldr; se stai scrivendo Logger.Instance.Log () in ogni metodo o classe, allora è un codice problematico (code smell / hard coupling) veramente disordinato. Questo è il momento in cui le persone iniziano ad abusare di SOLID. E i colleghi sviluppatori come OP iniziano a fare domande autentiche come questa.

    
risposta data 15.05.2016 - 13:31
fonte
2

Ho risolto questo problema utilizzando una combinazione di ereditarietà e tratti (chiamati anche mixin in alcune lingue). I tratti sono molto utili per risolvere questo tipo di preoccupazione trasversale. Di solito è una funzionalità linguistica, quindi penso che la vera risposta sia che dipende dalle caratteristiche della lingua.

    
risposta data 13.05.2016 - 16:24
fonte

Leggi altre domande sui tag