Come faccio a sapere quanto dovrebbero essere riutilizzabili i miei metodi? [chiuso]


133

Mi occupo dei miei affari a casa e mia moglie viene da me e mi dice

Tesoro .. Puoi stampare tutti i Day Light Savings in tutto il mondo per il 2018 nella console? Devo controllare qualcosa.

E sono super felice perché era quello che stavo aspettando da tutta la mia vita con la mia esperienza con Java e mi sono inventato:

import java.time.*;
import java.util.Set;

class App {
    void dayLightSavings() {
        final Set<String> availableZoneIds = ZoneId.getAvailableZoneIds();
        availableZoneIds.forEach(
            zoneId -> {
                LocalDateTime dateTime = LocalDateTime.of(
                    LocalDate.of(2018, 1, 1), 
                    LocalTime.of(0, 0, 0)
                );
                ZonedDateTime now = ZonedDateTime.of(dateTime, ZoneId.of(zoneId));
                while (2018 == now.getYear()) {
                    int hour = now.getHour();
                    now = now.plusHours(1);
                    if (now.getHour() == hour) {
                        System.out.println(now);
                    }
                }
            }
        );
    }
}

Ma poi dice che mi stava solo testando se ero un ingegnere informatico eticamente addestrato, e mi dice che sembra che non lo sia da allora (preso da qui ) ..

Va notato che nessun ingegnere informatico addestrato eticamente acconsentirebbe mai a scrivere una procedura di DestroyBaghdad. L'etica professionale di base gli richiederebbe invece di scrivere una procedura di DestroyCity, alla quale Baghdad potrebbe essere dato come parametro.

E io sono, bene, ok, mi hai preso .. Passa ogni anno che ti piace, eccoti qui:

import java.time.*;
import java.util.Set;

class App {
    void dayLightSavings(int year) {
        final Set<String> availableZoneIds = ZoneId.getAvailableZoneIds();
        availableZoneIds.forEach(
            zoneId -> {
                LocalDateTime dateTime = LocalDateTime.of(
                    LocalDate.of(year, 1, 1), 
                    LocalTime.of(0, 0, 0)
                );
                ZonedDateTime now = ZonedDateTime.of(dateTime, ZoneId.of(zoneId));
                while (year == now.getYear()) {
                    // rest is same..

Ma come faccio a sapere quanto (e cosa) parametrizzare? Dopo tutto, potrebbe dire ..

  • vuole passare un formattatore di stringhe personalizzato, forse non le piace il formato in cui sto già stampando: 2018-10-28T02:00+01:00[Arctic/Longyearbyen]

void dayLightSavings(int year, DateTimeFormatter dtf)

  • è interessata solo a determinati periodi del mese

void dayLightSavings(int year, DateTimeFormatter dtf, int monthStart, int monthEnd)

  • è interessata a determinati periodi di ora

void dayLightSavings(int year, DateTimeFormatter dtf, int monthStart, int monthEnd, int hourStart, int hourend)

Se stai cercando una domanda concreta:

Se destroyCity(City city)è meglio di destroyBaghdad(), è takeActionOnCity(Action action, City city)ancora meglio? Perché perché no?

Dopotutto, posso prima chiamarlo con Action.DESTROYallora Action.REBUILD, no?

Ma intraprendere azioni sulle città non è abbastanza per me, che ne dici takeActionOnGeographicArea(Action action, GeographicalArea GeographicalArea)? Dopo tutto, non voglio chiamare:

takeActionOnCity(Action.DESTORY, City.BAGHDAD);

poi

takeActionOnCity(Action.DESTORY, City.ERBIL);

e così via quando posso fare:

takeActionOnGeographicArea(Action.DESTORY, Country.IRAQ);

ps Ho costruito la mia domanda solo attorno alla citazione che ho citato, non ho nulla contro nessun paese, religione, razza o altro al mondo. Sto solo cercando di fare un punto.



71
Il punto che stai facendo qui è uno che ho cercato di esprimere molte volte: la generalità è costosa, e quindi deve essere giustificata da benefici chiari e specifici . Ma va più in profondità di così; i linguaggi di programmazione sono creati dai loro progettisti per rendere alcuni tipi di generalità più facili di altri e ciò influenza le nostre scelte come sviluppatori. È facile parametrizzare un metodo in base a un valore e quando questo è lo strumento più semplice che hai nella tua cassetta degli attrezzi, la tentazione è di usarlo indipendentemente dal fatto che abbia senso per l'utente.
Eric Lippert,

30
Il riutilizzo non è qualcosa che desideri per se stesso. Diamo la priorità al riutilizzo perché crediamo che gli artefatti del codice siano costosi da costruire e quindi dovrebbero essere utilizzabili nel maggior numero possibile di scenari, per ammortizzare quel costo in quegli scenari. Questa convinzione spesso non è giustificata da osservazioni e pertanto il consiglio di progettare per la riusabilità è spesso applicato erroneamente . Progetta il tuo codice per ridurre il costo totale dell'applicazione .
Eric Lippert,

7
Tua moglie non è etica per aver perso tempo mentendo a te. Chiese una risposta e fornì un mezzo suggerito; In base a quel contratto, il modo in cui ottieni quell'output è solo tra te e te stesso. Inoltre, destroyCity(target)è molto più non etico di destroyBagdad()! Che tipo di mostro scrive un programma per spazzare via una città, per non parlare di qualsiasi città al mondo? E se il sistema fosse stato compromesso ?! Inoltre, cosa ha a che fare la gestione del tempo / delle risorse (sforzo investito) con l'etica? Finché il contratto verbale / scritto è stato completato come concordato.
Tezra,

25
Penso che potresti leggere troppo in questa battuta. È uno scherzo sul modo in cui i programmatori di computer prendono decisioni etiche sbagliate, perché danno la priorità alle considerazioni tecniche sugli effetti del loro lavoro sugli esseri umani. Non intende essere un buon consiglio per la progettazione del programma.
Eric Lippert,

Risposte:


114

Sono le tartarughe fino in fondo.

O astrazioni in questo caso.

Il codice delle buone pratiche è qualcosa che può essere applicato all'infinito, e ad un certo punto stai astrattendo per motivi di astrazione, il che significa che l'hai portato troppo lontano. Trovare quella linea non è qualcosa che è facile da mettere in una regola empirica, in quanto dipende molto dal tuo ambiente.

Ad esempio, abbiamo avuto clienti che erano noti per richiedere prima delle semplici applicazioni ma poi per le espansioni. Abbiamo anche avuto clienti che chiedono ciò che vogliono e generalmente non tornano più da noi per un'espansione.
Il tuo approccio varierà per cliente. Per il primo cliente, a pagare per preventivamente estratto il codice perché sei ragionevolmente certo che sarà necessario rivedere il codice in futuro. Per il secondo cliente, potresti non voler investire quello sforzo extra se ti aspetti che non voglia espandere l'applicazione in qualsiasi momento (nota: questo non significa che non segui alcuna buona pratica, ma semplicemente che stai evitando di fare più di quanto sia attualmente necessario.

Come faccio a sapere quali funzionalità implementare?

Il motivo per cui menziono quanto sopra è perché sei già caduto in questa trappola:

Ma come faccio a sapere quanto (e cosa) parametrizzare? Dopotutto, potrebbe dire .

"Potrebbe dire" non è un requisito aziendale attuale. È un'ipotesi per un futuro requisito aziendale. Come regola generale, non basarsi su ipotesi, sviluppare solo ciò che è attualmente richiesto.

Tuttavia, il contesto si applica qui. Non conosco tua moglie. Forse hai accuratamente valutato che in realtà lo vorrà. Ma dovresti comunque confermare con il cliente che questo è davvero quello che vogliono, perché altrimenti trascorrerai del tempo a sviluppare una funzionalità che non finirai mai con.

Come faccio a sapere quale architettura implementare?

Questo è più complicato. Al cliente non interessa il codice interno, quindi non puoi chiedere loro se ne hanno bisogno. La loro opinione in merito è per lo più irrilevante.

Tuttavia, puoi ancora confermare la necessità di farlo ponendo le giuste domande al cliente. Invece di chiedere informazioni sull'architettura, chiedi loro le loro aspettative di sviluppo futuro o espansioni alla base di codice. Puoi anche chiedere se l'obiettivo attuale ha una scadenza, perché potresti non essere in grado di implementare la tua architettura di fantasia nei tempi necessari.

Come faccio a sapere quando estrarre ulteriormente il mio codice?

Non so dove l'ho letto (se qualcuno lo sa, fammelo sapere e darò credito), ma una buona regola empirica è che gli sviluppatori dovrebbero contare come un uomo delle caverne: uno, due molti .

inserisci qui la descrizione dell'immagine XKCD # 764

In altre parole, quando un determinato algoritmo / modello viene utilizzato per la terza volta, dovrebbe essere estratto in modo da renderlo riutilizzabile (= utilizzabile più volte).

Per essere chiari, non sto insinuando che non dovresti scrivere codice riutilizzabile quando ci sono solo due istanze dell'algoritmo in uso. Naturalmente puoi anche astrarre questo, ma la regola dovrebbe essere che per tre casi devi astrarre.

Ancora una volta, questo fattore nelle tue aspettative. Se sai già che hai bisogno di tre o più istanze, ovviamente puoi immediatamente astrarre. Ma se solo indovini che potresti voler implementarlo più volte, la correttezza dell'implementazione dell'astrazione si basa completamente sulla correttezza della tua ipotesi.
Se hai indovinato correttamente, ti sei risparmiato un po 'di tempo. Se hai indovinato in modo errato, hai sprecato un po 'del tuo tempo e dei tuoi sforzi e probabilmente hai compromesso la tua architettura per implementare qualcosa di cui non hai bisogno.

Se destroyCity(City city)è meglio di destroyBaghdad(), è takeActionOnCity(Action action, City city)ancora meglio? Perché perché no?

Dipende molto da più cose:

  • Esistono più azioni che possono essere intraprese in qualsiasi città?
  • Queste azioni possono essere usate in modo intercambiabile? Perché se le azioni "distruggi" e "ricostruisci" hanno esecuzioni completamente diverse, non ha senso unire le due in un unico takeActionOnCitymetodo.

Inoltre, tieni presente che se lo fai in modo ricorsivo, finirai con un metodo così astratto che non è altro che un contenitore in cui eseguire un altro metodo, il che significa che hai reso il tuo metodo irrilevante e insignificante.
Se l'intero takeActionOnCity(Action action, City city)corpo del metodo finisce per essere nient'altro che action.TakeOn(city);, dovresti chiederti se il takeActionOnCitymetodo ha davvero uno scopo o non è solo un livello aggiuntivo che non aggiunge nulla di valore.

Ma intraprendere azioni sulle città non è abbastanza per me, che ne dici takeActionOnGeographicArea(Action action, GeographicalArea GeographicalArea)?

La stessa domanda si apre qui:

  • Hai un caso d'uso per le aree geografiche?
  • L'esecuzione di un'azione su una città e una regione è la stessa?
  • È possibile intraprendere azioni su qualsiasi regione / città?

Se puoi rispondere definitivamente "sì" a tutti e tre, allora è giustificata un'astrazione.


16
Non posso sottolineare abbastanza la regola "uno, due, molti". Ci sono infinite possibilità di astrarre / parametrizzare qualcosa, ma il sottoinsieme utile è piccolo, spesso zero. Sapere esattamente quale variante ha valore può essere determinato più spesso solo a posteriori. Attenersi quindi ai requisiti immediati * e aggiungere complessità se necessario a nuovi requisiti o senno di poi. * A volte conosci bene lo spazio del problema, quindi potrebbe essere giusto aggiungere qualcosa perché sai di averne bisogno domani. Ma usa saggiamente questo potere, può anche portare alla rovina.
Christian Sauer,

2
> "Non so dove l'ho letto [..]". Potresti aver letto Coding Horror: The Rule of Three .
Runa,

10
L '"una, due, molte regole" è davvero lì per impedirti di costruire l'astrazione sbagliata applicando DRY alla cieca. Il fatto è che due parti di codice possono iniziare a sembrare quasi identiche, quindi è allettante allontanare le differenze; ma all'inizio non sai quali parti del codice sono stabili e quali no; inoltre, potrebbe rivelarsi che hanno effettivamente bisogno di evolversi in modo indipendente (diversi modelli di cambiamento, diversi insiemi di responsabilità). In entrambi i casi, un'astrazione errata agisce contro di te e si mette di mezzo.
Filip Milovanović,

4
Attendere più di due esempi di "la stessa logica" consente di essere un giudice migliore di ciò che dovrebbe essere astratto e di come (e in realtà, si tratta di gestire le dipendenze / accoppiamento tra codice con diversi modelli di cambiamento).
Filip Milovanović,

1
@kukis: la linea realistica dovrebbe essere tracciata a 2 (secondo il commento di Baldrickk): zero-uno-molti (come nel caso delle relazioni con il database). Tuttavia, questo apre la porta a comportamenti inutili alla ricerca di schemi. Due cose possono vagamente assomigliarsi, ma ciò non significa che siano effettivamente le stesse. Tuttavia, quando una terza istanza entra nella mischia che assomiglia sia alla prima che alla seconda istanza, puoi dare un giudizio più accurato sul fatto che le loro somiglianze sono effettivamente un modello riutilizzabile. Quindi la linea di buonsenso è tracciata a 3, che tiene conto dell'errore umano quando si individuano "schemi" tra solo due istanze.
Flater

44

Pratica

Questo è Software Engineering SE, ma la creazione di software è molto più arte che ingegneria. Non esiste un algoritmo universale da seguire o misure da prendere per capire quanta riusabilità è sufficiente. Come in ogni altra cosa, più pratica acquisisci nel progettare programmi, meglio riuscirai a farlo. Avrai un'idea migliore di ciò che è "abbastanza" perché vedrai cosa va storto e come va storto quando parametrizzi troppo o troppo poco.

Ora non è molto utile , quindi che ne dici di alcune linee guida?

Guarda indietro alla tua domanda. C'è un sacco di "potrebbe dire" e "io potrei". Molte dichiarazioni che teorizzano su alcune esigenze future. Gli umani sono schifosi nel predire il futuro. E tu (molto probabilmente) sei un essere umano. Il problema schiacciante della progettazione del software sta cercando di spiegare un futuro che non conosci.

Linea guida 1: non ne avrai bisogno

Sul serio. Semplicemente fermati. Più spesso, quel problema futuro immaginato non si presenta - e certamente non si presenterà proprio come l'hai immaginato.

Orientamento 2: costo / beneficio

Bene, quel piccolo programma ti ha impiegato qualche ora a scrivere forse? Che importa se tua moglie non tornare indietro e chiedere quelle cose? Peggio ancora, passi qualche ora in più a mettere insieme un altro programma per farlo. In questo caso, non è troppo tempo per rendere questo programma più flessibile. E non aggiungerà molto alla velocità di runtime o all'utilizzo della memoria. Ma i programmi non banali hanno risposte diverse. Scenari diversi hanno risposte diverse. Ad un certo punto, i costi chiaramente non valgono il vantaggio anche con imperfette capacità di racconto futuro.

Orientamento 3: attenzione alle costanti

Guarda indietro alla domanda. Nel tuo codice originale, ci sono molti ints costanti. 2018, 1. Interi costanti, stringhe costanti ... Sono le cose più probabilmente bisogno di essere non-costanti . Meglio ancora, impiegano solo un po 'di tempo per parametrizzare (o almeno definire come costanti effettive). Ma un'altra cosa di cui diffidare è il comportamento costante . IlSystem.out.printlnper esempio. Questo tipo di ipotesi sull'uso tende ad essere qualcosa che cambia in futuro e tende ad essere molto costoso da risolvere. Non solo, ma IO come questo rende impura la funzione (insieme al recupero del fuso orario). Parametrizzare quel comportamento può rendere la funzione più pura portando ad una maggiore flessibilità e testabilità. Grandi vantaggi con costi minimi (soprattutto se si verifica un sovraccarico che utilizza System.outper impostazione predefinita).


1
È solo una linea guida, gli 1 vanno bene ma li guardi e vai "questo cambierà mai?" Nah. E la stampa potrebbe essere parametrizzata con una funzione di ordine superiore, anche se Java non è eccezionale in questi.
Telastyn,

5
@KorayTugay: se il programma fosse davvero per tua moglie che torna a casa, YAGNI ti direbbe che la tua versione iniziale è perfetta e non dovresti investire altro tempo per introdurre costanti o parametri. YAGNI ha bisogno di un contesto : il tuo programma è una soluzione a eliminazione diretta o un programma di migrazione eseguito solo per pochi mesi o fa parte di un enorme sistema ERP, destinato ad essere utilizzato e mantenuto per diversi decenni?
Doc Brown,

8
@KorayTugay: separare l'I / O dal calcolo è una tecnica di strutturazione del programma fondamentale. Separare la generazione di dati dal filtraggio dei dati dalla trasformazione dei dati dal consumo dei dati dalla presentazione dei dati. Dovresti studiare alcuni programmi funzionali, quindi lo vedrai più chiaramente. Nella programmazione funzionale, è abbastanza comune generare una quantità infinita di dati, filtrare solo i dati che ti interessano, trasformare i dati nel formato che ti serve, costruirne una stringa e stampare questa stringa in 5 diverse funzioni, uno per ogni passaggio.
Jörg W Mittag,

3
Come sidenote, seguire con forza YAGNI porta alla necessità di continuamente refactoring: "Utilizzato senza refactoring continuo, potrebbe portare a codice disorganizzato e rilavorazioni di massa, noto come debito tecnico". Quindi, sebbene YAGNI sia una buona cosa in generale, ha una grande responsabilità di rivisitare e rivalutare il codice, che non è qualcosa che ogni sviluppatore / azienda è disposto a fare.
Flater,

4
@Telastyn: suggerisco di espandere la domanda a "questo non cambierà mai e l'intenzione del codice è banalmente leggibile senza nominare la costante ?" Anche per i valori che non cambiano mai, può essere rilevante nominarli solo per mantenere le cose leggibili.
Flater,

27

Primo: nessuno sviluppatore di software attento alla sicurezza scriverebbe un metodo DestroyCity senza passare un token di autorizzazione per qualsiasi motivo.

Anch'io posso scrivere qualsiasi cosa come un imperativo che ha saggezza evidente senza che sia applicabile in un altro contesto. Perché è necessario autorizzare una concatenazione di stringhe?

Secondo: tutto il codice eseguito deve essere completamente specificato .

Non importa se la decisione è stata codificata sul posto, o differita su un altro livello. Ad un certo punto c'è un pezzo di codice in qualche lingua che sa sia cosa deve essere distrutto sia come istruirlo.

Potrebbe essere nello stesso file oggetto destroyCity(xyz)e potrebbe essere in un file di configurazione: destroy {"city": "XYZ"}"oppure potrebbe essere una serie di clic e pressioni di tasti in un'interfaccia utente.

In terzo luogo:

Tesoro .. Puoi stampare tutti i Day Light Savings in tutto il mondo per il 2018 nella console? Devo controllare qualcosa.

è un insieme molto diverso di requisiti per:

vuole passare un formattatore di stringhe personalizzato, ... interessato solo a determinati periodi di mese, ... [e] interessato a determinati periodi di ora ...

Ora la seconda serie di requisiti rende ovviamente uno strumento più flessibile. Ha un pubblico target più ampio e un regno più ampio di applicazione. Il pericolo qui è che l'applicazione più flessibile al mondo sia in realtà un compilatore per il codice macchina. È letteralmente un programma così generico che può costruire qualsiasi cosa per rendere il computer qualunque sia necessario (entro i limiti del suo hardware).

In generale, le persone che necessitano di software non vogliono qualcosa di generico; vogliono qualcosa di specifico. Dando più opzioni in realtà stai rendendo le loro vite più complicate. Se avessero voluto quella complessità, avrebbero invece usato un compilatore, senza chiedertelo.

Tua moglie stava chiedendo funzionalità e sotto specificava le sue esigenze. In questo caso era apparentemente intenzionale, e in generale è perché non sanno niente di meglio. Altrimenti avrebbero appena usato il compilatore. Quindi il primo problema è che non hai chiesto maggiori dettagli su cosa esattamente voleva fare. Voleva farlo funzionare per diversi anni? Lo voleva in un file CSV? Non hai scoperto quali decisioni voleva prendere se stessa e cosa ti stava chiedendo di capire e decidere per lei. Una volta che hai capito quali decisioni devono essere rinviate, puoi capire come comunicare tali decisioni attraverso parametri (e altri mezzi configurabili).

Detto questo, la maggior parte dei clienti non comunica correttamente, presume o ignora alcuni dettagli (ovvero decisioni) che vorrebbero davvero fare se stessi o che in realtà non volevano fare (ma sembra fantastico). Ecco perché sono importanti metodi di lavoro come il PDSA (piano-sviluppo-studio-atto). Hai pianificato il lavoro in linea con i requisiti e poi hai sviluppato una serie di decisioni (codice). Ora è il momento di studiarlo, da solo o con il tuo cliente e apprendere nuove cose, e queste informano il tuo modo di pensare in futuro. Finalmente agisci sui tuoi nuovi approfondimenti - aggiorna i requisiti, perfeziona il processo, ottieni nuovi strumenti, ecc ... Quindi ricomincia a pianificare. Ciò avrebbe rivelato nel tempo eventuali requisiti nascosti e avrebbe dimostrato progressi per molti clienti.

Finalmente. Il tuo tempo è importante ; è molto reale e molto limitato. Ogni decisione che prendi comporta molte altre decisioni nascoste, e questo è lo sviluppo del software. Ritardare una decisione come argomento può rendere più semplice la funzione corrente, ma rende altrove più complessa. Tale decisione è pertinente in quell'altra posizione? È più rilevante qui? Di chi è davvero la decisione da prendere? Lo stai decidendo; questa è codifica. Se ripeti spesso serie di decisioni, c'è un vero vantaggio nel codificarle all'interno di alcune astrazioni. XKCD ha una prospettiva utile qui. E questo è rilevante a livello di sistema, sia esso una funzione, un modulo, un programma, ecc.

Il consiglio all'inizio implica che le decisioni che la tua funzione non ha il diritto di prendere dovrebbero essere trasmesse come argomento. Il problema è che una DestroyBaghdadfunzione potrebbe effettivamente essere la funzione che ha quel diritto.


+1 adoro la parte relativa al compilatore!
Lee,

4

Ci sono molte risposte a lungo termine qui, ma onestamente penso che sia super semplice

Qualsiasi informazione codificata che hai nella tua funzione che non fa parte del nome della funzione dovrebbe essere un parametro.

così nella tua funzione

class App {
    void dayLightSavings() {
        final Set<String> availableZoneIds = ZoneId.getAvailableZoneIds();
        availableZoneIds.forEach(zoneId -> {
            LocalDateTime dateTime = LocalDateTime.of(LocalDate.of(2018, 1, 1), LocalTime.of(0, 0, 0));
            ZonedDateTime now = ZonedDateTime.of(dateTime, ZoneId.of(zoneId));
            while (2018 == now.getYear()) {
                int hour = now.getHour();
                now = now.plusHours(1);
                if (now.getHour() == hour) {
                    System.out.println(now);
                }
            }
        });
    }
}

Hai:

The zoneIds
2018, 1, 1
System.out

Quindi sposterei tutti questi parametri in una forma o nell'altra. Si potrebbe sostenere che gli ZoneId siano impliciti nel nome della funzione, forse si vorrebbe renderlo ancora di più cambiandolo in "DaylightSavingsAroundTheWorld" o qualcosa del genere

Non hai una stringa di formato, quindi aggiungerne una è una richiesta di funzione e dovresti riferire tua moglie all'istanza Jira della tua famiglia. Può essere inserito nell'arretrato e attribuito la priorità alla riunione del comitato di gestione del progetto appropriata.


1
Tu (rivolgendomi a OP) non dovresti certamente aggiungere una stringa di formato, dal momento che non dovresti stampare nulla. L'unica cosa su questo codice che ne impedisce assolutamente il riutilizzo è che stampa. Dovrebbe riportare le zone o una mappa delle zone a quando escono dall'ora legale. (Anche se il motivo si identifica solo quando si interrompe l' ora legale e non l' ora legale, non capisco. Non sembra corrispondere alla dichiarazione del problema.)
David Conrad,

il requisito è stampare sulla console. puoi mitigare il couplung stretto passando nel flusso di output come parametro come suggerisco
Ewan,

1
Anche così, se vuoi che il codice sia riutilizzabile, non dovresti stampare sulla console. Scrivi un metodo che restituisce i risultati, quindi scrivi un chiamante che li ottiene e stampa. Ciò lo rende anche testabile. Se vuoi che produca output, non passerei in un flusso di output, passerei in un Consumer.
David Conrad,

un flusso di input è un consumatore
Ewan,


4

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 Object1potessero dire Object2di 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 destroyBaghdadesempio, 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 Citymai 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 Cityall'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 è Cityo Baghdaddipende da quanto sia generico il processo di distruzione della città. Con tutta probabilità, una Citysarà 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 Citydistruggere allo stesso modo in cui qualcuno dall'esterno ha chiesto Citydi 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 Bombche 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);
    }
}

ObjectsByRadiusrappresenta un insieme di oggetti che viene calcolato per gli Bombinput perché Bombnon 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 Bombe 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 ZonedDateTimedirettamente. 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, Resultsi preoccupa esclusivamente di incapsulare il risultato del calcolo e FormattedResultsi preoccupa esclusivamente di interagire con il Resultper 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.


Questa è una risposta molto dettagliata e ponderata, ma sfortunatamente penso che manchi il segno di ciò che l'OP stava veramente chiedendo. Non stava chiedendo una lezione sulle buone pratiche di OOP per risolvere il suo esempio specioso, stava chiedendo dei criteri per i quali decidiamo l'investimento di tempo in una soluzione rispetto alla generalizzazione.
maple_shaft

@maple_shaft Forse ho perso il segno, ma penso che anche tu l'abbia fatto. L'OP non chiede informazioni sull'investimento di tempo rispetto alla generalizzazione. Mi chiede "Come faccio a sapere quanto dovrebbero essere riutilizzabili i miei metodi?" Continua a porre nel corpo della sua domanda: "Se destroyCity (città) è meglio di destroyBaghdad (), takeActionOnCity (azione, città) è ancora migliore? Perché / perché no?" Ho fatto appello a un approccio alternativo alle soluzioni ingegneristiche che ritengo risolva il problema di capire quanto generici fossero i metodi e fornito esempi a supporto delle mie affermazioni. Mi dispiace che non ti sia piaciuto.
Stuporman,

@maple_shaft Francamente, solo l'OP può decidere se la mia risposta fosse pertinente alla sua domanda poiché il resto di noi potrebbe combattere guerre difendendo le nostre interpretazioni delle sue intenzioni, tutte cose che potrebbero essere ugualmente sbagliate.
Stuporman,

@maple_shaft Ho aggiunto un'introduzione per cercare di chiarire come si collega alla domanda e ho fornito una chiara delineazione tra la risposta e l'implementazione di esempio. Va meglio?
Stuporman,

1
Onestamente, se applichi tutti questi principi, la risposta arriverà naturalmente, sarà liscia e leggibile in modo massiccio. Inoltre, modificabile senza troppe storie. Non so chi sei, ma vorrei che ci fosse più di te! Continuo a strappare il codice reattivo per un OO decente, ed è SEMPRE la metà delle dimensioni, più leggibile, più controllabile e ha ancora thread / splitting / mapping. Penso che React sia per le persone che non comprendono i concetti "di base" che hai appena elencato.
Stephen J,

3

Esperienza , conoscenza del dominio e revisione dei codici.

E, indipendentemente da quanta o quanta poca esperienza , conoscenza del dominio o team possiedi, non puoi evitare la necessità di effettuare il refactoring secondo necessità.


Con Esperienza , inizierai a riconoscere gli schemi nei metodi (e nelle classi) non specifici del dominio che scrivi. E, se sei interessato al codice DRY, sentirai brutte sensazioni quando stai per scrivere un metodo di cui sai istintivamente che scriverai variazioni in futuro. Quindi, invece, scriverai intuitivamente un denominatore meno comune parametrizzato.

(Questa esperienza può trasferirsi istintivamente anche in alcuni oggetti e metodi del tuo dominio).

Con Domain Knowledge , avrai un'idea di quali concetti aziendali sono strettamente correlati, quali concetti hanno variabili, che sono abbastanza statiche, ecc.

Con Code Review, la sottoparametria e l'eccessiva parametrizzazione saranno più probabilmente colte prima che diventi codice di produzione, perché i tuoi colleghi avranno (si spera) esperienze e prospettive uniche, sia sul dominio che sulla codifica in generale.


Detto questo, i nuovi sviluppatori in genere non avranno questi Spidey Senses o un gruppo esperto di colleghi su cui appoggiarsi subito. E anche gli sviluppatori esperti traggono vantaggio da una disciplina di base per guidarli attraverso nuovi requisiti o attraverso giornate nebbiose. Quindi, ecco cosa suggerirei come inizio :

  • Inizia con l'implementazione ingenua, con una parametrizzazione minima.
    (Includi tutti i parametri che già sai di cui avrai bisogno, ovviamente ...)
  • Rimuovi numeri magici e stringhe, spostandoli in configurazioni e / o parametri
  • Fattore di metodi "grandi" in metodi più piccoli e ben definiti
  • Rifattorizzare metodi altamente ridondanti (se conveniente) in un denominatore comune, parametrizzando le differenze.

Questi passaggi non si verificano necessariamente nell'ordine indicato. Se ti siedi per scrivere un metodo che sai già essere altamente ridondante con un metodo esistente , passa direttamente al refactoring se è conveniente. (Se il refactoring non impiegherà molto più tempo di quanto sarebbe scrivere, testare e mantenere due metodi.)

Ma, oltre ad avere molta esperienza e così via, consiglio un codice DRY-ing piuttosto minimalista. Non è difficile riformulare le violazioni ovvie. E, se sei troppo zelante , puoi finire con un codice "troppo ASCIUTTO" che è ancora più difficile da leggere, comprendere e mantenere rispetto all'equivalente "WET".


2
Quindi non c'è una risposta giusta a If destoryCity(City city) is better than destoryBaghdad(), is takeActionOnCity(Action action, City city) even better?? È una domanda sì / no, ma non ha una risposta, giusto? O il presupposto iniziale è sbagliato, destroyCity(City)potrebbe non essere necessariamente migliore e in realtà dipende ? Quindi non significa che io non sono un ingegnere informatico non addestrato eticamente perché ho implementato direttamente senza parametri al primo posto? Voglio dire qual è la risposta alla domanda concreta che sto ponendo?
Koray Tugay,

La tua domanda in qualche modo pone alcune domande. La risposta alla domanda sul titolo è "esperienza, conoscenza del dominio, recensioni di codice ... e ... non abbiate paura di refactoring". La risposta a qualsiasi "concreto sono questi i parametri giusti per il metodo ABC" domanda è ... "Non lo so. Perché me lo chiedi? C'è qualcosa di sbagliato nel numero di parametri che ha attualmente ??? In tal caso, articola . Riparalo. " ... Potrei rimandarti al "POAP" per ulteriori indicazioni: devi capire perché stai facendo quello che stai facendo!
svidgen,

Voglio dire ... facciamo anche un passo indietro rispetto al destroyBaghdad()metodo. Qual è il contesto? È un videogioco in cui la fine del gioco provoca la distruzione di Baghdad ??? In tal caso ... destroyBaghdad()potrebbe essere un nome / firma del metodo perfettamente ragionevole ...
svidgen,

1
Quindi non sei d'accordo con la citazione citata nella mia domanda, giusto? It should be noted that no ethically-trained software engineer would ever consent to write a DestroyBaghdad procedure.Se tu fossi nella stanza con Nathaniel Borenstein, gli sosterresti che in realtà dipende e che la sua affermazione non è corretta? Voglio dire, è bello che molte persone rispondano, spendendo il loro tempo ed energia, ma non vedo una risposta concreta da nessuna parte. Spidey-sensi, recensioni di codice .. Ma a cosa risponde is takeActionOnCity(Action action, City city) better? null?
Koray Tugay,

1
@svidgen Naturalmente, un altro modo per astrarre questo senza alcuno sforzo aggiuntivo è quello di invertire la dipendenza: fare in modo che la funzione restituisca un elenco di città, invece di fare qualsiasi azione su di esse (come "println" nel codice originale). Questo può essere ulteriormente estratto se necessario, ma solo questo cambiamento da solo si occupa di circa la metà dei requisiti aggiunti nella domanda originale - e invece di una funzione impura che può fare ogni sorta di cose cattive, hai solo una funzione che restituisce un elenco e il chiamante fa le cose cattive.
Luaan,

2

La stessa risposta di qualità, usabilità, debito tecnico ecc:

Riutilizzabile come te, l'utente, ne ho bisogno

Fondamentalmente è un giudizio - se il costo di progettare e mantenere l'astrazione sarà ripagato dal costo (= tempo e sforzo) ti farà risparmiare.

  • Nota la frase "down the line": c'è un meccanico di payoff qui, quindi dipenderà da quanto lavorerai ulteriormente con questo codice. Per esempio:
    • Si tratta di un progetto unico o verrà progressivamente migliorato a lungo?
    • Sei fiducioso nel tuo design o probabilmente dovrai scartarlo o modificarlo in altro modo drasticamente per il prossimo progetto / traguardo (ad esempio provare un altro framework)?
  • Il vantaggio previsto dipende anche dalla capacità di prevedere il futuro (modifiche all'app). A volte, puoi ragionevolmente vedere i luoghi che la tua app prenderà. Più volte, pensi di poterlo fare, ma in realtà non puoi. Le regole empiriche qui sono il principio YAGNI e la regola del tre - entrambi enfatizzano il lavoro su ciò che sai, ora.

1 Questo è un costrutto di codice, quindi in questo caso sei "l'utente", l'utente del codice sorgente


1

C'è un processo chiaro che puoi seguire:

  • Scrivi un test fallito per una singola caratteristica che è di per sé una "cosa" (cioè, non una divisione arbitraria di una caratteristica in cui nessuna delle due metà ha davvero senso).
  • Scrivi il codice minimo assoluto per farlo passare in verde, non una riga in più.
  • Risciacqua e ripeti.
  • (Rifattorizzare incessantemente se necessario, il che dovrebbe essere facile grazie alla grande copertura del test.)

Questo si presenta con - almeno secondo l'opinione di alcune persone - un codice praticamente ottimale, dato che è il più piccolo possibile, ogni funzione finita richiede il minor tempo possibile (il che potrebbe essere o non essere vero se guardi il finito prodotto dopo il refactoring) e ha un'ottima copertura del test. Evita inoltre in modo evidente metodi o classi troppo generici.

Questo ti dà anche istruzioni molto chiare su quando rendere le cose generiche e quando specializzarle.

Trovo strano il tuo esempio di città; Molto probabilmente non scriverei mai il nome di una città. È così ovvio che ulteriori città verranno incluse in seguito, qualunque cosa tu stia facendo. Ma un altro esempio sarebbero i colori. In alcune circostanze, la codifica "rossa" o "verde" sarebbe una possibilità. Ad esempio, i semafori sono di un colore così onnipresente che puoi semplicemente cavartela (e puoi sempre refactoring). La differenza è che "rosso" e "verde" hanno un significato universale, "codificato" nel nostro mondo, è incredibilmente improbabile che cambi mai, e non esiste nemmeno un'alternativa.

Il tuo primo metodo di ora legale è semplicemente rotto. Sebbene sia conforme alle specifiche, il 2018 hardcoded è particolarmente negativo perché a) non è menzionato nel "contratto" tecnico (nel nome del metodo, in questo caso) eb) sarà presto obsoleto, quindi rottura è incluso dall'inizio. Per cose che sono legate a ora / data, raramente sarebbe logico codificare un valore specifico poiché, bene, il tempo passa. Ma a parte questo, tutto il resto è in discussione. Se gli dai un anno semplice e poi calcoli sempre l'anno completo, vai avanti. La maggior parte delle cose che hai elencato (formattazione, scelta di un intervallo più piccolo, ecc.) Urla che il tuo metodo sta facendo troppo, e dovrebbe invece probabilmente restituire un elenco / array di valori in modo che il chiamante possa fare da solo la formattazione / filtro.

Ma alla fine, la maggior parte di queste sono opinioni, gusti, esperienze e pregiudizi personali, quindi non preoccuparti troppo.


Per quanto riguarda il tuo penultimo paragrafo, guarda i "requisiti" inizialmente indicati, ovvero quelli su cui si basava il primo metodo. Specifica il 2018, quindi il codice è tecnicamente corretto (e probabilmente corrisponderebbe al tuo approccio basato sulle funzionalità).
dwizum,

@dwizum, è corretto per quanto riguarda i requisiti, ma c'è il nome del metodo è fuorviante. Nel 2019, qualsiasi programmatore che sta solo guardando il nome del metodo supporrebbe che stia facendo qualsiasi cosa (magari restituendo i valori per l'anno in corso), non il 2018 ... Aggiungerò una frase alla risposta per chiarire cosa intendevo dire.
AnoE

1

Sono giunto al parere che esistono due tipi di codice riutilizzabile:

  • Codice che è riutilizzabile perché è una cosa fondamentale e basilare.
  • Codice che è riutilizzabile perché ha parametri, sostituzioni e hook per ovunque.

Il primo tipo di riusabilità è spesso una buona idea. Si applica a cose come elenchi, hashaps, archivi chiave / valore, string matcher (es. Regex, glob, ...), tuple, unificazione, alberi di ricerca (prima profondità, ampiezza prima, approfondimento iterativo, ...) , combinatori parser, cache / memoisers, lettori / scrittori di formati di dati (s-espressioni, XML, JSON, protobuf, ...), code di attività, ecc.

Queste cose sono così generali, in un modo molto astratto, che vengono riutilizzate ovunque nella programmazione quotidiana. Se ti ritrovi a scrivere un codice speciale che sarebbe più semplice se fosse reso più astratto / generale (ad esempio se abbiamo "un elenco di ordini dei clienti", potremmo buttare via le cose "ordine del cliente" per ottenere "un elenco" ), potrebbe essere una buona idea estrarla. Anche se non viene riutilizzato, ci consente di disaccoppiare funzionalità non correlate.

Il secondo tipo è dove abbiamo del codice concreto, che risolve un problema reale, ma lo fa prendendo un sacco di decisioni. Possiamo renderlo più generale / riutilizzabile "codificando" tali decisioni, ad esempio trasformandole in parametri, complicando l'implementazione e fornendo dettagli ancora più concreti (ovvero la conoscenza di quali hook potremmo desiderare per le sostituzioni). Il tuo esempio sembra essere di questo tipo. Il problema con questo tipo di riusabilità è che possiamo finire per provare a indovinare i casi d'uso di altre persone o il nostro sé futuro. Alla fine potremmo finire con così tanti parametri che il nostro codice non è utilizzabile, figuriamoci riutilizzabile! In altre parole, quando si chiama ci vuole più sforzo che scrivere semplicemente la nostra versione. È qui che YAGNI (non ne avrai bisogno) è importante. Molte volte, tali tentativi di codice "riutilizzabile" finiscono per non essere riutilizzati, dal momento che potrebbe essere incompatibile con quei casi d'uso più fondamentalmente di quanto i parametri possano tenere conto, o quei potenziali utenti preferirebbero eseguire il rollup del proprio (diamine, guarda tutti standard e librerie là fuori i cui autori hanno preceduto la parola "Semplice", per distinguerli dai predecessori!).

Questa seconda forma di "riusabilità" dovrebbe fondamentalmente essere eseguita secondo le necessità. Certo, puoi inserire alcuni parametri "ovvi", ma non iniziare a cercare di prevedere il futuro. YAGNI.


Possiamo dire che sei d'accordo sul fatto che la mia prima interpretazione è andata bene, dove anche l'anno è stato programmato? O se inizialmente avessi implementato quel requisito, faresti dell'anno un parametro nel tuo primo take?
Koray Tugay,

La tua prima interpretazione è andata bene, dato che il requisito era uno script unico per "controllare qualcosa". Non supera il suo test "etico", ma fallisce il test "no dogma". "Potrebbe dire ..." sta inventando i requisiti di cui non hai bisogno.
Warbo,

Non possiamo dire quale 'distruggere la città' sia "migliore" senza ulteriori informazioni: destroyBaghdadè uno script unico (o almeno, è idempotente). Forse distruggere una città sarebbe un miglioramento, ma cosa succede se destroyBaghdadfunziona inondando il Tigri? Ciò può essere riutilizzabile per Mosul e Bassora, ma non per La Mecca o Atlanta.
Warbo,

Vedo quindi che non sei d'accordo con Nathaniel Borenstein, il proprietario della citazione. Sto cercando di capire lentamente penso leggendo tutte queste risposte e le discussioni.
Koray Tugay,

1
Mi piace questa differenziazione. Non è sempre chiaro e ci sono sempre "casi limite". Ma in generale, sono anche un fan di "blocchi" (spesso in forma di staticmetodi) che sono puramente funzionali e di basso livello e, al contrario, decidere su "configurare parametri e hook" è di solito quello in cui devi costruire alcune strutture che chiedono di essere giustificate.
Marco13,

1

Esistono già molte risposte eccellenti ed elaborate. Alcuni approfondiscono dettagli specifici, espongono alcuni punti di vista sulle metodologie di sviluppo del software in generale, e alcuni hanno certamente elementi controversi o "opinioni" diffuse.

La risposta di Warbo ha già evidenziato diversi tipi di riusabilità. Vale a dire, se qualcosa è riutilizzabile perché è un elemento fondamentale, o se qualcosa è riutilizzabile perché è "generico" in qualche modo. Riferendosi a quest'ultimo, c'è qualcosa che considererei una sorta di misura per la riusabilità:

Se un metodo può emularne un altro.

Per quanto riguarda l'esempio della domanda: immagina che il metodo

void dayLightSavings()

era l'implementazione di una funzionalità richiesta da un cliente. Quindi sarà qualcosa che altri programmatori dovrebbero usare , e quindi, essere un metodo pubblico , come in

publicvoid dayLightSavings()

Questo potrebbe essere implementato come mostrato nella tua risposta. Ora, qualcuno vuole parametrizzarlo con l'anno. Quindi puoi aggiungere un metodo

publicvoid dayLightSavings(int year)

e modificare l'implementazione originale in modo che sia

public void dayLightSavings() {
    dayLightSavings(2018);
}

Le successive "richieste di funzionalità" e le generalizzazioni seguono lo stesso modello. Quindi se e solo se esiste la domanda per il modulo più generico, è possibile implementarlo, sapendo che questo modulo più generico consente implementazioni banali di quelli più specifici:

public void dayLightSavings() {
    dayLightSavings(2018, 0, 12, 0, 12, new DateTimeFormatter(...));
}

Se avessi anticipato future estensioni e richieste di funzionalità e avessi avuto un po 'di tempo a tua disposizione e volessi trascorrere un fine settimana noioso con generalizzazioni (potenzialmente inutili), avresti potuto iniziare con il più generico fin dall'inizio. Ma solo come metodo privato . Fintanto che hai esposto solo il metodo semplice richiesto dal cliente come metodo pubblico , sei al sicuro.

tl; dr :

In realtà la domanda non è tanto "quanto dovrebbe essere riutilizzabile un metodo". La domanda è: quanto di questa riusabilità è esposta e come appare l'API. Creare un'API affidabile in grado di resistere alla prova del tempo (anche quando sorgono ulteriori requisiti in un secondo momento) è un'arte e un mestiere, e l'argomento è troppo complesso per affrontarlo qui. Dai un'occhiata a questa presentazione di Joshua Bloch o al wiki del libro di progettazione API per iniziare.


dayLightSavings()chiamare dayLightSavings(2018)non mi sembra una buona idea.
Koray Tugay,

@KorayTugay Quando la richiesta iniziale è di stampare "l'ora legale per il 2018", va bene. In realtà, questo è esattamente il metodo implementato originariamente. Se dovesse stampare l '"ora legale per l' anno in corso , allora ovviamente chiameresti dayLightSavings(computeCurrentYear());...
Marco13,

0

Una buona regola empirica è: il tuo metodo dovrebbe essere riutilizzabile come ... riutilizzabile.

Se ti aspetti di chiamare il tuo metodo solo in un punto, dovrebbe avere solo parametri noti al sito di chiamata e che non sono disponibili per questo metodo.

Se hai più chiamanti, puoi introdurre nuovi parametri purché altri chiamanti possano passare tali parametri; altrimenti hai bisogno di un nuovo metodo.

Poiché il numero di chiamanti può aumentare nel tempo, è necessario essere pronti per il refactoring o il sovraccarico. In molti casi significa che devi sentirti sicuro di selezionare l'espressione ed eseguire l'azione "Estrai parametro" del tuo IDE.


0

Risposta ultra breve: minore è l' accoppiamento o la dipendenza da altri codici del modulo generico, più riutilizzabile può essere.

Il tuo esempio dipende solo da

import java.time.*;
import java.util.Set;

quindi in teoria può essere altamente riutilizzabile.

In pratica non credo che avrai mai un secondo caso d'uso che necessita di questo codice, quindi seguendo il principio yagni non lo renderei riutilizzabile se non ci sono più di 3 diversi progetti che necessitano di questo codice.

Altri aspetti della riusabilità sono la facilità d'uso e la documentazione correlata allo sviluppo guidato dai test : è utile se si dispone di un semplice test unitario che dimostra / documenta un facile utilizzo del modulo generico come esempio di codifica per gli utenti della propria libreria.


0

Questa è una buona opportunità per dichiarare una regola che ho coniato di recente:

Essere un buon programmatore significa essere in grado di prevedere il futuro.

Certo, questo è assolutamente impossibile! Dopotutto, non sai mai con certezza quali generalizzazioni troverai utili in seguito, quali attività correlate vorrai svolgere, quali nuove funzionalità vorranno i tuoi utenti, ecc. Ma l'esperienza a volte ti dà un'idea approssimativa di ciò che potrebbe tornare utile.

Gli altri fattori con cui devi bilanciarti sono la quantità di tempo e impegno aggiuntivi necessari e quanto più complesso renderebbe il tuo codice. A volte sei fortunato, e risolvere il problema più generale è in realtà più semplice! (Almeno concettualmente, se non nella quantità di codice.) Ma più spesso, c'è un costo di complessità oltre a uno di tempo e fatica.

Quindi, se pensi che sia molto probabile una generalizzazione, spesso vale la pena farlo (a meno che non aggiunga molto lavoro o complessità); ma se sembra molto meno probabile, probabilmente non lo è (a meno che non sia molto semplice e / o semplifichi il codice).

(Per un esempio recente: la scorsa settimana mi è stata data una specifica per le azioni che un sistema dovrebbe prendere esattamente 2 giorni dopo che qualcosa è scaduto. Quindi, naturalmente, ho fatto del periodo di 2 giorni un parametro. Questa settimana gli uomini d'affari sono stati felici, mentre stavo per chiedere quel miglioramento! Sono stato fortunato: è stato un cambiamento facile, e ho immaginato che probabilmente sarebbe stato voluto. Spesso è più difficile da giudicare. Ma vale comunque la pena provare a prevedere, e l'esperienza è spesso una buona guida .)


0

In primo luogo, la migliore risposta a "Come faccio a sapere quanto dovrebbero essere riutilizzabili i miei metodi?" È "esperienza". Fai questo alcune migliaia di volte e in genere ottieni la risposta giusta. Ma come teaser, posso darti l'ultimo linea di questa risposta: il tuo cliente ti dirà quanta flessibilità e quanti livelli di generalizzazione dovresti cercare.

Molte di queste risposte hanno consigli specifici. Volevo dare qualcosa di più generico ... perché l'ironia è troppo divertente da trasmettere!

Come alcune delle risposte hanno notato, la generalità è costosa. Tuttavia, in realtà non lo è. Non sempre. Comprendere le spese è essenziale per giocare al gioco della riusabilità.

Mi concentro sul mettere le cose su una scala da "irreversibile" a "reversibile". È una scala regolare. L'unica cosa veramente irreversibile è "il tempo speso nel progetto". Non recupererai mai quelle risorse. Leggermente meno reversibili potrebbero essere situazioni "manette d'oro" come l'API di Windows. Le funzionalità obsolete rimangono nell'API per decenni perché il modello di business di Microsoft lo richiede. Se hai clienti la cui relazione verrebbe danneggiata in modo permanente annullando alcune funzionalità API, questo dovrebbe essere considerato irreversibile. Guardando l'altra estremità della scala, hai cose come il codice prototipo. Se non ti piace dove va, puoi semplicemente buttarlo via. Un po 'meno reversibili potrebbero essere le API per uso interno. Possono essere refactored senza disturbare un cliente,tempo (la risorsa più irreversibile di tutte!)

Quindi mettili su una scala. Ora puoi applicare un'euristica: più qualcosa è reversibile, più puoi usarla per attività future. Se qualcosa è irreversibile, utilizzalo solo per compiti concreti guidati dal cliente. Questo è il motivo per cui vedi principi come quelli della programmazione estrema che suggeriscono di fare solo ciò che il cliente chiede e niente di più. Questi principi sono bravi a fare in modo di non fare qualcosa di cui ti penti.

Cose come il principio DRY suggeriscono un modo per spostare questo equilibrio. Se ti ritrovi a ripetere te stesso, questa è un'opportunità per creare ciò che è fondamentalmente un'API interna. Nessun cliente lo vede, quindi puoi sempre cambiarlo. Una volta che hai questa API interna, ora puoi iniziare a giocare con cose lungimiranti. Quante diverse attività basate sul fuso orario pensi che tua moglie ti darà? Hai altri clienti che potrebbero desiderare attività basate sul fuso orario? La tua flessibilità qui viene acquisita dalle esigenze concrete dei tuoi attuali clienti e supporta le potenziali richieste future dei futuri clienti.

Questo approccio di pensiero stratificato, che proviene naturalmente da DRY, fornisce naturalmente la generalizzazione che desideri senza sprechi. Ma c'è un limite? Certo che c'è. Ma per vederlo, devi vedere la foresta per gli alberi.

Se hai molti livelli di flessibilità, spesso portano ad una mancanza di controllo diretto degli strati che devono affrontare i tuoi clienti. Ho avuto un software in cui ho avuto il brutale compito di spiegare a un cliente perché non possono avere quello che vogliono a causa della flessibilità integrata in 10 livelli che non avrebbero mai dovuto vedere. Ci siamo scritti in un angolo. Ci siamo annodati con tutta la flessibilità di cui pensavamo di aver bisogno.

Quindi, quando stai facendo questo trucco generalizzazione / DEUMIDIFICAZIONE, mantieni sempre un impulso sul tuo cliente . Cosa pensi che chiederà tua moglie dopo? Ti stai mettendo in condizione di soddisfare queste esigenze? Se hai il talento, il cliente ti dirà efficacemente le sue esigenze future. Se non hai il talento, beh, la maggior parte di noi si affida alle congetture! (specialmente con i coniugi!) Alcuni clienti vorranno una grande flessibilità e saranno disposti ad accettare il costo aggiuntivo del tuo sviluppo con tutti questi livelli perché beneficiano direttamente della flessibilità di quei livelli. Altri clienti hanno piuttosto fissato requisiti incrollabili e preferiscono che lo sviluppo sia più diretto. Il tuo cliente ti dirà quanta flessibilità e quanti livelli di generalizzazione dovresti cercare.


Bene, ci dovrebbero essere altre persone che lo hanno fatto 10000 volte, quindi perché dovrei farlo 10000 volte e fare esperienza quando posso imparare dagli altri? Perché la risposta sarà diversa per ogni individuo, quindi le risposte degli esperti non sono applicabili a me? Inoltre Your customer will tell you how much flexibility and how many layers of generalization you should seek.che mondo è questo?
Koray Tugay,

@KorayTugay È un mondo degli affari. Se i tuoi clienti non ti stanno dicendo cosa fare, allora non stai ascoltando abbastanza. Certo, non te lo diranno sempre a parole, ma te lo diranno in altri modi. L'esperienza ti aiuta ad ascoltare i loro messaggi più sottili. Se non hai ancora l'abilità, trova qualcuno nella tua azienda che abbia l'abilità di ascoltare quei sottili suggerimenti dei clienti e falli fare attenzione. Qualcuno avrà quell'abilità, anche se sono l'amministratore delegato o nel marketing.
Cort Ammon,

Nel tuo caso specifico, se non fossi riuscito a portare fuori la spazzatura perché eri troppo impegnato a codificare una versione generalizzata di questo problema di fuso orario invece di hackerare insieme la soluzione specifica, come si sentirebbero i tuoi clienti?
Cort Ammon,

Quindi sei d'accordo sul fatto che il mio primo approccio è stato quello giusto, hardcoding 2018 nel primo take invece di parametrizzare l'anno? (a proposito, non sto ascoltando il mio cliente, credo, l'esempio dell'immondizia. Questo è conoscere il tuo cliente .. Anche se ottieni supporto da Oracle, non ci sono messaggi sottili da ascoltare quando dice che ho bisogno di un elenco di luce del giorno modifiche per il 2018.) Grazie per il tuo tempo e risposta tra l'altro.
Koray Tugay,

@KorayTugay Senza conoscere ulteriori dettagli, direi che l'hardcoding era l'approccio giusto. Non avevi modo di sapere se avresti avuto bisogno del futuro codice DLS, né hai idea di quale tipo di richiesta potrebbe dare in seguito. E se il tuo cliente sta provando a metterti alla prova, ottengono ciò che ottengono = D
Cort Ammon,

0

nessun ingegnere informatico addestrato eticamente acconsentirebbe mai a scrivere una procedura di DestroyBaghdad. L'etica professionale di base gli richiederebbe invece di scrivere una procedura di DestroyCity, alla quale Baghdad potrebbe essere dato come parametro.

Questo è qualcosa che è noto nei circoli dell'ingegneria del software avanzato come uno "scherzo". Le barzellette non devono essere ciò che chiamiamo "vero", anche se per essere divertenti in genere devono accennare a qualcosa di vero.

In questo caso particolare, lo "scherzo" non è "vero". Il lavoro richiesto per scrivere una procedura generale per distruggere qualsiasi città è, possiamo tranquillamente supporre, ordini di grandezza oltre a quelli richiesti per distruggere una specificacittà. Altrimenti, chiunque abbia distrutto una o una manciata di città (il biblico Giosuè, diciamo, o il presidente Truman) potrebbe banalmente generalizzare ciò che hanno fatto ed essere in grado di distruggere qualsiasi città a volontà. Questo in realtà non è il caso. I metodi che quelle due persone hanno usato per distruggere un piccolo numero di città specifiche non funzionerebbero necessariamente su qualsiasi città in qualsiasi momento. Un'altra città le cui mura avevano una frequenza di risonanza diversa o le cui difese aeree ad alta quota erano piuttosto migliori, avrebbe bisogno di cambiamenti di approccio minori o fondamentali (una tromba di diversa tonalità o un razzo).

Questo porta anche alla manutenzione del codice contro i cambiamenti nel tempo: ora ci sono molte città che non rientrerebbero in nessuno di questi approcci, grazie ai moderni metodi di costruzione e al radar onnipresente.

Sviluppare e testare un mezzo completamente generale che distruggerà qualsiasi città, prima che tu accetti di distruggere una sola città, è un approccio disperatamente inefficiente. Nessun ingegnere del software con una formazione etica cercherebbe di generalizzare un problema in misura tale da richiedere ordini di grandezza più lavoro di quello che il loro datore di lavoro / cliente deve effettivamente pagare, senza necessità dimostrate.

Quindi cosa è vero? A volte l'aggiunta di generalità è banale. Dovremmo quindi aggiungere sempre generalità quando è banale farlo? Direi ancora "no, non sempre", a causa del problema di manutenzione a lungo termine. Supponendo che al momento della scrittura, tutte le città siano sostanzialmente le stesse, quindi vado avanti con DestroyCity. Una volta che ho scritto questo, insieme ai test di integrazione che (a causa dello spazio finitamente enumerabile degli input) iterano su ogni città conosciuta e assicurano che la funzione funzioni su ognuna (non sono sicuro di come funzioni. Probabilmente chiama City.clone () e distrugge il clone? Comunque).

In pratica la funzione viene utilizzata esclusivamente per distruggere Baghdad, supponiamo che qualcuno costruisca una nuova città che sia resistente alle mie tecniche (è in profondità nel sottosuolo o qualcosa del genere). Ora ho un test di integrazione fallito per un caso d'uso che non esiste nemmeno , e prima di poter continuare la mia campagna di terrore contro gli innocenti civili iracheni, devo capire come distruggere Subterrania. Non importa se questo è etico o no, è stupido e una perdita del mio tempo eccentrico .

Quindi, vuoi davvero una funzione in grado di produrre l'ora legale per qualsiasi anno, solo per produrre dati per il 2018? Forse, ma richiederà sicuramente un piccolo sforzo extra solo per mettere insieme i casi di test. Potrebbe essere necessario un grande sforzo per ottenere un database del fuso orario migliore di quello che hai effettivamente. Quindi, ad esempio, nel 1908, la città di Port Arthur, in Ontario, ebbe inizio un periodo di ora legale il 1 ° luglio. È nel database del fuso orario del tuo sistema operativo? Pensato di no, quindi la tua funzione generalizzata è sbagliata . Non c'è nulla di particolarmente etico nello scrivere codice che promette che non può mantenere.

Va bene, quindi, con avvertenze appropriate è facile scrivere una funzione che faccia i fusi orari per un intervallo di anni, diciamo oggi 1970. Ma è altrettanto facile prendere la funzione che hai effettivamente scritto e generalizzarla per parametrizzare l'anno. Quindi non è davvero più etico / sensato generalizzare ora, è fare quello che hai fatto e quindi generalizzare se e quando ne hai bisogno.

Tuttavia, se sapessi perché tua moglie voleva controllare questo elenco di DST, allora avresti un'opinione informata se è probabile che possa porre di nuovo la stessa domanda nel 2019 e, in tal caso, se puoi uscire da quel ciclo dando lei una funzione che può chiamare, senza bisogno di ricompilarla. Una volta effettuata tale analisi, la risposta alla domanda "dovrebbe generalizzare agli ultimi anni" potrebbe essere "sì". Ma crei ancora un altro problema per te stesso, che è che i dati sul fuso orario in futuro sono solo provvisori, e quindi se li eseguirà per il 2019 oggi, potrebbe o meno rendersi conto che le sta dando una supposizione migliore. Quindi devi ancora scrivere un sacco di documentazione che non sarebbe necessaria per la funzione meno generale ("i dati provengono dal database del fuso orario blah blah ecco il link per vedere le loro politiche sulla spinta degli aggiornamenti blah blah"). Se rifiuti il ​​caso speciale, per tutto il tempo che lo fai, non può andare avanti con l'attività per la quale aveva bisogno dei dati del 2018, a causa di alcune sciocchezze sul 2019 che non le interessano ancora.

Non fare cose difficili senza pensarci bene, solo perché ti è stato detto uno scherzo. È utile? È abbastanza economico per quel grado di utilità?


0

Questa è una linea facile da tracciare perché la riutilizzabilità, come definita dagli astronauti dell'architettura, è una parata.

Quasi tutto il codice creato dagli sviluppatori di applicazioni è estremamente specifico per il dominio. Questo non è il 1980. Quasi tutto ciò che vale la pena è già in un quadro.

Astrazioni e convenzioni richiedono documentazione e sforzo di apprendimento. Smetti gentilmente di crearne di nuovi solo per il gusto di farlo. (Sto guardando voi , persone JavaScript!)

Concediamo l'immaginabile fantasia che tu abbia trovato qualcosa che dovrebbe essere veramente nella tua cornice di scelta. Non puoi semplicemente rompere il codice come fai di solito. Oh no, è necessaria la copertura del test non solo per l'uso previsto, ma anche per le deviazioni dall'uso previsto, per tutti i casi limite noti, per tutte le modalità di guasto immaginabili, casi di test per diagnostica, dati di test, documentazione tecnica, documentazione utente, gestione delle versioni, supporto script, test di regressione, gestione delle modifiche ...

Il tuo datore di lavoro è felice di pagare per tutto ciò? Sto per dire di no.

L'astrazione è il prezzo che paghiamo per la flessibilità. Rende il nostro codice più complesso e più difficile da capire. A meno che la flessibilità non soddisfi un'esigenza reale e attuale, non farlo, perché YAGNI.

Diamo un'occhiata a un esempio del mondo reale che ho appena dovuto affrontare: HTMLRenderer. Non è stato ridimensionato correttamente quando ho provato a eseguire il rendering nel contesto del dispositivo di una stampante. Mi ci è voluto tutto il giorno per scoprire che per impostazione predefinita stava usando GDI (che non si ridimensiona) piuttosto che GDI + (che fa, ma non antialias) perché ho dovuto superare sei livelli di indiretta in due assiemi prima di trovare codice che ha fatto qualsiasi cosa .

In questo caso perdonerò l'autore. L'astrazione è effettivamente necessaria perché si tratta di un codice di framework che ha come target cinque target di rendering molto diversi: WinForms, WPF, dotnet Core, Mono e PdfSharp.

Ma questo sottolinea solo il mio punto: quasi sicuramente non stai facendo qualcosa di estremamente complesso (rendering HTML con fogli di stile) indirizzato a più piattaforme con un obiettivo dichiarato di alte prestazioni su tutte le piattaforme.

Il tuo codice è quasi sicuramente l'ennesimo griglia di database con regole aziendali che si applicano solo al tuo datore di lavoro e regole fiscali che si applicano solo nel tuo stato, in un'applicazione che non è in vendita.

Tutta questa indiretta risolve un problema che non hai e rende il tuo codice molto più difficile da leggere, il che aumenta notevolmente il costo di manutenzione ed è un enorme disservizio per il tuo datore di lavoro. Fortunatamente le persone che dovrebbero lamentarsi di questo non sono in grado di capire cosa stai facendo loro.

Una contro argomentazione è che questo tipo di astrazione supporta lo sviluppo guidato da test, ma penso che anche il TDD sia un punto fermo, perché presuppone che l'azienda abbia una comprensione chiara, completa e corretta dei suoi requisiti. TDD è ottimo per la NASA e il software di controllo per attrezzature mediche e auto a guida autonoma, ma è troppo costoso per tutti gli altri.


Per inciso, non è possibile prevedere tutti gli risparmi di luce del giorno nel mondo. Israele in particolare ha circa 40 transizioni ogni anno che saltano dappertutto perché non possiamo avere persone che pregano nel momento sbagliato e dio non fa l'ora legale.


Anche se sono d'accordo con quello che hai detto su astrazioni e sforzi di apprendimento, e in particolare quando si rivolge all'abominio che si chiama "JavaScript", sono fortemente in disaccordo con la prima frase. La riutilizzabilità può avvenire a molti livelli, può andare troppo in là e il codice usa e getta può essere scritto da programmatori usa e getta. Ma ci sono persone abbastanza idealiste da mirare almeno al codice riutilizzabile. Un peccato per te se non vedi i benefici che questo può avere.
Marco13,

@ Marco13 - Poiché la tua obiezione è ragionevole, espanderò il punto.
Peter Wone,

-3

Se stai usando almeno java 8, dovresti scrivere la classe WorldTimeZones per fornire quella che in sostanza sembra essere una raccolta di fusi orari.

Quindi aggiungere un metodo di filtro (filtro predicato) alla classe WorldTimeZones. Ciò fornisce al chiamante la possibilità di filtrare tutto ciò che desidera passando un'espressione lambda come parametro.

In sostanza, il metodo del filtro singolo supporta il filtro su tutto ciò che è contenuto nel valore passato al predicato.

In alternativa, aggiungi un metodo stream () alla classe WorldTimeZones che produce un flusso di fusi orari quando viene chiamato. Quindi il chiamante può filtrare, mappare e ridurre come desiderato senza che tu scriva alcuna specializzazione.


3
Queste sono buone idee di generalizzazione, tuttavia questa risposta manca completamente il segno su ciò che viene posto nella domanda. La domanda non è su come generalizzare al meglio la soluzione, ma su dove tracciare la linea di generalizzazioni e su come pesare queste considerazioni eticamente.
maple_shaft

Quindi sto dicendo che dovresti valutarli in base al numero di casi d'uso che supportano modificati dalla complessità della creazione. Un metodo che supporta un caso d'uso non è prezioso come un metodo che supporta 20 casi d'uso. D'altra parte, se tutto ciò che sai è un caso d'uso, e ci vogliono 5 minuti per codificarlo - provalo. Spesso i metodi di codifica che supportano usi specifici ti informano sul modo di generalizzare.
Rodney P. Barbati,
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.