Ai miei colleghi piace dire "logging / caching / etc. È una preoccupazione trasversale" e quindi procedere usando il singleton corrispondente ovunque. Tuttavia amano IoC e DI.
È davvero una scusa valida per violare il principio SOLI D ?
Ai miei colleghi piace dire "logging / caching / etc. È una preoccupazione trasversale" e quindi procedere usando il singleton corrispondente ovunque. Tuttavia amano IoC e DI.
È davvero una scusa valida per violare il principio SOLI D ?
Risposte:
No.
SOLID esiste come linee guida per tenere conto dell'inevitabile cambiamento. Stai veramente mai andare a cambiare la vostra libreria di registrazione, o di destinazione, o filtrando, o la formattazione, o ...? Stai davvero non ha intenzione di cambiare la vostra libreria di caching, o di destinazione, o di una strategia, o di scoping, o ...?
Certo che lo sei. Per lo meno , vorrai deridere queste cose in modo sano per isolarle per i test. E se vuoi isolarli per i test, probabilmente ti imbatterai in un motivo commerciale in cui li vuoi isolare per motivi di vita reale.
E otterrai quindi l'argomento secondo cui il logger stesso gestirà la modifica. "Oh, se il target / il filtro / la formattazione / la strategia cambiano, cambieremo semplicemente la configurazione!" Questa è spazzatura. Non solo hai un oggetto God che gestisce tutte queste cose, stai scrivendo il tuo codice in XML (o simile) in cui non ottieni analisi statiche, non ricevi errori di compilazione e non ottengo davvero test unitari efficaci.
Esistono casi per violare le linee guida SOLID? Assolutamente. A volte le cose non cambieranno (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 la registrazione e la memorizzazione nella cache (e altre onnipresenti preoccupazioni trasversali) non sono questi casi. Di solito sono ottimi esempi dei problemi di accoppiamento e di progettazione che si verificano quando si ignorano le linee guida.
String
o Int32
o addirittura List
fuori dal tuo modulo. C'è solo una misura in cui è ragionevole e ragionevole pianificare un cambiamento. E, al di là dei tipi "core" per lo più ovvi, discernere cosa potresti probabilmente cambiare è davvero solo una questione di esperienza e giudizio.
sì
Questo è l'intero punto del termine "preoccupazione trasversale" - significa qualcosa che non si adatta perfettamente al principio SOLID.
È qui che l'idealismo incontra la realtà.
Le persone semi-nuove di SOLID e trasversali spesso affrontano questa sfida mentale. Va bene, non andare fuori di testa. Cerca di mettere tutto in termini di SOLID, ma ci sono alcuni posti come la registrazione e la memorizzazione nella cache in cui SOLID non ha senso. Il taglio trasversale è il fratello di SOLID, vanno di pari passo.
HttpContextBase
(che è stata introdotta proprio per questo motivo). So per certo che la mia realtà sarebbe davvero acida senza questa classe.
Per il logging penso che lo sia. La registrazione è pervasiva e generalmente non correlata alla funzionalità del servizio. È comune e ben compreso l'utilizzo dei modelli singleton del framework di registrazione. In caso contrario, stai creando e iniettando logger ovunque e non lo desideri.
Un problema di cui sopra è che qualcuno dirà "ma come posso testare la registrazione?" . Il mio pensiero è che normalmente non collaudo la registrazione, oltre ad affermare che posso effettivamente leggere i file di registro e comprenderli. Quando ho visto il logging testato, 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 di gran lunga registrare un ascoltatore / osservatore su quella classe e affermare nei miei test che viene chiamato. È quindi possibile inserire la registrazione degli eventi all'interno di tale osservatore.
Penso che la memorizzazione nella cache sia uno scenario completamente diverso, tuttavia.
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ù elevato. Quindi, con una comprensione adeguatamente condizionata, alcune apparenti violazioni potrebbero non essere vere e proprie violazioni dello "spirito" o del "corpus di principi nel suo insieme".
I principi SOLID in particolare, oltre a richiedere molte sfumature, sono in definitiva subordinati all'obiettivo di "fornire software funzionante e gestibile". Pertanto, aderire a un particolare principio SOLID è autolesionista e contraddittorio quando lo fa è in conflitto con gli obiettivi di SOLID. E qui, noto spesso che offrire la manutenibilità di briscole .
Quindi, per quanto riguarda la D in SOLID ? Bene, contribuisce ad aumentare la manutenibilità rendendo il tuo modulo riutilizzabile 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, serie di classi e programmi.
E sì, cambiare l'implementazione del logger probabilmente mette il tuo modulo in un "altro contesto distinto".
Quindi, lasciami offrire i miei due grandi avvertimenti :
In primo luogo: tracciare 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 dispone attualmente prevede di utilizzare un modulo in un altro contesto, è probabilmente OK per esso dipenda impotente su di esso. L'avvertimento al avvertimento: I vostri piani sono probabilmente sbagliato - ma questo è anche OK. Più a lungo scrivi un modulo dopo l'altro, più sempre più intuitivo e preciso sarà il tuo senso se "un giorno ne avrò bisogno di nuovo". Ma probabilmente non sarai mai in grado di dire in modo retrospettivo: "Ho modulare e disaccoppiato tutto nella massima misura possibile, ma senza eccessi ".
Se ti senti in colpa per i tuoi errori di giudizio, vai alla confessione e vai avanti ...
Secondo: invertire il controllo non equivale a iniettare dipendenze .
Ciò è particolarmente vero quando si inizia a iniettare dipendenze e nausea . L'iniezione di dipendenza è una tattica utile per la strategia IoC globale. Ma direi che DI è di minore efficacia rispetto ad altre tattiche - come usare interfacce e adattatori - singoli punti di esposizione al contesto dall'interno del modulo.
E concentriamoci davvero su questo per un secondo. Perché, anche se si inietta un Logger
annuncio nauseam , è necessario scrivere codice Logger
sull'interfaccia. Non è possibile iniziare a utilizzare un nuovo Logger
di un fornitore diverso che accetta parametri in un ordine diverso. Tale capacità deriva dalla codifica, all'interno del modulo, rispetto a un'interfaccia esistente all'interno del modulo e che contiene un singolo sottomodulo (Adapter) per gestire la dipendenza.
E se stai codificando un adattatore, che Logger
sia iniettato in quell'adattatore o scoperto dall'adattatore è generalmente dannatamente insignificante rispetto all'obiettivo generale di manutenibilità. E, soprattutto, se si dispone di un adattatore a livello di modulo, è probabilmente assurdo iniettarlo in qualsiasi cosa. È scritto per il modulo.
tl; dr - Smetti di preoccuparti dei principi senza considerare il motivo per cui stai usando i principi. E, più praticamente, crea un Adapter
modulo per ogni modulo. Usa il tuo giudizio quando decidi dove disegnare i confini del "modulo". Dall'interno di ciascun modulo, andare avanti e fare riferimento direttamente a Adapter
. E sicuramente, iniettare il vero logger nel Adapter
- ma non in ogni piccola cosa che potrebbe averne bisogno.
L'idea che il logging debba sempre essere implementato come singleton è una di quelle bugie a cui è stato detto così spesso che ha guadagnato trazione.
Fino a quando i moderni sistemi operativi sono stati informati, è stato riconosciuto che potresti voler 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 alla cieca in nuove. Se non svolgono tale diligenza, non stanno facendo il loro lavoro.
La registrazione è davvero un caso speciale.
@Telastyn scrive:
Non hai mai intenzione di cambiare la tua libreria di registrazione, o destinazione, filtro, formattazione o ...?
Se si prevede che potrebbe essere necessario modificare la libreria di registrazione, è necessario utilizzare una facciata; cioè SLF4J se sei nel mondo Java.
Per il resto, una libreria di registrazione decente si occupa di cambiare dove va la registrazione, quali eventi vengono filtrati, come gli eventi di registro vengono formattati usando i file di configurazione del logger e (se necessario) le classi di plugin personalizzate. Esistono numerose alternative standard.
In breve, questi sono problemi risolti ... per la registrazione ... e quindi non è necessario utilizzare Dependency Injection per risolverli.
L'unico caso in cui DI potrebbe essere utile (rispetto agli approcci di registrazione standard) è se si desidera sottoporre la registrazione dell'applicazione a test di unità. Tuttavia, sospetto che la maggior parte degli sviluppatori direbbe che la registrazione non fa parte della funzionalità di una classe e non è qualcosa che deve essere testato.
@Telastyn scrive:
E otterrai quindi l'argomento secondo cui il logger stesso gestirà la modifica. "Oh, se il target / il filtro / la formattazione / la strategia cambiano, cambieremo semplicemente la configurazione!" Questa è spazzatura. Non solo hai un oggetto God che gestisce tutte queste cose, stai scrivendo il tuo codice in XML (o simile) in cui non ottieni analisi statiche, non ricevi errori di compilazione e non ottengo davvero test unitari efficaci.
Temo che sia un ripost molto teorico. In pratica, alla maggior parte degli sviluppatori e degli integratori di sistema piace il fatto che sia possibile configurare la registrazione tramite un file di configurazione. E gli piace il fatto che non si prevede che testeranno l'unità la registrazione di un modulo.
Certo, se si riempiono le configurazioni di registrazione, è possibile che si verifichino problemi, ma si manifesteranno come l'applicazione non riuscita durante l'avvio o una registrazione troppo / troppo scarsa. 1) Questi problemi possono essere facilmente risolti correggendo l'errore nel file di configurazione. 2) L'alternativa è un ciclo completo di compilazione / analisi / test / distribuzione ogni volta che si modifica i livelli di registrazione. Non è accettabile
Sì e no !
Sì: penso che sia ragionevole che sottosistemi diversi (o livelli o librerie semantici o altre nozioni di raggruppamento modulare) accettino ciascuno (lo stesso o) logger potenzialmente diverso durante la loro inizializzazione piuttosto che tutti i sottosistemi che fanno affidamento sullo stesso singleton condiviso comune .
Tuttavia,
No: è allo stesso tempo irragionevole parametrizzare la registrazione in ogni piccolo oggetto (per metodo di costruzione o istanza). Per evitare un gonfiamento inutile e inutile, le entità più piccole dovrebbero usare il logger singleton del loro contesto racchiuso.
Questo è uno dei tanti motivi per pensare 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 all'interno dei confini modulari rispetto a quando li attraversiamo.
Innanzitutto inizia con una forte cache singleton, le prossime cose che vedi sono forti singleton per il livello di database che introducono stato globale, API non descrittive di class
es e codice non verificabile.
Se decidi di non avere un singleton per un database, probabilmente non è una buona idea avere un singleton per una cache, dopo tutto, 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 ne ha, teoricamente , un numero infinito, 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 ad uno sforzo per cambiare la logica di registrazione (che è stata poi scritta come singleton). Quindi, anche se amo le iniezioni di dipendenza, il disboscamento non è davvero una preoccupazione così grande. La cache, d'altra parte, farei sicuramente sempre una dipendenza.
Sì e No, ma principalmente No
Suppongo che la maggior parte della conversazione si basi sull'istanza statica vs iniettata. Nessuno propone che la registrazione rompa l'SRP che presumo? Parliamo principalmente del "principio di inversione di dipendenza". Tbh concordo principalmente con la non risposta di Telastyn.
Quando va bene usare la statica? Perché chiaramente a volte va bene. Le risposte Sì indicano i vantaggi 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 risposta che puoi scrivere e applicare a tutte le situazioni.
Prendere:
Convert.ToInt32("1")
Preferisco questo a:
private readonly IConverter _converter;
public MyClass(IConverter converter)
{
Guard.NotNull(converter)
_converter = conveter
}
....
var foo = _converter.ToInt32("1");
Perché? Sto accettando che avrò bisogno di refactoring del codice se ho bisogno della flessibilità per scambiare il codice di conversione. Sto accettando che non sarò in grado di deridere questo. Credo che la semplicità e la tenesseness valgano questo commercio.
Osservando l'altra estremità dello spettro, se IConverter
fosse un SqlConnection
, sarei abbastanza inorridito nel vederlo come una chiamata statica. I motivi per cui sono falliti ovvi. Vorrei sottolineare che a SQLConnection
può essere abbastanza "trasversale" in un applciation, quindi non avrei usato quelle parole esatte.
La registrazione è più simile a un SQLConnection
o Convert.ToInt32
? Direi più come 'SQLConnection`.
Dovresti prendere in giro la registrazione . Parla con il mondo esterno. Quando scrivo un metodo usando Convert.ToIn32
, lo sto usando come uno strumento per calcolare qualche altro output testabile separatamente della classe. Non è necessario verificare che sia Convert
stato chiamato correttamente quando si verifica che "1" + "2" == "3". La registrazione è diversa, è un output completamente indipendente della classe. Suppongo 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 dovrebbero superare. Dovresti testare ciò che registra la tua classe. Penso che questo sia l'argomento killer, potrei davvero fermarmi qui.
Penso anche che sia qualcosa che molto probabilmente cambierà. Una buona registrazione non solo stampa stringhe, è una visione di ciò che l'applicazione sta facendo (sono un grande fan della registrazione basata su eventi). Ho visto la registrazione di base trasformarsi in UI di reporting piuttosto elaborate. Ovviamente è molto più facile andare in questa direzione se la tua registrazione sembra _logger.Log(new ApplicationStartingEvent())
e meno simile Logger.Log("Application has started")
. Si potrebbe sostenere che questo sta creando inventario per un futuro che potrebbe non accadere mai, questa è una richiesta di giudizio e penso che ne valga la pena.
In effetti, in un mio progetto personale, ho creato un'interfaccia utente senza registrazione puramente usando il _logger
per capire cosa stava facendo l'applicazione. Ciò significava che non dovevo scrivere codice per capire cosa stesse facendo l'applicazione e alla fine ho ottenuto un registro solido. Mi sento come se il mio atteggiamento nei confronti del disboscamento fosse semplice e immutabile, quell'idea non mi sarebbe venuta in mente.
Quindi concordo con Telastyn per il caso della registrazione.
Semantic Logging Application Block
. Non usarlo, come la maggior parte del codice creato dal team "pattern and practice" di MS, ironicamente, è un antipattern.
In primo luogo le preoccupazioni trasversali non sono elementi costitutivi importanti e non dovrebbero essere trattate come dipendenze in un sistema. Un sistema dovrebbe funzionare se, ad esempio, Logger non è inizializzato o la cache non funziona. Come renderai il sistema meno accoppiato e coeso? Ecco dove SOLID entra in scena nella progettazione del sistema OO.
Mantenere l'oggetto come singleton non ha nulla a che fare con SOLID. Questo è il tuo ciclo di vita dell'oggetto per quanto tempo vuoi che l'oggetto viva nella memoria.
Una classe che necessita di una dipendenza per l'inizializzazione non dovrebbe sapere se l'istanza della classe fornita è singola o temporanea. Ma tldr; se stai scrivendo Logger.Instance.Log () in ogni metodo o classe, allora è un codice problematico (odore di codice / accoppiamento duro) davvero disordinato. Questo è il momento in cui le persone iniziano ad abusare di SOLID. E altri sviluppatori come OP iniziano a fare domande autentiche come questa.
Ho risolto questo problema usando una combinazione di ereditarietà e tratti (chiamati anche mixin in alcune lingue). I tratti sono super utili per risolvere questo tipo di preoccupazione trasversale. Di solito è una caratteristica della lingua, quindi penso che la vera risposta sia che dipende dalle caratteristiche della lingua.