Information Hiding v.s. Tipo statico di sicurezza

7

Sto lavorando a un progetto con altri e discutiamo sulla protezione delle informazioni e sulla sicurezza del tipo statico. Il nostro scenario è descritto di seguito.

Lingua : C ++ 11

Scenario : vogliamo creare una struttura ad albero. Ogni albero ha una propria classe, che sono tutti sottotipi di classe base NodeType . Ogni sottotipo ha la propria regola per collegare altri nodi. Ad esempio, NodeTypeA , NodeTypeB e NodeTypeC sono sottoclassi di NodeType e

  • NodeTypeA può avere solo NodeTypeB come primo figlio, NodeTypeA come secondo figlio.
  • NodeTypeB può avere solo NodeTypeB come childron. (Può avere qualsiasi numero di childron)
  • NodeTypeC può avere solo NodeTypeA come primo figlio, NodeTypeB come secondo figlio.

L'esempio potrebbe avere un problema di ricorsione, ma per l'illustrazione va bene.

Ora esiste una classe Factory per creare ciascun nodo:

  • createNodeTypeA
  • createNodeTypeB
  • createNodeTypeC

Esiste una classe, Builder , che vuole convertire l'input di testo dell'utente nell'albero.

Builder non è necessario conoscere le informazioni sul tipo di ogni nodo. Porta solo i puntatori ottenuti da Factory e li passa a un altro metodo di Factory . Nella prospettiva di Builder , il tipo di tutti i nodi può essere la classe base NodeType .

Dilemma :

Se valutiamo Nascondere le informazioni di più, il metodo di Factory dovrebbe essere:

  • NodeType *createNodeTypeA(NodeType *first, NodeType *second)
  • NodeType *createNodeTypeB(std::vector<NodeType *> children)
  • NodeType *createNodeTypeC(NodeType *first, NodeType *second)

e per fornire correttezza, dobbiamo fornire un controllo run-time su ogni parametro nel metodo Factory.

Se valutiamo Sicurezza statica di tipo di più, il metodo di Factory dovrebbe essere:

  • NodeTypeA *createNodeTypeA(NodeTypeB *first, NodeTypeA *second)
  • NodeTypeB *createNodeTypeB(std::vector<NodeTypeB *> childron)
  • NodeTypeC *createNodeTypeC(NodeTypeA *first, NodeTypeB *second)

e Builder devono conoscere i sottotipi di ciascun nodo. Ciò che otteniamo è la sicurezza del tipo.

La figura seguente illustra i due modi:

Discussione:

FavorisciNascondereleinformazioni:

  • L'informazionechesinascondeèimportante.GlisviluppatoridiBuilderpossonolavoraresenzaconoscenzadeisottotipidinodi.
  • Siamoingradodicompensarelaperditadisicurezzaditipostaticomedianterevisionedelcodice,testestrumentodianalisiautomatica.Cisiaspettacheunteamqualificatocompletiabbastanzabene.

FavorisciSicurezzadeltipostatico:

Domanda :

Quale può essere una buona pratica? Apprezziamo di più l'impatto a lungo termine.

    
posta Leo Jacob 13.10.2018 - 22:46
fonte

5 risposte

4
NodeType *createNodeTypeA(NodeType *first, NodeType *second)
NodeType *createNodeTypeB(std::vector<NodeType *> children)
NodeType *createNodeTypeC(NodeType *first, NodeType *second)

Questo non "nasconde le informazioni", Builder sa ancora quale fabbrica chiama e quali dati dovrebbe passare ad essa. Quindi, l'uso del tipo dinamico non sembra dare alcun vantaggio in questo caso

Cosa si potrebbe fare per rendere il Builder praticamente privo di tipo di nodo, i couls utilizzano un'unica fabbrica:

NodeType *createNode(std::vector<NodeType*> children)

Questo non consente di specificare il tipo di nodo. Questo è spesso il caso della costruzione, e in generale pone una domanda: è in linea di principio possibile rimuovere le informazioni sul tipo di nodo dal Builder. Forse il disaccoppiamento dovrebbe piuttosto essere sul lato di preparazione / pre-elaborazione.

    
risposta data 14.10.2018 - 08:25
fonte
4

Favor Information Hiding:

  • Information hiding itself is important. Developers of Builder can work without knowledge of sub-types of nodes.

Questo non è corretto, perché gli argomenti passati ai metodi factory devono essere di un certo tipo (runtime). Pertanto, il tuo programma deve essere a conoscenza dei sottotipi durante la costruzione dell'albero, indipendentemente dal fatto che le relazioni siano controllate in fase di compilazione o di esecuzione.

  • We can compensate the loss of static type safety by code review, testing and automatic analyzing tool. A qualified team is expected to complete those pretty well.

Oppure puoi risparmiare un sacco di lavoro lasciando che il compilatore faccia il suo lavoro.

Il vero problema sembra essere che la tua interfaccia sia mal progettata. Diversi sottotipi richiedono conoscenza reciproca, quindi non sono indipendenti e non sono intercambiabili. Chiediti quanta estensibilità offre effettivamente il tuo design e quanto effettivamente ti serve (se non riesci a trovare un caso d'uso che potrebbe realisticamente verificarsi nel prossimo futuro, non ne hai bisogno).

Ho il sospetto che la causa di questo errore di progettazione sia il fatto che si sta rappresentando la struttura dei dati dell'albero in modo intrusivo, vale a dire l'archiviazione della relazione tra i nodi come dati dei nodi stessi. L'unica distinzione tra i sottotipi che hai menzionato sono le diverse relazioni che hanno con gli altri nodi. Pertanto, se si separano queste relazioni dalla rappresentazione dei nodi, si può finire senza la necessità di nodi di sottotitoli o, almeno, essere in grado di fornire una migliore astrazione.

La difficoltà di tale cambiamento è che la funzionalità che utilizza le relazioni tra i nodi deve essere riformulata come un algoritmo grafico (di ricerca). Questo spesso richiede un cambio di mentalità. L'utilizzo della Boost Graph Library ti aiuterà con questo e fornisce la struttura dei dati e algoritmi in un modo che rende facile separare la rappresentazione ad albero, la rappresentazione e gli algoritmi dei dati nodo / bordo.

    
risposta data 14.10.2018 - 15:16
fonte
1

Penso che un po 'più di contesto sarebbe stato apprezzato :).

Lasciatemi riformulare il tuo dilemma:

  • Vuoi essere in grado di vedere un albero nel modo più astratto possibile. Vuoi essere in grado di dire: "Un albero è solo una raccolta non vuota di Nodi che ha un solo genitore (eccetto il nodo radice) che può fare tutto ciò che un albero generale può fare".
  • Vuoi essere in grado di verificare rapidamente l'invalidità dell'albero.

Un algoritmo che costruisce l'albero deve essere consapevole dei diversi tipi di nodo e costruire un albero valido. Se all'algoritmo viene fornito un flusso di nodi, dovrebbe essere in grado di dire, dopo aver controllato, quale nodo dovrebbe andare dove. Se vede un input non valido o è costretto a creare un albero non valido, interrompe semplicemente e stampa l'errore.

Quindi, definisci un Tree contenente i solo metodi che prendono un NodeType come il parametro come findChildren , getRoot , getSize , getDepth ecc. Puoi chiama il tuo Parser (algoritmo) dopo aver dato un metodo buildTree che restituisce un NodeType (che è equivalente a un albero !!!). Decideremo se un nodo è NodeTypeA , NodeTypeB e NodeTypeC osservando il payload del NodeType che può contenere enum come TypeA , TypeB ecc. Tutta la logica che distingue tra i tipi di nodo saranno nella Parser !

In conclusione, abbiamo usato qualcosa chiamato Parser (o Algorithm) che è a conoscenza dei diversi tipi di nodi per separare le preoccupazioni dall'astrazione Tree che può fare cose Tree molto generali.

    
risposta data 15.10.2018 - 05:43
fonte
1

È un confronto errato

"Nascondere informazioni" e "digitare dati statici" non sono concetti opposti e non si escludono a vicenda né in modo assoluto né relativamente.

Questa è una falsa dicotomia che si allontana dal vero problema che @DDmmr tocca la risposta - design . Quindi un commento correlato fornisce un indizio chiave: La struttura ad albero è una sintassi astratta Tree (AST)] di un semplice script

Il design manca una grammatica per lo "script semplice"

There is a class, Builder, that wants to convert user text input to the tree.

Suggerisco che questa affermazione progettuale confonde l'analisi con la costruzione di un oggetto che conduce direttamente alla domanda dell'OP. L'analisi richiede una struttura definita dello "script semplice". Questa definizione è chiamata grammatica.

Una grammatica descrive i bit fondamentali dello "script semplice", nonché le combinazioni consentite e le combinazioni di combinazioni e così via. Inevitabilmente alcuni bit (complessi) verranno mappati direttamente come Node s e Sottotipi di nodo (e proprietà potenzialmente correlate).

È importante sottolineare che tutti questi bit distinti dal fondamentale al più complesso hanno nomi, ad es. "numero", "parola chiave", "cifra", "lettera", "operatore", ecc.

Separazione delle preoccupazioni

Gli AST "nodi nominati" sono metadati, un'astrazione di oggetti concreti Node ancora da costruire. Questi metadati sono la "cucitura" che separa l'AST dall'implementazione dell'albero Node .

Il design è quindi composto da tre parti generali: una grammatica definita, l'analisi e la costruzione di un oggetto Node concreto.

Progettazione dell'implementazione del nodo

L'analisi dell'output è l'AST. Ma l'eventuale albero Node è derivato dall'AST, non è l'AST. Ma posso certamente vedere la confusione. Sembra un'astrazione artificiale ma è un punto di design importante.

Ora il Builder prenderà l'AST. Si noti che le regole della relazione Node sono già inserite nella struttura dell'AST. Pertanto, il Builder costruisce un albero Node basato esclusivamente sulla struttura AST senza una conoscenza esplicita di queste regole.

Immagino che il "semplice script" non sia un linguaggio di scripting generalista completo, quindi l'analisi è veramente semplice, ma non lasciarti convincere a violare la singola responsabilità a.k.a. Separation of Concerns.

Forse l'AST non ha bisogno di essere completamente analizzato in elementi / nodi di particelle fondamentali dello script. Forse ci sono oggetti NodeMetaData , diciamo, nell'AST che corrispondono alle proprietà di classe super di Node .

Il dilemma dell'OP è progettato lontano

"Nascondere informazioni" è intrinsecamente appropriato data la separazione delle preoccupazioni e un Builder progettato con cura. I parametri di tipo statico lasciano il posto a un oggetto AST con metadati ben definiti.

Builder e le fabbriche non prendono Node né sottotipi oggetti come argomenti. Builder controlla l'avanzamento dell'AST e le fabbriche vengono istanziate e richiamate in base ai metadati AST. Builder assembla il NodeTree dalle parti NodeTree create in fabbrica.

La costruzione dell'albero dei nodi è potenzialmente complessa, quindi studia modelli di costruzione come "Builder" e "Factory". Nota che il modello di fabbrica varia in base alla complessità.

    
risposta data 19.10.2018 - 03:09
fonte
0

Nascondere informazioni è un vantaggio esterno. Data la classe A, A ha zero benefici nel nascondere le sue informazioni. Non aiuta la classe a fare il suo lavoro e non aiuta il motivo dello sviluppatore sul codice. Aiuta gli sviluppatori esterni che non devono essere esposti a conoscenze inutili. Nascondere informazioni è un favore che la tua classe fa per le altre classi.

Il controllo statico dei tipi è un vantaggio per la tua classe e per gli sviluppatori di classe. Ne beneficia la classe perché ha bisogno di fare meno convalida. Dare la classe A e il metodo Foo, che accetta un intero e restituisce un decimale. Il metodo Foo non deve controllare se il numero intero nullo o un array o qualcos'altro, che il controllo sia eseguito in fase di compilazione o in fase di esecuzione altrove all'interno di quel metodo, sarà un numero intero. Se Foo è privato restituirà un decimale. Questo è un vantaggio per i tuoi sviluppatori.

Ora, se hai davvero bisogno di fare questo scambio non posso dire, ma si può fare a meno di nascondere le informazioni. I negativi sono esterni e quindi non direttamente il tuo problema.

Aggiornamento in risposta a un commento: immagina di avere un'implementazione procedurale di un algoritmo di ordinamento, utilizza diverse variabili globali e un paio di funzioni per restituire risultati intermedi. È assolutamente solido, veloce, efficiente, grande O. Non è il miglior obiettivo per riscrivere usando il design orientato agli oggetti, ma stai facendo il resto dell'applicazione. Se lo completi in una classe, è comunque un'implementazione solida, rapida ed efficiente e priva di bug. Nascondendone i globali (ora campi o proprietà di livello di classe) e i metodi privati non rendono più veloce o eliminano eventuali bug all'interno della classe. Quello che fa è impedire a chiunque altro di aver bisogno di imparare i dettagli di come funziona internamente per poterlo usare in sicurezza. Zero vantaggio per la nostra ipotetica classe di smistamento, enorme vantaggio per la nostra applicazione.

    
risposta data 16.10.2018 - 01:08
fonte

Leggi altre domande sui tag