In breve, non progettare il software per la riusabilità perché nessun utente finale si preoccupa se le funzioni possono essere riutilizzate. Invece, ingegnere per la comprensibilità del design : il mio codice è facile da capire per qualcun altro o per il mio futuro io smemorato? - e flessibilità di progettazione- quando inevitabilmente devo correggere bug, aggiungere funzionalità o modificare in altro modo la funzionalità, quanto il mio codice resisterà alle modifiche? L'unica cosa che interessa ai tuoi clienti è la velocità con cui puoi rispondere quando segnala un bug o chiede una modifica. Porre queste domande sul tuo progetto tende a generare accidentalmente codice riutilizzabile, ma questo approccio ti tiene concentrato sull'evitare i problemi reali che dovrai affrontare durante la vita di quel codice in modo da poter servire meglio l'utente finale piuttosto che perseguire alte, poco pratiche ideali di "ingegneria" per compiacere le barbe al collo.
Per qualcosa di semplice come l'esempio che hai fornito, l'implementazione iniziale va bene a causa di quanto sia piccola, ma questo design semplice diventerà difficile da capire e fragile se provi a inceppare troppa flessibilità funzionale (al contrario di flessibilità di progettazione) in una procedura. Di seguito è la mia spiegazione del mio approccio preferito alla progettazione di sistemi complessi per la comprensibilità e la flessibilità che spero dimostrerà cosa intendo con loro. Non utilizzerei questa strategia per qualcosa che potrebbe essere scritto in meno di 20 righe in un'unica procedura perché qualcosa di così piccolo già soddisfa i miei criteri di comprensibilità e flessibilità così com'è.
Oggetti, non procedure
Piuttosto che usare classi come i moduli della vecchia scuola con un mucchio di routine che chiami per eseguire le cose che il tuo software dovrebbe fare, considera di modellare il dominio come oggetti che interagiscono e cooperano per svolgere il compito da svolgere. I metodi in un paradigma orientato agli oggetti sono stati originariamente creati per essere segnali tra oggetti in modo che Object1
potessero dire Object2
di fare la sua cosa, qualunque cosa sia, e possibilmente ricevere un segnale di ritorno. Questo perché il paradigma orientato agli oggetti è intrinsecamente basato sulla modellazione degli oggetti del dominio e delle loro interazioni piuttosto che su un modo elegante per organizzare le stesse vecchie funzioni e procedure del paradigma imperativo. Nel caso delvoid destroyBaghdad
esempio, invece di provare a scrivere un metodo generico senza contesto per gestire la distruzione di Baghdad o qualsiasi altra cosa (che potrebbe rapidamente diventare complessa, difficile da capire e fragile), ogni cosa che può essere distrutta dovrebbe essere responsabile della comprensione di come per autodistruggersi. Ad esempio, hai un'interfaccia che descrive il comportamento delle cose che possono essere distrutte:
interface Destroyable {
void destroy();
}
Quindi hai una città che implementa questa interfaccia:
class City implements Destroyable {
@Override
public void destroy() {
...code that destroys the city
}
}
Nulla che richieda la distruzione di un'istanza di City
mai si preoccuperà di come ciò accada, quindi non vi è alcuna ragione per cui quel codice esista ovunque al di fuori di City::destroy
, e in effetti, la conoscenza intima del funzionamento interno City
all'esterno di se stesso sarebbe un accoppiamento stretto che riduce felxibility poiché devi considerare quegli elementi esterni nel caso dovessi mai aver bisogno di modificare il comportamento di City
. Questo è il vero scopo dietro l'incapsulamento. Pensalo come se ogni oggetto avesse una sua API che ti consentisse di fare tutto ciò che ti occorre per permetterti di preoccuparti di soddisfare le tue richieste.
Delega, non "Controllo"
Ora, se la tua classe di esecuzione è City
o Baghdad
dipende da quanto sia generico il processo di distruzione della città. Con tutta probabilità, una City
sarà composta da pezzi più piccoli che dovranno essere distrutti singolarmente per realizzare la distruzione totale della città, quindi in questo caso, ciascuno di questi pezzi sarebbe anche implementare Destroyable
, e avrebbero ogni essere istruiti dal City
distruggere allo stesso modo in cui qualcuno dall'esterno ha chiesto City
di distruggersi.
interface Part extends Destroyable {
...part-specific methods
}
class Building implements Part {
...part-specific methods
@Override
public void destroy() {
...code to destroy a building
}
}
class Street implements Part {
...part-specific methods
@Override
public void destroy() {
...code to destroy a building
}
}
class City implements Destroyable {
public List<Part> parts() {...}
@Override
public void destroy() {
parts().forEach(Destroyable::destroy);
}
}
Se vuoi diventare davvero pazzo e implementare l'idea di un oggetto Bomb
che viene lasciato cadere in una posizione e distrugge tutto in un certo raggio, potrebbe assomigliare a questo:
class Bomb {
private final Integer radius;
public Bomb(final Integer radius) {
this.radius = radius;
}
public void drop(final Grid grid, final Coordinate target) {
new ObjectsByRadius(
grid,
target,
this.radius
).forEach(Destroyable::destroy);
}
}
ObjectsByRadius
rappresenta un insieme di oggetti che viene calcolato per gli Bomb
input perché Bomb
non si preoccupa del modo in cui tale calcolo viene eseguito fino a quando può funzionare con gli oggetti. Questo è riutilizzabile per inciso, ma l'obiettivo principale è quello di isolare il calcolo dai processi di rilascio Bomb
e distruzione degli oggetti in modo da poter comprendere ogni pezzo e come si adattano insieme e modificare il comportamento di un singolo pezzo senza dover rimodellare l'intero algoritmo .
Interazioni, non algoritmi
Invece di provare a indovinare il giusto numero di parametri per un algoritmo complesso, ha più senso modellare il processo come un insieme di oggetti interagenti, ognuno con ruoli estremamente ristretti, poiché ti darà la possibilità di modellare la complessità del tuo elaborare le interazioni tra questi oggetti ben definiti, facili da comprendere e quasi immutabili. Se fatto correttamente, questo rende anche alcune delle modifiche più complesse banali come implementare un'interfaccia o due e rielaborare quali oggetti sono istanziati nel tuo main()
metodo.
Ti darei qualcosa per il tuo esempio originale, ma onestamente non riesco a capire cosa significhi "stampare ... Day Light Savings". Quello che posso dire di quella categoria di problemi è che ogni volta che si esegue un calcolo, il cui risultato potrebbe essere formattato in diversi modi, il mio modo preferito di scomporre questo è il seguente:
interface Result {
String print();
}
class Caclulation {
private final Parameter paramater1;
private final Parameter parameter2;
public Calculation(final Parameter parameter1, final Parameter parameter2) {
this.parameter1 = parameter1;
this.parameter2 = parameter2;
}
public Result calculate() {
...calculate the result
}
}
class FormattedResult {
private final Result result;
public FormattedResult(final Result result) {
this.result = result;
}
@Override
public String print() {
...interact with this.result to format it and return the formatted String
}
}
Dato che il tuo esempio utilizza classi della libreria Java che non supportano questo design, puoi semplicemente usare l'API di ZonedDateTime
direttamente. L'idea qui è che ogni calcolo è incapsulato all'interno del proprio oggetto. Non fa ipotesi su quante volte dovrebbe essere eseguito o su come dovrebbe formattare il risultato. Si occupa esclusivamente di eseguire la forma più semplice del calcolo. Questo rende sia facile da capire che flessibile da cambiare. Allo stesso modo, Result
si preoccupa esclusivamente di incapsulare il risultato del calcolo e FormattedResult
si preoccupa esclusivamente di interagire con il Result
per formattare secondo le regole che definiamo. In questo modo,possiamo trovare il numero perfetto di argomenti per ciascuno dei nostri metodi poiché ognuno di essi ha un compito ben definito . È anche molto più semplice modificare andare avanti fintanto che le interfacce non cambiano (cosa che non hanno la stessa probabilità di fare se hai minimizzato correttamente le responsabilità dei tuoi oggetti). Il nostromain()
metodo potrebbe apparire così:
class App {
public static void main(String[] args) {
final List<Set<Paramater>> parameters = ...instantiated from args
parameters.forEach(set -> {
System.out.println(
new FormattedResult(
new Calculation(
set.get(0),
set.get(1)
).calculate()
).print()
);
});
}
}
È un dato di fatto, la programmazione orientata agli oggetti è stata inventata specificamente come soluzione al problema di complessità / flessibilità del paradigma imperativo perché non esiste una buona risposta (che tutti possono concordare o arrivare in modo indipendente, comunque) su come ottimizzare in modo ottimale specificare funzioni e procedure imperative all'interno del linguaggio.