Quanto sono gestite le basi di codice non OO?


27

Vedo sempre che l'astrazione è una funzione molto utile che OO fornisce per gestire la base di codice. Ma come vengono gestite le grandi basi di codice non OO? O alla fine diventano semplicemente una " grande palla di fango "?

Aggiornamento:
sembrava che tutti pensino che l'astrazione sia solo una modularizzazione o un nascondiglio di dati. Ma IMHO, significa anche l'uso di "Classi astratte" o "Interfacce", che è un must per l'iniezione di dipendenza e quindi i test. In che modo gestiscono basi di codice non OO? Inoltre, oltre all'astrazione, l'incapsulamento aiuta anche molto a gestire basi di codice di grandi dimensioni poiché definisce e limita la relazione tra dati e funzioni.

Con C, è molto possibile scrivere codice pseudo-OO. Non so molto su altre lingue non OO. Quindi, è il modo di gestire basi di codice C di grandi dimensioni?


6
In modo agnostico, descrivi un oggetto. Cos'è, come viene modificato, cosa dovrebbe ereditare e cosa dovrebbe fornire? Il kernel Linux è pieno di strutture allocate con molti helper e puntatori di funzioni, ma ciò probabilmente non soddisferebbe la definizione di oggetto orientata per la maggior parte. Tuttavia, è uno dei migliori esempi di una base di codice molto ben mantenuta. Perché? Perché ogni manutentore del sottosistema sa cosa si trova nella propria area di responsabilità.
Tim Post

In modo indipendente dalla lingua, descrivi come vedi la gestione delle basi di codice e cosa OO ha a che fare con questo.
David Thornley,

@Tim Post Sono interessato alla gestione del codice sorgente del kernel Linux. Descriveresti di più il sistema? Forse come risposta con un esempio?
Gulshan,

7
Ai vecchi tempi, usavamo collegamenti separati per mock e stub per test unitari. L'iniezione di dipendenza è solo una tecnica tra le varie. La compilazione condizionale è un'altra.
Macneil,

Penso che sia un tratto riferirsi a basi di codice di grandi dimensioni (OO o altro) come "gestito". Sarebbe bene avere una migliore definizione del termine centrale nella tua domanda.
Tottinge,

Risposte:


43

Sembra che tu pensi che OOP sia l'unico mezzo per raggiungere l'astrazione.

Mentre OOP è sicuramente molto bravo a farlo, non è affatto l'unico modo. I progetti di grandi dimensioni possono anche essere gestiti da una modularizzazione senza compromessi (basta guardare Perl o Python, entrambi eccellenti in questo, e così anche linguaggi funzionali come ML e Haskell), e usando meccanismi come i template (in C ++).


27
+1 Inoltre, è possibile scrivere una "Big Ball of Mud" usando OOP se non sai cosa stai facendo.
Larry Coleman,

Che dire delle basi di codice C?
Gulshan,

6
@Gulshan: molte basi di codice C di grandi dimensioni sono OOP. Solo perché C non ha classi non significa che OOP non possa essere raggiunto con un po 'di sforzo. Inoltre, C consente una buona modularizzazione usando le intestazioni e il linguaggio PIMPL. Non altrettanto comodo o potente dei moduli nelle lingue moderne, ma ancora una volta abbastanza buono.
Konrad Rudolph,

9
C consente la modularizzazione a livello di file. L'interfaccia va nel file .h, le funzioni disponibili pubblicamente nel file .c e le variabili e le funzioni private ottengono il staticmodificatore di accesso allegato.
David Thornley,

1
@Konrad: anche se sono d'accordo sul fatto che OOP non sia l'unico modo per farlo, credo che OP probabilmente avesse in mente strettamente C, che non è né un linguaggio funzionale né dinamico. Quindi dubito che menzionare Perl e Haskell sarà di qualche utilità per lui / lei. In realtà trovo il tuo commento più pertinente e utile per OP ( non significa che OOP non possa essere realizzato con un po 'di sforzo ); potresti considerare di aggiungerlo come una risposta separata con dettagli aggiuntivi, magari supportato da uno snippet di codice o da un paio di link. Vincerebbe almeno il mio voto, e molto probabilmente OP. :)
Groo,

11

Moduli, funzioni (esterne / interne), subroutine ...

come ha detto Konrad, OOP non è l'unico modo per gestire basi di codice di grandi dimensioni. È un dato di fatto, un sacco di software è stato scritto prima (prima di C ++ *).


* E sì, so che C ++ non è l'unico a supportare OOP, ma in qualche modo è stato quando quell'approccio ha iniziato a prendere inerzia.
Torre del

8

Il principio di modularità non si limita ai linguaggi orientati agli oggetti.


6

Realisticamente o cambiamenti rari (pensa ai calcoli della pensione della previdenza sociale) e / o conoscenze profondamente radicate perché le persone che mantengono tale sistema lo fanno da un po 'di tempo (la presa cinica è la sicurezza del lavoro).

Le soluzioni migliori sono la convalida ripetibile, con il quale intendo test automatizzati (ad esempio test unitari) e test umani che seguono passaggi vietati (ad esempio test di regressione) "anziché fare clic su e vedere cosa si rompe".

Per iniziare a muovermi verso una sorta di test automatizzato con una base di codice esistente, consiglio di leggere Michael Feather's Working Effectively with Legacy Code , che descrive in dettaglio gli approcci per portare le codebase esistenti fino a una sorta di framework di test ripetibile OO o meno. Questo porta al tipo di idee a cui altri hanno risposto come la modularizzazione, ma il libro descrive l'approccio giusto per farlo senza rompere le cose.


+1 per il libro di Michael Feather. Quando ti senti depresso per una brutta base di codice, (ri) leggila :)
Matthieu

5

Sebbene l'iniezione di dipendenza basata su interfacce o classi astratte sia un modo molto carino di fare test, non è necessaria. Non dimenticare che quasi ogni lingua ha un puntatore a funzione o un eval, che può fare tutto ciò che puoi fare con un'interfaccia o una classe astratta (il problema è che possono fare di più , incluse molte cose cattive, e che non fanno ' in sé forniscono metadati). Un tale programma può effettivamente ottenere un'iniezione di dipendenza con questi meccanismi.

Ho trovato molto utile essere rigorosi con i metadati. Nei linguaggi OO le relazioni tra bit di codice sono definite (in una certa misura) dalla struttura della classe, in un modo abbastanza standardizzato da avere cose come un'API di riflessione. In linguaggi procedurali può essere utile inventare quelli da soli.

Ho anche scoperto che la generazione di codice è molto più utile in un linguaggio procedurale (rispetto a un linguaggio orientato agli oggetti). Ciò garantisce che i metadati siano sincronizzati con il codice (poiché viene utilizzato per generarlo) e ti dà qualcosa di simile ai punti di taglio della programmazione orientata all'aspetto - un posto dove puoi iniettare il codice quando ne hai bisogno. A volte è l'unico modo per fare la programmazione DRY in un ambiente che posso capire.


3

In realtà, come hai scoperto di recente , le funzioni del primo ordine sono tutto ciò che serve per l'inversione delle dipendenze.

C supporta le funzioni del primo ordine e persino le chiusure in una certa misura . E le macro C sono una potente funzionalità per la programmazione generica, se gestite con le cure necessarie.

È tutto lì. SGLIB è un buon esempio di come C può essere usato per scrivere codice altamente riutilizzabile. E credo che ci sia molto di più là fuori.


2

Anche senza astrazione la maggior parte dei programmi è suddivisa in sezioni di qualche tipo. Quelle sezioni di solito si riferiscono a compiti o attività specifici e lavori su quelli nello stesso modo in cui lavoreresti sui bit più specifici dei programmi astratti.

Nei progetti di piccole e medie dimensioni questo è effettivamente più facile farlo con un'implementazione OO purista a volte.


2

Astrazione, classi astratte, iniezione di dipendenza, incapsulamento, interfacce e così via, non sono l'unico modo per controllare basi di codice di grandi dimensioni; questo è un modo giusto e orientato agli oggetti.

Il segreto principale è evitare di pensare a OOP quando si codifica non-OOP.

La modularità è la chiave nei linguaggi non OO. In C questo si ottiene proprio come ha appena detto David Thornley in un commento:

L'interfaccia va nel file .h, le funzioni disponibili pubblicamente nel file .c e le variabili e le funzioni private ottengono il modificatore di accesso statico allegato.


1

Un modo di gestire il codice è di scomporlo nei seguenti tipi di codice, seguendo le linee dell'architettura MVC (model-view-controller).

  • Gestori di input: questo codice si occupa di dispositivi di input come mouse, tastiera, porta di rete o astrazioni di livello superiore come eventi di sistema.
  • Gestori di output: questo codice si occupa dell'utilizzo dei dati per manipolare dispositivi esterni come monitor, luci, porte di rete, ecc.
  • Modelli: questo codice si occupa della dichiarazione della struttura dei dati persistenti, delle regole per la convalida dei dati persistenti e del salvataggio dei dati persistenti su disco (o di altri dispositivi di dati persistenti).
  • Visualizzazioni: questo codice si occupa della formattazione dei dati per soddisfare i requisiti di vari metodi di visualizzazione come browser Web (HTML / CSS), GUI, riga di comando, formati di dati del protocollo di comunicazione (ad es. JSON, XML, ASN.1, ecc.).
  • Algoritmi: questo codice trasforma ripetutamente un set di dati di input in un set di dati di output il più rapidamente possibile.
  • Controller: questo codice accetta input tramite i gestori di input, analizza gli input utilizzando algoritmi e quindi trasforma i dati con altri algoritmi combinando facoltativamente input con dati persistenti o semplicemente trasformando gli input e quindi facoltativamente salvando i dati trasformati in persistenti tramite il modello software e facoltativamente la trasformazione dei dati tramite il software di visualizzazione per renderizzarli su un dispositivo di output.

Questo metodo di organizzazione del codice funziona bene per i software scritti in qualsiasi linguaggio OO o non OO perché i modelli di progettazione comuni sono spesso comuni a ciascuna delle aree. Inoltre, questi tipi di limiti di codice sono spesso i più vagamente accoppiati, tranne gli algoritmi perché collegano insieme i formati di dati dagli input al modello e quindi agli output.

Le evoluzioni del sistema spesso assumono la forma di gestire più tipi di input o più tipi di output, ma i modelli e le viste sono uguali e i controller si comportano in modo molto simile. Oppure, nel tempo, potrebbe essere necessario che un sistema supporti tipi di output sempre più diversi anche se input, modelli, algoritmi sono gli stessi e controller e viste simili. Oppure è possibile aumentare un sistema per aggiungere nuovi modelli e algoritmi per lo stesso set di input, output simili e viste simili.

Un modo in cui la programmazione OO rende difficile l'organizzazione del codice è perché alcune classi sono profondamente legate alle strutture di dati persistenti, altre no. Se le strutture di dati persistenti sono intimamente correlate a cose come le relazioni 1: N a cascata o le relazioni m: n, è molto difficile decidere i limiti delle classi fino a quando non si è codificata una parte significativa e significativa del sistema prima di sapere di aver capito bene . Qualsiasi classe legata alle strutture di dati persistenti sarà difficile da evolvere quando cambia lo schema dei dati persistenti. Le classi che gestiscono algoritmi, formattazione e analisi hanno meno probabilità di essere vulnerabili ai cambiamenti nello schema delle strutture di dati persistenti. L'utilizzo di un tipo di organizzazione del codice MVC isola meglio le modifiche al codice più complesse al codice del modello.


0

Quando si lavora in linguaggi privi di funzionalità integrate di struttura e organizzazione (ad es. Se non ha spazi dei nomi, pacchetti, assiemi ecc ...) o dove questi sono insufficienti per tenere sotto controllo una base di codice di quelle dimensioni, la risposta naturale è sviluppare le nostre strategie per organizzare il codice.

Questa strategia organizzativa probabilmente include standard relativi a dove conservare diversi file, cose che devono accadere prima / dopo determinati tipi di operazioni, convenzioni di denominazione e altri standard di codifica, oltre a molti "ecco come è impostato - non scherzare! " scrivi commenti - che sono validi fintanto che spiegano perché!

Poiché molto probabilmente la strategia finirà per essere adattata alle esigenze specifiche del progetto (persone, tecnologie, ambiente, ecc.), È difficile fornire una soluzione unica per la gestione di basi di codice di grandi dimensioni.

Pertanto credo che il miglior consiglio sia quello di abbracciare la strategia specifica del progetto e di farne una priorità chiave: documentare la struttura, perché è così, i processi per apportare modifiche, verificarlo per assicurarsi che venga rispettato, e soprattutto: cambiarlo quando deve cambiare.

Conosciamo principalmente classi e metodi di refactoring, ma con una base di codice di grandi dimensioni in un linguaggio del genere è la stessa strategia organizzativa (completa di documentazione) che deve essere riformattata come e quando necessario.

Il ragionamento è lo stesso del refactoring: svilupperai un blocco mentale verso il lavoro su piccole parti del sistema se ritieni che l'organizzazione complessiva di esso sia un disastro e alla fine gli consentirai di deteriorarsi (almeno questo è il mio punto di vista esso).

Le avvertenze sono uguali: utilizzare il test di regressione, assicurarsi di poter facilmente ripristinare se il refactoring non funziona correttamente e progettare in modo da facilitare il refactoring in primo luogo (o semplicemente non lo farai!).

Concordo sul fatto che sia molto più complicato del refactoring del codice diretto, ed è più difficile convalidare / nascondere il tempo da manager / clienti che potrebbero non capire perché debba essere fatto, ma questi sono anche i tipi di progetto più inclini al marciume del software causato da inflessibili design di alto livello ...


0

Se stai chiedendo informazioni sulla gestione di una base di codice di grandi dimensioni, stai chiedendo come mantenere la tua base di codice ben strutturata a un livello relativamente approssimativo (librerie / moduli / costruzione di sottosistemi / utilizzo di spazi dei nomi / avere i documenti giusti nei posti giusti eccetera.). I principi OO, in particolare le "classi astratte" o le "interfacce", sono principi per mantenere il codice pulito internamente, a un livello molto dettagliato. Pertanto, le tecniche per mantenere gestibile una base di codice di grandi dimensioni non differiscono per OO o codice non OO.


0

Come è gestito è che scopri i bordi degli elementi che usi. Ad esempio, i seguenti elementi in C ++ hanno un bordo chiaro e tutte le dipendenze al di fuori del bordo devono essere attentamente studiate:

  1. funzione gratuita
  2. funzione membro
  3. classe
  4. oggetto
  5. interfaccia
  6. espressione
  7. chiamata del costruttore / creazione di oggetti
  8. chiamata di funzione
  9. tipo di parametro del modello

Combinando questi elementi e riconoscendone i bordi, è possibile creare quasi tutti gli stili di programmazione desiderati in c ++.

Un esempio di ciò è che una funzione sarebbe riconoscere che è male chiamare altre funzioni da una funzione, perché causa dipendenza, invece, si dovrebbero chiamare solo le funzioni membro dei parametri della funzione originale.


-1

La più grande sfida tecnica è il problema dello spazio dei nomi. Il collegamento parziale può essere utilizzato per aggirare questo. L'approccio migliore è progettare utilizzando standard di codifica. Altrimenti tutti i simboli diventano un disastro.


-2

Emacs ne è un buon esempio:

Emacs Architecture

Componenti Emacs

I test Emacs Lisp utilizzano skip-unlesse let-bindper eseguire il rilevamento di funzionalità e dispositivi di test:

A volte, non ha senso eseguire un test a causa di precondizioni mancanti. Una funzione Emacs richiesta potrebbe non essere compilata, la funzione da testare potrebbe chiamare un file binario esterno che potrebbe non essere disponibile sulla macchina di prova, si chiama. In questo caso, la macro skip-unlesspotrebbe essere utilizzata per saltare il test:

 (ert-deftest test-dbus ()
   "A test that checks D-BUS functionality."
   (skip-unless (featurep 'dbusbind))
   ...)

Il risultato dell'esecuzione di un test non dovrebbe dipendere dallo stato corrente dell'ambiente e ogni test dovrebbe lasciare il proprio ambiente nello stesso stato in cui lo ha trovato. In particolare, un test non dovrebbe dipendere da alcuna variabile o hook di personalizzazione di Emacs, e se deve apportare modifiche allo stato di Emacs o allo stato esterno a Emacs (come il file system), dovrebbe annullare queste modifiche prima che ritorni, indipendentemente dal fatto che sia passato o meno.

I test non dovrebbero dipendere dall'ambiente perché tali dipendenze possono rendere il test fragile o portare a guasti che si verificano solo in determinate circostanze e che sono difficili da riprodurre. Naturalmente, il codice in prova può avere impostazioni che ne influenzano il comportamento. In tal caso, è consigliabile eseguire let-bindtutte le variabili di impostazione del test per impostare una configurazione specifica per la durata del test. Il test può anche impostare una serie di diverse configurazioni ed eseguire il codice in prova con ciascuna.

Come è SQLite. Ecco il suo design:

  1. sqlite3_open () → Apri una connessione a un database SQLite nuovo o esistente. Il costruttore per sqlite3.

  2. sqlite3 → L'oggetto connessione al database. Creato da sqlite3_open () e distrutto da sqlite3_close ().

  3. sqlite3_stmt → L'oggetto istruzione preparato. Creato da sqlite3_prepare () e distrutto da sqlite3_finalize ().

  4. sqlite3_prepare () → Compilare il testo SQL in byte-code che farà il lavoro di interrogazione o aggiornamento del database. Il costruttore per sqlite3_stmt.

  5. sqlite3_bind () → Archivia i dati dell'applicazione nei parametri dell'SQL originale.

  6. sqlite3_step () → Avanza di sqlite3_stmt alla riga dei risultati successiva o al completamento.

  7. sqlite3_column () → Valori di colonna nella riga del risultato corrente per sqlite3_stmt.

  8. sqlite3_finalize () → Destructor per sqlite3_stmt.

  9. sqlite3_exec () → Una funzione wrapper che esegue sqlite3_prepare (), sqlite3_step (), sqlite3_column () e sqlite3_finalize () per una stringa di una o più istruzioni SQL.

  10. sqlite3_close () → Destructor per sqlite3.

architettura sqlite3

I componenti Tokenizer, Parser e Code Generator vengono utilizzati per elaborare le istruzioni SQL e convertirle in programmi eseguibili in un linguaggio macchina virtuale o codice byte. In parole povere , questi tre livelli principali implementano sqlite3_prepare_v2 () . Il codice byte generato dai primi tre livelli è un'istruzione preparata. Il modulo Macchina virtuale è responsabile dell'esecuzione del codice byte dell'istruzione SQL. Il modulo B-Tree organizza un file di database in più archivi chiave / valore con chiavi ordinate e prestazioni logaritmiche. Il modulo Pager è responsabile del caricamento in memoria delle pagine del file di database, dell'implementazione e del controllo delle transazioni e della creazione e manutenzione dei file journal che impediscono il danneggiamento del database a seguito di un arresto anomalo o di un'interruzione dell'alimentazione. L'interfaccia del sistema operativo è una sottile astrazione che fornisce un insieme comune di routine per adattare SQLite per l'esecuzione su diversi sistemi operativi. In parole povere , i quattro livelli inferiori implementano sqlite3_step () .

tabella virtuale sqlite3

Una tabella virtuale è un oggetto registrato con una connessione al database SQLite aperta. Dal punto di vista di un'istruzione SQL, l'oggetto tabella virtuale appare come qualsiasi altra tabella o vista. Ma dietro le quinte, query e aggiornamenti su una tabella virtuale invocano metodi di callback dell'oggetto tabella virtuale invece di leggere e scrivere sul file di database.

Una tabella virtuale potrebbe rappresentare una struttura di dati in memoria. Oppure potrebbe rappresentare una vista di dati su disco che non è nel formato SQLite. Oppure l'applicazione potrebbe calcolare il contenuto della tabella virtuale su richiesta.

Ecco alcuni usi esistenti e postulati per le tabelle virtuali:

Un'interfaccia di ricerca full-text
Indici spaziali usando R-alberi
Introspect il contenuto del disco di un file di database SQLite (la tabella virtuale dbstat)
Leggere e / o scrivere il contenuto di un file con valori separati da virgola (CSV)
Accedi al filesystem del computer host come se fosse una tabella di database
Abilitazione della manipolazione SQL dei dati in pacchetti statistici come R

SQLite utilizza una varietà di tecniche di test tra cui:

Tre cinture di prova sviluppate in modo indipendente
Copertura del test di filiale al 100% in una configurazione distribuita
Milioni e milioni di casi di test
Test di memoria insufficiente
Test di errore I / O
Test di crash e perdita di potenza
Test Fuzz
Test del valore limite
Test di ottimizzazione per disabili
Test di regressione
Test di database non validi
Ampio uso di assert () e controlli di runtime
Analisi di Valgrind
Controlli del comportamento indefiniti
Liste di controllo

Riferimenti

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.