Quale strategia usi per la denominazione dei pacchetti nei progetti Java e perché?

Ci ho pensato un po ‘fa e di recente sono riemerso perché il mio negozio sta facendo la sua prima vera web app Java.

Come introduzione, vedo due principali strategie di denominazione dei pacchetti. (Per essere chiari, non mi riferisco all’intera parte ‘domain.company.project’, sto parlando della convenzione del pacchetto sotto quella.) Comunque, le convenzioni di denominazione dei pacchetti che vedo sono le seguenti:

  1. Funzionalità: denominare i pacchetti in base alla loro funzione architettonica piuttosto che alla loro id quadro in base al dominio aziendale. Un altro termine potrebbe essere la denominazione in base al “livello”. Quindi, avresti un pacchetto * .ui e un pacchetto * .dominio e un pacchetto * .orm. I pacchetti sono fette orizzontali anziché verticali.

    Questo è molto più comune della denominazione logica. In effetti, non credo di aver mai visto o sentito parlare di un progetto che lo fa. Questo ovviamente mi rende sospettoso (un po ‘come pensare che tu abbia trovato una soluzione a un problema di NP) poiché non sono terribilmente intelligente e presumo che tutti debbano avere ottimi motivi per farlo nel modo in cui lo fanno. D’altra parte, non sono contrario alla gente che manca l’elefante nella stanza e non ho mai sentito una vera discussione per fare il naming dei pacchetti in questo modo. Sembra essere lo standard di fatto.

  2. Logico: denominare i pacchetti in base alla loro id quadro di dominio aziendale e inserire ogni class che ha a che fare con quella porzione verticale di funzionalità in quel pacchetto.

    Non ho mai visto o sentito parlare di questo, come ho detto prima, ma per me ha un senso.

    1. Tendo ad avvicinare i sistemi verticalmente piuttosto che orizzontalmente. Voglio entrare e sviluppare il sistema di elaborazione degli ordini, non il livello di accesso ai dati. Ovviamente, ci sono buone probabilità che tocchi il livello di accesso ai dati nello sviluppo di quel sistema, ma il punto è che non ci penso in questo modo. Ciò significa, ovviamente, che quando ricevo un ordine di cambiamento o se voglio implementare qualche nuova funzionalità, sarebbe bello non dover andare a pescare in giro in un mucchio di pacchetti per trovare tutte le classi correlate. Invece, guardo solo nel pacchetto X perché quello che sto facendo ha a che fare con X.

    2. Dal punto di vista dello sviluppo, considero la vittoria principale il fatto che i tuoi pacchetti documentino il tuo dominio aziendale piuttosto che la tua architettura. Credo che il dominio sia quasi sempre la parte del sistema che è più difficile da trovare dove l’architettura del sistema, specialmente a questo punto, sta diventando quasi banale nella sua implementazione. Il fatto di poter venire a un sistema con questo tipo di convenzione di denominazione e immediatamente dalla denominazione dei pacchetti sa che si tratta di ordini, clienti, imprese, prodotti, ecc. Sembra dannatamente utile.

    3. Sembra che questo ti consentirebbe di sfruttare molto meglio i modificatori di accesso di Java. Ciò consente di definire in modo molto più chiaro le interfacce nei sottosistemi piuttosto che nei livelli del sistema. Quindi se hai un sottosistema di ordini che vuoi essere persistentemente trasparente, potresti in teoria non lasciare mai che qualcun altro sappia che è persistente non dover creare interfacce pubbliche alle sue classi di persistenza nel livello dao e invece impacchettare la class dao in con solo le classi con cui si occupa. Ovviamente, se si desidera esporre questa funzionalità, è ansible fornire un’interfaccia o renderla pubblica. Sembra proprio che tu perda molto di ciò avendo una sezione verticale delle caratteristiche del tuo sistema divisa in più pacchetti.

    4. Suppongo che uno svantaggio che posso vedere sia che rende lo strappo degli strati un po ‘più difficile. Invece di eliminare o rinominare un pacchetto e poi eliminarne uno nuovo con una tecnologia alternativa, devi entrare e modificare tutte le classi in tutti i pacchetti. Tuttavia, non vedo che questo è un grosso problema. Potrebbe derivare da una mancanza di esperienza, ma devo immaginare che la quantità di volte in cui si scambiano le tecnologie scompare in confronto alla quantità di volte che si entra e modificare le sezioni di feature verticali all’interno del sistema.

Quindi immagino che la domanda poi verrebbe da te, come si chiamano i pacchetti e perché? Per favore, capisci che non penso necessariamente che sia incappato nell’oca dorata o qualcosa qui. Sono abbastanza nuovo a tutto questo con esperienza prevalentemente accademica. Tuttavia, non riesco a individuare i buchi nel mio ragionamento, quindi spero che tutti voi possiate essere in grado di andare avanti.

Per il design del pacchetto, prima divido per livello, poi con qualche altra funzionalità.

Ci sono alcune regole aggiuntive:

  1. i livelli sono impilati dal più generale (in basso) al più specifico (in alto)
  2. ogni livello ha un’interfaccia pubblica (astrazione)
  3. un livello può dipendere solo dall’interfaccia pubblica di un altro strato (incapsulamento)
  4. un livello può dipendere solo da livelli più generali (dipendenze dall’alto verso il basso)
  5. uno strato preferibilmente dipende dallo strato direttamente sotto di esso

Ad esempio, per un’applicazione Web, potresti avere i seguenti livelli nel livello applicazione (dall’alto verso il basso):

  • livello di presentazione: genera l’interfaccia utente che verrà visualizzata nel livello client
  • livello applicazione: contiene la logica specifica per un’applicazione, stateful
  • livello di servizio: raggruppa le funzionalità in base al dominio, senza stato
  • livello di integrazione: fornisce l’accesso al livello di back-end (db, jms, email, …)

Per il layout del pacchetto risultante, queste sono alcune regole aggiuntive:

  • la radice di ogni nome di pacchetto è ..
  • l’interfaccia di un livello è ulteriormente suddivisa in base alla funzionalità: .
  • l’implementazione privata di un layer è preceduta da private: .private

Ecco un esempio di layout.

Il livello di presentazione è diviso per la tecnologia di visualizzazione e facoltativamente per (gruppi di) applicazioni.

 com.company.appname.presentation.internal com.company.appname.presentation.springmvc.product com.company.appname.presentation.servlet ... 

Il livello dell’applicazione è diviso in casi d’uso.

 com.company.appname.application.lookupproduct com.company.appname.application.internal.lookupproduct com.company.appname.application.editclient com.company.appname.application.internal.editclient ... 

Il livello di servizio è suddiviso in domini aziendali, influenzato dalla logica del dominio in un livello di backend.

 com.company.appname.service.clientservice com.company.appname.service.internal.jmsclientservice com.company.appname.service.internal.xmlclientservice com.company.appname.service.productservice ... 

Il livello di integrazione è suddiviso in “tecnologie” e oggetti di accesso.

 com.company.appname.integration.jmsgateway com.company.appname.integration.internal.mqjmsgateway com.company.appname.integration.productdao com.company.appname.integration.internal.dbproductdao com.company.appname.integration.internal.mockproductdao ... 

I vantaggi di separare i pacchetti in questo modo è che è più facile gestire la complessità e aumenta la testabilità e la riusabilità. Anche se sembra un sovraccarico, nella mia esperienza, in realtà, è molto naturale e tutti quelli che lavorano su questa struttura (o simili) lo raccolgono in pochi giorni.

Perché penso che l’approccio verticale non sia così buono?

Nel modello a strati, diversi moduli di alto livello possono utilizzare lo stesso modulo di livello inferiore. Ad esempio: è ansible creare più viste per la stessa applicazione, più applicazioni possono utilizzare lo stesso servizio, più servizi possono utilizzare lo stesso gateway. Il trucco qui è che quando si passa attraverso gli strati, il livello di funzionalità cambia. I moduli in layer più specifici non mappano 1-1 sui moduli dal livello più generale, perché i livelli di funzionalità che esprimono non mappano 1-1.

Quando si utilizza l’approccio verticale per il design del pacchetto, ovvero prima si divide per funzionalità, quindi si forzano tutti gli elementi costitutivi con diversi livelli di funzionalità nello stesso “jacket di funzionalità”. Potresti progettare i tuoi moduli generali per uno più specifico. Ma questo viola l’importante principio secondo cui il livello più generale non dovrebbe conoscere strati più specifici. Il livello di servizio, per esempio, non dovrebbe essere modellato dopo i concetti dal livello dell’applicazione.

Mi trovo a rispettare i principi del design della confezione dello zio Bob. In breve, le classi che devono essere riutilizzate insieme e cambiate insieme (per lo stesso motivo, ad esempio un cambiamento di dipendenza o un cambiamento di struttura) dovrebbero essere inserite nello stesso pacchetto. IMO, la ripartizione funzionale avrebbe migliori possibilità di raggiungere questi obiettivi rispetto alla suddivisione verticale / business-specifica nella maggior parte delle applicazioni.

Ad esempio, una porzione orizzontale di oggetti di dominio può essere riutilizzata da diversi tipi di front-end o persino applicazioni e una sezione orizzontale del front-end Web probabilmente cambierà insieme quando sarà necessario modificare il framework Web sottostante. D’altra parte, è facile immaginare l’effetto a catena di queste modifiche su molti pacchetti se le classi in diverse aree funzionali sono raggruppate in tali pacchetti.

Ovviamente, non tutti i tipi di software sono uguali e la suddivisione verticale può avere senso (in termini di raggiungimento degli obiettivi di riusabilità e chiusibilità-di-modificare) in alcuni progetti.

Di solito ci sono entrambi i livelli di divisione presenti. Dall’alto, ci sono unità di schieramento. Questi sono chiamati “logicamente” (secondo i tuoi termini, pensa alle funzionalità di Eclipse). All’interno dell’unità di distribuzione, si ha una divisione funzionale dei pacchetti (si pensi ai plugin di Eclipse).

Ad esempio, feature è com.feature e consiste in com.feature.client , com.feature.core e com.feature.ui plugins. All’interno dei plugin, ho una divisione molto limitata rispetto ad altri pacchetti, anche se non è inusuale.

Aggiornamento: Btw, ci sono grandi discorsi di Juergen Hoeller sull’organizzazione del codice in InfoQ: http://www.infoq.com/presentations/code-organization-large-projects . Juergen è uno degli architetti di Spring e conosce molto su questa roba.

La maggior parte dei progetti java su cui ho lavorato suddividono i pacchetti java funzionalmente prima, poi logicamente.

Di solito le parti sono sufficientemente grandi da essere suddivise in artefatti di compilazione separati, in cui è ansible inserire le funzionalità principali in un unico contenitore, le apis in un altro contenuto del frontend Web in un warfile, ecc.

I pacchetti devono essere compilati e distribuiti come una unità. Quando si considera quali classi appartengono a un pacchetto, uno dei criteri chiave sono le sue dipendenze. Da quali altri pacchetti (incluse le librerie di terze parti) dipende questa class. Un sistema ben organizzato raggrupperà classi con dipendenze simili in un pacchetto. Ciò limita l’impatto di una modifica in una libreria, poiché solo pochi pacchetti ben definiti dipenderanno da essa.

Sembra che il tuo sistema logico verticale possa tendere a “sporcare” le dipendenze nella maggior parte dei pacchetti. Cioè, se ogni funzionalità è confezionata come una porzione verticale, ogni pacchetto dipenderà da ogni libreria di terze parti che si utilizza. È probabile che qualsiasi modifica a una libreria si ripercuota nell’intero sistema.

Personalmente preferisco raggruppare logicamente le classi all’interno di quelle che includono un subpackage per ogni partecipazione funzionale.

Obiettivi di confezionamento

Dopotutto, i pacchetti riguardano il raggruppamento delle cose – l’idea di classi correlate si vive l’una vicina all’altra. Se vivono nello stesso pacchetto possono usufruire del pacchetto privato per limitare la visibilità. Il problema è quello di raggruppare tutte le informazioni relative alla vista e alla perseveranza in un unico pacchetto, in modo che molte classi possano essere mescolate in un unico pacchetto. La prossima cosa sensata da fare è quindi creare di conseguenza la vista, la persistenza, i pacchetti secondari di util e le classi di refactoring. Purtroppo, la protezione e l’esplorazione privata del pacchetto non supportano il concetto dell’attuale pacchetto e sottoprocesso poiché ciò contribuirebbe a far rispettare tali regole di visibilità.

Vedo ora il valore nella separazione tramite la funzionalità perché il valore è lì per raggruppare tutte le cose relative alla vista. Le cose in questa strategia di denominazione diventano disconnesse con alcune classi nella vista mentre altre sono in persistenza e così via.

Un esempio della mia struttura di packaging logico

Per scopi di illustrazione, consente di denominare due moduli: utilizzare il modulo nome come un concetto che raggruppa le classi sotto un particolare ramo di un albero pacckage.

apple.model apple.store banana.model banana.store

vantaggi

Un cliente che utilizza Banana.store.BananaStore è esposto solo alle funzionalità che desideriamo rendere disponibili. La versione di ibernazione è un dettaglio di implementazione di cui non è necessario essere a conoscenza, né dovrebbero vedere queste classi mentre aggiungono disordine alle operazioni di archiviazione.

Altro Logico v Vantaggi funzionali

Più avanti verso la radice più ampio diventa l’ambito e le cose che appartengono a un pacchetto iniziano a mostrare sempre più dipendenze su cose che appartengono ai loro moduli. Se si dovesse esaminare ad esempio il modulo “banana”, la maggior parte delle dipendenze sarebbe limitata a quel modulo. Di fatto, la maggior parte degli helper sotto “banana” non farebbe riferimento al di fuori di questo pacchetto.

Perché la funzionalità?

Che valore si ottiene tagliando le cose in base alla funzionalità. La maggior parte delle classi in questo caso sono indipendenti l’una dall’altra, con poca o nessuna necessità di sfruttare i metodi o le classi private del pacchetto. Refactoring loro così nei loro subpackages guadagna poco, ma aiuta a ridurre il disordine.

Cambiamenti dello sviluppatore nel sistema

Quando gli sviluppatori hanno il compito di apportare modifiche un po ‘più che banali, sembra sciocco che potenzialmente abbiano modifiche che includono file da tutte le aree dell’albero dei pacchetti. Con l’approccio logico strutturato i loro cambiamenti sono più locali all’interno della stessa parte dell’albero dei pacchetti che sembra giusto.

Entrambi gli approcci funzionali (architettonici) e logici (funzionalità) al packaging hanno un posto. Molte applicazioni di esempio (quelle presenti nei libri di testo, ecc.) Seguono l’approccio funzionale di collocare presentazione, servizi aziendali, mapping dei dati e altri livelli architettonici in pacchetti separati. Nelle applicazioni di esempio, ogni pacchetto spesso ha solo pochi o solo una class.

Questo approccio iniziale va bene poiché un esempio forzato spesso serve a: 1) mappare concettualmente l’architettura del framework presentato, 2) è fatto in modo tale con un unico scopo logico (ad esempio aggiungere / rimuovere / aggiornare / eliminare gli animali domestici da una clinica) . Il problema è che molti lettori lo considerano uno standard senza limiti.

Come un’applicazione “business” si espande per includere sempre più funzionalità, seguendo l’approccio funzionale diventa un onere. Sebbene io sappia dove cercare i tipi basati sul livello dell’architettura (ad esempio i web controller sotto un pacchetto “web” o “ui”, ecc.), Lo sviluppo di una singola funzione logica inizia a richiedere il salto avanti e indietro tra molti pacchetti. Questo è ingombrante, per lo meno, ma è peggio di così.

Poiché i tipi correlati logicamente non sono raggruppati insieme, l’API è pubblicizzata eccessivamente; l’interazione tra tipi correlati logicamente è forzata ad essere “pubblica” in modo che i tipi possano importare e interagire tra loro (la capacità di minimizzare a default / visibilità del pacchetto è persa).

Se sto costruendo una libreria di framework, i miei pacchetti seguiranno sicuramente un approccio di packaging funzionale / architettonico. I miei utenti API potrebbero persino apprezzare che le loro istruzioni di importazione contengono pacchetti intuitivi che prendono il nome dall’architettura.

Al contrario, quando costruisci un’applicazione commerciale eseguirò un pacchetto per funzionalità. Non ho problemi a collocare Widget, WidgetService e WidgetController tutti nello stesso pacchetto ” com.myorg.widget. ” E quindi sfruttare la visibilità predefinita (e avere meno istruzioni di importazione e dipendenze tra pacchetti).

Vi sono, tuttavia, casi incrociati. Se il mio WidgetService è utilizzato da molti domini logici (funzionalità), potrei creare un pacchetto ” com.myorg.common.service. “. Vi sono anche buone possibilità di creare classi con l’intenzione di essere riutilizzabili attraverso le funzionalità e finire con pacchetti come ” com.myorg.common.ui.helpers ” e ” com.myorg.common.util “. Potrei persino finire per spostare tutte queste classi “comuni” successivamente in un progetto separato e includerle nella mia applicazione aziendale come una dipendenza myorg-commons.jar.

Dipende dalla granularità dei tuoi processi logici?

Se sono indipendenti, spesso hanno un nuovo progetto per loro nel controllo del codice sorgente, piuttosto che un nuovo pacchetto.

Il progetto in cui mi trovo al momento è errato verso la divisione logica, c’è un pacchetto per l’aspetto jython, un pacchetto per un motore di regole, pacchetti per foo, bar, binglewozzle, ecc. Sto cercando di avere i parser XML specifici / scrittori per ogni modulo all’interno di quel pacchetto, piuttosto che avere un pacchetto XML (che ho già fatto in precedenza), anche se ci sarà ancora un pacchetto XML di base dove va la logica condivisa. Uno dei motivi è che può essere estensibile (plugin) e quindi ogni plugin dovrà anche definire il suo codice XML (o database, ecc.), In modo da centralizzare questo potrebbe introdurre problemi in seguito.

Alla fine sembra essere il modo più sensato per il particolare progetto. Penso che sia facile confezionare comunque le linee del tipico schema a strati del progetto. Finirai con un mix di packaging logico e funzionale.

Ciò di cui c’è bisogno sono gli spazi dei nomi con tag. Un parser XML per alcune funzionalità Jython potrebbe essere taggato sia Jython che XML, piuttosto che dover scegliere l’uno o l’altro.

O forse sto barcollando.

Personalmente vorrei andare per la denominazione funzionale. La breve ragione: evita la duplicazione del codice o l’incubo della dipendenza.

Lasciami elaborare un po ‘. Cosa succede quando si utilizza un file jar esterno, con il proprio albero dei pacchetti? Si sta effettivamente importando il codice (compilato) nel progetto e con esso un albero di pacchetti (separato da un punto di vista funzionale). Avrebbe senso usare le due convenzioni di denominazione contemporaneamente? No, a meno che non fosse nascosto a te. Ed è, se il tuo progetto è abbastanza piccolo e ha un singolo componente. Ma se hai diverse unità logiche, probabilmente non vuoi reimplementare, diciamo, il modulo di caricamento dei file di dati. Vuoi condividerlo tra unità logiche, non avere dipendenze artificiali tra unità logicamente non correlate, e non devi scegliere a quale unità vuoi mettere quel particolare strumento condiviso.

Immagino che questo sia il motivo per cui la denominazione funzionale è la più utilizzata in progetti che raggiungono, o sono destinati a raggiungere, una certa dimensione, e la denominazione logica viene utilizzata nelle convenzioni di denominazione delle classi per tenere traccia del ruolo specifico, se c’è ne di ogni class in una pacchetto.

Proverò a rispondere in modo più preciso a ciascuno dei tuoi punti sulla denominazione logica.

  1. Se devi andare a pesca nelle vecchie classi per modificare le funzionalità quando hai un cambio di piani, è un segno di brutta astrazione: dovresti build classi che forniscano una funzionalità ben definita, definibile in una breve frase. Solo alcune classi di alto livello dovrebbero assemblare tutte queste informazioni per riflettere la tua business intelligence. In questo modo, sarai in grado di riutilizzare più codice, avere una manutenzione più semplice, una documentazione più chiara e meno problemi di dipendenza.

  2. Questo dipende principalmente dal modo in cui fai il tuo progetto. Sicuramente, la vista logica e funzionale sono ortogonali. Quindi, se si utilizza una convenzione di denominazione, è necessario applicare l’altra ai nomi di class per mantenere un certo ordine, o passare da una convenzione di denominazione a un’altra in una certa profondità.

  3. I modificatori di accesso sono un buon modo per consentire ad altre classi che comprendono la tua elaborazione di accedere alle interiora della tua class. La relazione logica non significa una comprensione dei vincoli algoritmici o di concorrenza. Funzionale può, anche se non è così. Sono molto stanco dei modificatori di accesso diversi da quelli pubblici e privati, perché spesso nascondono una mancanza di un’architettura e un’astrazione di class appropriate.

  4. Nei grandi progetti commerciali, il cambiamento delle tecnologie avviene più spesso di quanto si possa credere. Ad esempio, ho dovuto modificare 3 volte il parser XML, 2 volte la tecnologia di caching e 2 volte il software di geolocalizzazione. Per fortuna avevo nascosto tutti i dettagli grintosi in un pacchetto dedicato …

Cerco di progettare le strutture dei pacchetti in modo tale che se dovessi disegnare un grafico di dipendenza, sarebbe facile seguire e utilizzare un modello coerente, con il minor numero ansible di riferimenti circolari.

Per me, questo è molto più facile da mantenere e visualizzare in un sistema di denominazione verticale piuttosto che orizzontale. se component1.display ha un riferimento a component2.dataaccess, questo genera più campane di avviso che se display.component1 ha un riferimento a dataaccess. Component2.

Ovviamente, i componenti condivisi da entrambi vanno nel loro pacchetto.

Seguo e propongo totalmente l’organizzazione logica (“by-feature”)! Un pacchetto dovrebbe seguire il concetto di un “modulo” il più vicino ansible. L’organizzazione funzionale può diffondere un modulo su un progetto, con conseguente minore incapsulamento e incline alle modifiche nei dettagli di implementazione.

Prendiamo ad esempio un plugin Eclipse: mettere tutte le viste o le azioni in un pacchetto sarebbe un disastro. Invece, ogni componente di una funzione dovrebbe andare al pacchetto della funzione, o se ce ne sono molti, in sottofacili (featureA.handlers, featureA.preferences ecc.)

Certo, il problema sta nel sistema di pacchetti gerarchici (che tra l’altro ha Java), il che rende la gestione delle preoccupazioni ortogonali imansible o quantomeno molto difficile – sebbene avvengano ovunque!

Da un punto di vista prettamente pratico, i costrutti di visibilità di java consentono alle classi nello stesso pacchetto di accedere a metodi e proprietà con visibilità protected e default , oltre a quelle public . Usare metodi non pubblici da uno strato completamente diverso del codice sarebbe sicuramente un big odore di codice. Quindi tendo a inserire classi dello stesso livello nello stesso pacchetto.

Non uso spesso questi metodi protetti o predefiniti altrove, tranne forse nei test delle unità per la class, ma quando lo faccio, viene sempre da una class sullo stesso livello

Dipende. Nella mia linea di lavoro, a volte dividiamo i pacchetti per funzioni (accesso ai dati, analisi) o per class di attività (credito, titoli azionari, tassi di interesse). Basta selezionare la struttura che è più conveniente per la tua squadra.

È un esperimento interessante non utilizzare affatto i pacchetti (eccetto il pacchetto radice).

La domanda che sorge allora, è, quando e perché ha senso introdurre i pacchetti. Presumibilmente, la risposta sarà diversa da quella che avresti risposto all’inizio del progetto.

Presumo che la tua domanda si pone a tutti, perché i pacchetti sono come categorie ed è talvolta difficile decidere per l’uno o l’altro. A volte i tag sarebbero più apprezzati nel comunicare che una class è utilizzabile in molti contesti.

Dalla mia esperienza, la riutilizzabilità crea più problemi che soluzioni. Con i processori e la memoria più recenti e economici, preferirei la duplicazione del codice piuttosto che la stretta integrazione per poterli riutilizzare.