Progettazione dell'API con riferimenti all'oggetto radice

0

[Normalmente pubblico su StackOverflow ma dal momento che questa è più una domanda di progettazione / teoria piuttosto che una domanda di codice, darò un'occhiata qui]

La maggior parte delle mie applicazioni attualmente utilizza un modello di oggetti di base che ho scritto originariamente 6 anni fa e che è cresciuto al bisogno, come con la maggior parte delle mie cose, è stato codificato così com'è, senza pensare prima a un progetto. Parte di essa è troppo rigida, una parte è mal progettata e ancora più è goffo. Così ho deciso di ricominciare da zero piuttosto che provare ad aggiornare una nuova API su quella esistente. E ho anche deciso di mappare prima l'intera API in anticipo, quindi scrivere il codice per esso.

Fin qui l'approccio sta dando i suoi frutti e suppongo di avere un buon modello di birrificazione. Tuttavia, ho un dilemma nel nuovo design. Se si considerano i modelli di automazione offerti da cose come Microsoft Word, la maggior parte degli oggetti primari ha una proprietà denominata Application che punta a un oggetto principale. Il mio modello di oggetti esistente segue praticamente lo stesso principio e lo sto usando anche nel nuovo.

L'API corrente ha tuttavia un mix di approcci. Alcuni oggetti memorizzano un riferimento a tale oggetto radice. Altri no, e la proprietà semplicemente guarda un oggetto "servizio" singleton, che nel suo cuore ha un ServiceContainer per cercare oggetti registrati. Sì, un localizzatore di servizi. Alcuni lo fanno passare attraverso un costruttore, altri lo cercano quindi memorizzano un riferimento. Normalmente mi piace la coerenza, ma questo è un casino.

Il mio nuovo design non sta cambiando questo approccio - gli oggetti avranno ancora una proprietà indietro dell'oggetto radice con l'idea che tutti gli oggetti si comportano allo stesso modo e non devo (manualmente) andare a cercare un'applicazione quando scrivere il codice per una determinata implementazione.

Con quelle informazioni di base fuori mano, ecco la mia domanda. Ogni oggetto dovrebbe contenere un riferimento all'oggetto radice, o non dovrebbe memorizzare un riferimento diretto, ma cercare in qualche modo, cioè da un singleton o da un localizzatore di servizi.

Con il primo approccio, userò più memoria, 4 byte extra per oggetto per oggetto, mentre al momento ho compilato tutto come 32 bit. Anche se questo non suona molto e sono sicuro che non creerò molte migliaia di oggetti, preferisco che l'API di base sia il più efficiente possibile, senza dubbio le app su cui sono incollato vincono essere! Questo approccio significa anche che dovrò passare il riferimento a quell'oggetto in ogni singolo costruttore, non un grosso problema per il codice reale, ma rendere i test di scrittura un po 'più complicati.

Quest'ultimo approccio significa che risparmio la memoria, ma poi continuo a usare la cattiva pratica di un localizzatore di servizi, e suppongo che ci sia almeno un accenno a un problema di prestazioni dato che la ricerca di un riferimento non è sufficiente essere veloce come restituire uno diretto.

Oppure, esiste un altro approccio che usano modelli di oggetti di grandi dimensioni che non ho considerato sopra? Come accennato, normalmente mi tuffo, faccio qualcosa che funziona e poi lo lascio da solo fino a quando non si rompe o ne ho bisogno per fare qualcos'altro.

Immagino che la risposta sarà "conserva solo il riferimento e smetti di lamentarti" ma è meglio avere un'idea delle opinioni di altre persone!

    
posta Richard Moss 04.01.2014 - 17:12
fonte

4 risposte

1

Gli oggetti core hanno una strong tendenza a diventare "oggetti di Dio" nel tempo, quindi la prima cosa che vorrei chiedere è "ho davvero bisogno di quella cosa?". I modelli di oggetti di Word o Excel riflettono la moda nella modellazione di oggetti ~ 15 anni fa, quando quei modelli sono stati creati. Ad esempio, su .NET Framework e, ad esempio, su WinForms, non esiste un oggetto Application, solo una "Application" di classe statica con una manciata di metodi, e in programmi reali è necessario accedere raramente a tale classe.

Ma se sei convinto di aver davvero bisogno di un qualche tipo di oggetto "Application" o "core", ti direi "usa il localizzatore di servizio in modo coerente e smetti di lamentarti dei problemi di prestazioni che non hai". Il localizzatore di servizi è a volte visto come un pattern anti perché potrebbe introdurre dipendenze globali nascoste, ma se si introduce la dipendenza globale da un "oggetto principale" attraverso un localizzatore di servizi o da un riferimento in ognuno dei propri oggetti principali, in realtà non fa un differenza, lo considererei "ugualmente cattivo".

Ti suggerisco di leggere la parte "Svantaggi" del articolo di Wikipedia su Service Locator , e pensa un po 'quale di questi punti si applicano davvero alla tua situazione, quando accedi al tuo "oggetto principale" attraverso un tale meccanismo, e quali di questi punti sono causati dal fatto che tu possiedi un "oggetto principale".

    
risposta data 04.01.2014 - 19:41
fonte
0

Should each object hold a reference to the root object, or should it not store a direct reference, but look it up somehow, ie from a singleton or a service locator.

In un mondo ideale, nessuno dei tuoi figli dovrebbe avere qualsiasi conoscenza della cosa che li contiene. Specificamente, così facendo, hai eliminato ogni speranza di riutilizzo in un'applicazione diversa. Peggio ancora, questi oggetti radice spesso consentono di ottenere dai loro figli, fornendo un buon gateway per i tuoi figli non solo per dipendere da questo oggetto principale, ma anche per i loro parenti.

Un buon design si basa sul concetto che le cose devono solo sapere di cosa hanno bisogno per fare la loro unica responsabilità. I tuoi componenti hanno bisogno dell'intero oggetto radice di Dio per funzionare? Ne dubito sinceramente.

Invece, questi componenti dichiarano ciò di cui hanno bisogno e lascia che il codice che li costruisce / li assembla / li usi fornisca la cosa che soddisfa quel bisogno. In questo modo, si ottengono tutti i vantaggi del corretto disaccoppiamento - testabilità poiché i test possono passare a implementazioni diverse, manutenibilità poiché il componente si preoccupa solo di sé e le modifiche ad altre cose non influiscono su di esso, e flessibilità poiché i diversi utenti possono modificare l'oggetto in modo specifico ai loro bisogni.

    
risposta data 04.01.2014 - 19:48
fonte
0

Ciò che hai detto nel tuo commento ha una certa importanza. Forse potresti progettare una sorta di gerarchia. Invece di fare riferimento all'applicazione, fare riferimento a una specie di oggetto genitore nella gerarchia. L'applicazione può avere finestre / frame dell'interfaccia utente, quelli con controlli. L'app può avere documenti, anche quelli con alcuni figli, ecc. Forse potresti creare qualcosa di molto più specifico di tutto ciò che fa riferimento all'oggetto radice dell'applicazione.

Anch'io sono d'accordo con Telastyn. L'oggetto dovrebbe avere riferimento al suo genitore se utilizza questo genitore in qualche modo. Non aggiungere ciecamente riferimenti genitoriali a tutti gli oggetti, anche se non li usa.

Inoltre, se stai pensando al consumo di memoria e al conteggio dei byte, allora .NET non è una buona scelta.

Modifica: dopo aver letto i tuoi commenti, ci sono alcune cose a cui vorrei rispondere:

Informazioni sulla coerenza del traversal. Solo perché l'oggetto ha riferimento al suo genitore non significa che debba esporlo. Di solito, tali informazioni sono dettagli di implementazione. Quindi dall'esterno, tutti gli oggetti sono uguali. Dall'interno dall'altra parte, c'è un problema con la tua mentalità. Dovresti pensare all'oggetto in un isolamento quasi completo. Pensando che "quando l'oggetto X ha Y, allora tutti gli oggetti dovrebbero avere Y, perché è coerente" non è buono, perché aumenta l'accoppiamento e diminuisce la coesione.

"Cosa c'è di veramente sbagliato nel chiedere all'applicazione cosa sia il documento attivo e quindi fare qualcosa con detto documento?" Perché non esprime chiaramente l'intento della classe, che richiede dati che cambiano. Se ad esempio hai usato gli eventi e c'era un evento DocumentChanged , che la classe si iscrive e viene spinto il nuovo documento, allora questo indica chiaramente l'intento della classe.

Infine, riguardo al tuo commento sulle cose tecniche contro i modelli. I modelli e i principi di progettazione sono chiaramente elementi tecnici. Sono un modo per strutturare il codice in un modo che esprima chiaramente le proprie intenzioni e consenta una facile manutenzione e modifica. Se non fossero menzionati in nessuno di quei libri, allora quei libri sono probabilmente piuttosto cattivi. Inoltre, ti consiglio di leggere: Cosa succede se non lo userò Modelli di progettazione software?

E un po 'di nitpick: il vero ingegnere conosce molti strumenti, i loro punti di forza e di debolezza e sa quando usarne uno rispetto all'altro. Dire che stai bene lavorando solo con uno ti fa sembrare una stupida scimmia di codice invece di un ingegnere.

    
risposta data 04.01.2014 - 21:56
fonte
0

Puoi provare ad abolire il Application altogheter, il che significa che ora alcuni dei tuoi oggetti non potrebbero svolgere il loro lavoro, non avendo alcun modo di raggiungere i loro collaboratori.

Quindi, inizi a passare solo gli oggetti realmente necessari per il tuo lavoro. Supponiamo che, usando il tuo esempio di plugin "Incolla" ( nel commento qui ), abbia bisogno di un documento in ordine fare il suo lavoro, così si passa un documento.

Penso che questo si riferisca alla Legge di Demetra , chiedi, non alla cosa su cui troverai la cosa che vuoi , cioè dipende solo dalle cose molto legato alla tua singola responsabilità.

Quando passi l'applicazione al tuo plugin, stai unendo le cose inutilmente insieme. Se per qualche motivo vorresti cambiare il meccanismo di scoperta del documento attivo, rischi di rompere alcuni plug-in. Ora, se hai centinaia di plugin a seconda di cose del genere, finisci col perdere una grande quantità di tempo per sistemarli, o non li cambi, il che significa che non puoi refactoring, e il codebase potrebbe marcire con soluzioni alternative.

Ovviamente, nell'esempio del tuo plugin forse hai bisogno di altre cose, e forse avresti bisogno di un qualche tipo di uniformità. Tuttavia, non è necessario passare l'intera applicazione, è possibile definire un'interfaccia con solo i plug-in di elementi (o un determinato tipo di plug-in) e quindi utilizzarla ( Principio di segregazione dell'interfaccia ).

Diciamo che la tua applicazione ha questo metodo:

interface Application {
    void quit();
    void openSaveAsDialog();
    Document getActiveDocument();
}

Se i tuoi plugin non sono concepiti per utilizzare quit o openSaveAsDialog , puoi definire qualcosa come:

interface Benchwork {
    Document getActiveDocument();
}

Per i plugin, puoi ancora implementare Benchwork in termini di chiamate Application , ma puoi probabilmente rompere Application in diversi pezzi più piccoli (diversi sottosistemi nidificati, come un banco, contenente un display, contenente viste, uno dei quali è come un editor che si occupa di documenti, composto da elementi (immagini / paragrafi / allegati), ecc. e ha Application per essere una cosa veramente di alto livello.

Non sono un architetto io stesso, ma il lato negativo che posso vedere, è che probabilmente finirai con molte più classi e interfacce nel tuo progetto, ma dal momento che rendono le cose più gestibili, non sono sicuro che sia davvero un problema.

Puoi controllare i talk di Google Clean Code (che è per lo più sulle dipendenze e su come renderle verificabili).

    
risposta data 05.01.2014 - 13:56
fonte

Leggi altre domande sui tag