Programmazione per l'uso futuro delle interfacce


42

Ho un collega seduto accanto a me che ha progettato un'interfaccia come questa:

public interface IEventGetter {

    public List<FooType> getFooList(String fooName, Date start, Date end)
        throws Exception;
    ....

}

Il problema è che in questo momento non stiamo usando questo parametro "end" da nessuna parte nel nostro codice, è solo lì perché potremmo doverlo usare un po 'di tempo in futuro.

Stiamo cercando di convincerlo che è una cattiva idea inserire parametri in interfacce che non servono al momento, ma continua a insistere sul fatto che molto lavoro dovrà essere fatto se implementiamo l'uso della data "fine" un po 'di tempo più tardi e poi adattare tutto il codice.

Ora, la mia domanda è: ci sono fonti che stanno trattando un argomento come questo dei guru del codice "rispettati" a cui possiamo collegarlo?


29
"La previsione è molto difficile, soprattutto per il futuro."
Jörg W Mittag,

10
Parte del problema è che non è l'interfaccia migliore per cominciare. Ottenere Foos e filtrarli sono due problemi separati. Quell'interfaccia ti sta costringendo a filtrare l'elenco in un modo particolare; un approccio molto più generale è quello di passare a una funzione che decide se includere il foo. Tuttavia, questo è elegante solo se si ha accesso a Java 8.
Doval,

5
Contrastare è punto. Rendi semplicemente quel metodo che accetta il tipo personalizzato che per ora contiene solo ciò di cui hai bisogno e potrebbe eventualmente aggiungere il endparametro a quell'oggetto e persino predefinito per non rompere il codice
Rémi

3
Con i metodi predefiniti di Java 8 non c'è motivo di aggiungere argomenti non necessari. Un metodo può essere aggiunto in seguito con un'implementazione predefinita che semplici chiamate l'definiti con con, ad esempio, null. Le classi di implementazione possono quindi sostituire, se necessario.
Boris the Spider,

1
@Doval Non conosco Java, ma in .NET a volte vedrai cose come evitare di esporre le stranezze di IQueryable(può solo prendere determinate espressioni) al codice al di fuori del DAL
Ben Aaronson,

Risposte:


62

Invitalo a conoscere YAGNI . La parte logica della pagina di Wikipedia può essere particolarmente interessante qui:

Secondo coloro che sostengono l'approccio YAGNI, la tentazione di scrivere codice che al momento non è necessario, ma che potrebbe essere in futuro, presenta i seguenti svantaggi:

  • Il tempo impiegato viene impiegato per aggiungere, testare o migliorare la funzionalità necessaria.
  • Le nuove funzionalità devono essere sottoposte a debug, documentate e supportate.
  • Qualsiasi nuova funzione impone vincoli su ciò che può essere fatto in futuro, quindi una funzione non necessaria può impedire l'aggiunta di funzioni necessarie in futuro.
  • Fino a quando la funzionalità non è effettivamente necessaria, è difficile definire completamente cosa dovrebbe fare e testarla. Se la nuova funzionalità non è definita e testata correttamente, potrebbe non funzionare correttamente, anche se alla fine è necessaria.
  • Porta a codice gonfio; il software diventa più grande e più complicato.
  • A meno che non ci siano specifiche e un qualche tipo di controllo di revisione, la funzione potrebbe non essere nota ai programmatori che potrebbero farne uso.
  • L'aggiunta della nuova funzionalità può suggerire altre nuove funzionalità. Se anche queste nuove funzionalità sono implementate, ciò potrebbe comportare un effetto valanga verso lo scorrimento delle funzionalità.

Altri possibili argomenti:

  • "L'80% del costo della vita di un software è destinato alla manutenzione" . Scrivere il codice giusto in tempo riduce il costo della manutenzione: bisogna mantenere meno codice e concentrarsi sul codice effettivamente necessario.

  • Il codice sorgente viene scritto una volta, ma letto decine di volte. Un ulteriore argomento, non usato da nessuna parte, porterebbe a perdere tempo a capire perché esiste un argomento che non è necessario. Dato che questa è un'interfaccia con diverse possibili implementazioni rende le cose solo più difficili.

  • Il codice sorgente dovrebbe essere auto-documentato. La firma effettiva è fuorviante, dal momento che un lettore potrebbe pensare che endinfluisca sul risultato o sull'esecuzione del metodo.

  • Le persone che scrivono implementazioni concrete di questa interfaccia potrebbero non capire che l'ultimo argomento non dovrebbe essere usato, il che porterebbe a approcci diversi:

    1. Non ho bisogno end, quindi ignorerò semplicemente il suo valore,

    2. Non ho bisogno end, quindi lancerò un'eccezione se non lo è null,

    3. Non ho bisogno end, ma proverò ad usarlo in qualche modo,

    4. Scriverò un sacco di codice che potrebbe essere usato in seguito quando endsarà necessario.

Ma tieni presente che il tuo collega potrebbe avere ragione.

Tutti i punti precedenti si basano sul fatto che il refactoring è facile, quindi aggiungere un argomento in seguito non richiederà molto sforzo. Ma questa è un'interfaccia e, come interfaccia, può essere utilizzata da diversi team che contribuiscono ad altre parti del prodotto. Ciò significa che cambiare un'interfaccia potrebbe essere particolarmente doloroso, nel qual caso YAGNI in realtà non si applica qui.

La risposta di hjk offre una buona soluzione: aggiungere un metodo a un'interfaccia già utilizzata non è particolarmente difficile, ma in alcuni casi ha anche un costo sostanziale:

  • Alcuni framework non supportano i sovraccarichi. Ad esempio e se ricordo bene (correggimi se sbaglio), WCF .NET non supporta i sovraccarichi.

  • Se l'interfaccia ha molte implementazioni concrete, aggiungere un metodo all'interfaccia richiederebbe di esaminare tutte le implementazioni e aggiungere anche il metodo lì.


4
Penso che sia anche importante considerare la probabilità che questa funzione appaia in futuro. Se sai per certo che verrà aggiunto ma non ora per qualsiasi motivo, direi che è un buon argomento da tenere a mente poiché non è solo una funzionalità "nel caso".
Jeroen Vannevel,

3
@JeroenVannevel: certamente, ma è molto speculativo. Dalla mia esperienza, la maggior parte delle funzionalità che credevo sarebbero state implementate sono state cancellate o ritardate per sempre. Ciò include funzionalità che sono state inizialmente evidenziate come molto importanti dalle parti interessate.
Arseni Mourzenko,

26

ma continua a insistere che molto lavoro dovrà essere fatto se implementiamo l'uso della data di "fine" qualche tempo dopo e dovremo adattare tutto il codice allora.

(qualche tempo dopo)

public class EventGetter implements IEventGetter {

    private static final Date IMPLIED_END_DATE_ASOF_20140711 = new Date(Long.MAX_VALUE); // ???

    @Override
    public List<FooType> getFooList(String fooName, Date start) throws Exception {
        return getFooList(fooName, start, IMPLIED_END_DATE_ASOF_20140711);
    }

    @Override
    public List<FooType> getFooList(String fooName, Date start, Date end) throws Exception {
        // Final implementation goes here
    }
}

Questo è tutto ciò di cui hai bisogno, sovraccarico di metodo. Il metodo aggiuntivo in futuro può essere introdotto in modo trasparente senza influire sulle chiamate al metodo esistente.


6
Ad eccezione del fatto che la modifica implica che non è più un'interfaccia e che non sarà possibile utilizzare nei casi in cui un oggetto deriva già da un oggetto e implementa l'interfaccia. È un problema banale se tutte le classi che implementano l'interfaccia sono nello stesso progetto (insegui errori di compilazione). Se si tratta di una libreria utilizzata da più progetti, l'aggiunta del campo provoca confusione e lavoro per altre persone in periodi sporadici nei prossimi x mesi / anni.
Kieveli,

1
Se il team di OP (salva il collega) crede davvero che il endparametro non sia necessario ora e abbia continuato a utilizzare il endmetodo senza-uso in tutti i progetti, quindi l'aggiornamento dell'interfaccia è il minimo dei loro problemi perché diventa una necessità aggiornare il classi di implementazione. La mia risposta si rivolge specificamente alla parte "adatta tutto il codice", perché sembra che il collega stia pensando di dover aggiornare i chiamanti del metodo originale in tutti i progetti per introdurre un terzo parametro predefinito / fittizio .
hjk,

18

[Esistono] guru del codice "rispettati" a cui possiamo collegarlo [per convincerlo]?

Gli appelli all'autorità non sono particolarmente convincenti; meglio presentare un argomento valido indipendentemente da chi lo ha detto.

Ecco una reductio ad absurdum che dovrebbe convincere o dimostrare che il tuo collega è bloccato per essere "giusto", indipendentemente dalla sensibilità:


Ciò di cui hai veramente bisogno è

getFooList(String fooName, Date start, Date end, Date middle, 
           Date one_third, JulianDate start, JulianDate end,
           KlingonDate start, KlingonDate end)

Non si sa mai quando si dovrà internazionalizzare per Klingon, quindi è meglio occuparsene ora perché richiederà un sacco di lavoro per il retrofit e i Klingon non sono noti per la loro pazienza.


22
You never know when you'll have to internationalize for Klingon. Ecco perché dovresti semplicemente passare un Calendar, e lasciare che il codice client decida se vuole inviare un GregorianCalendaro un KlingonCalendar(scommetto che alcuni l'hanno già fatto). Duh. :-p
SJuan76,

3
Che dire di StarDate sdStarte StarDate sdEnd? Dopo tutto, non possiamo dimenticare la Federazione ...
WernerCD,

Tutti quelli sono ovviamente sottoclassi o convertibili in Date!
Darkhogg,

Se solo fosse così semplice .
ms

@msw, non sono sicuro che chiedesse un "appello all'autorità" quando ha chiesto collegamenti a "rispettati guru del codice". Come dici tu, ha bisogno di "un argomento valido indipendentemente da chi lo ha detto". Le persone di tipo "Guru" tendono a conoscerne molte. Anche tu hai dimenticato HebrewDate starte HebrewDate end.
trysis

12

Dal punto di vista dell'ingegneria del software, credo che la soluzione corretta per questo tipo di problemi sia nel modello del costruttore. Questo è sicuramente un link degli autori di 'guru' per il tuo collega http://en.wikipedia.org/wiki/Builder_pattern .

Nel modello builder, l'utente crea un oggetto che contiene i parametri. Questo contenitore di parametri verrà quindi passato al metodo. Ciò si occuperà di eventuali estensioni e sovraccarichi dei parametri di cui il tuo collega avrebbe bisogno in futuro, rendendo il tutto molto stabile quando è necessario apportare modifiche.

Il tuo esempio diventerà:

public interface IEventGetter {
    public List<FooType> getFooList(ISearchFooRequest req) {
        throws Exception;
    ....
    }
}

public interface ISearchFooRequest {
        public String getName();
        public Date getStart();
        public Date getEnd();
        public int getOffset();
        ...
    }
}

public class SearchFooRequest implements ISearchFooRequest {

    public static SearchFooRequest buildDefaultRequest(String name, Date start) {
        ...
    }

    public String getName() {...}
    public Date getStart() {...}
    ...
    public void setEnd(Date end) {...}
    public void setOffset(int offset) {...}
    ...
}

3
Questo è un buon approccio generale, ma in questo caso sembra solo spingere il problema da IEventGettera ISearchFootRequest. Invece suggerirei qualcosa di simile public IFooFilteral membro public bool include(FooType item)che restituisce se includere o meno l'elemento. Quindi le implementazioni di singole interfacce possono decidere come faranno quel filtro
Ben Aaronson,

@Ben Aaronson Ho appena modificato il codice per mostrare come viene creato l'oggetto parametro Request
InformedA

Qui il costruttore non è necessario (e francamente, un modello abusato / sconsigliato in generale); non ci sono regole complesse su come i parametri devono essere impostati, quindi un oggetto parametro va perfettamente bene se c'è una legittima preoccupazione sul fatto che i parametri del metodo siano complessi o frequentemente modificati. E sono d'accordo con @BenAaronson, anche se il punto di usare un oggetto parametro non è quello di includere i parametri non necessari immediatamente, ma renderli molto facili da aggiungere in seguito (anche se ci sono implementazioni di interfacce multiple).
Aaronaught,

7

Non ti serve ora, quindi non aggiungerlo. Se ne hai bisogno in seguito, estendi l'interfaccia:

public interface IEventGetter {

    public List<FooType> getFooList(String fooName, Date start)
         throws Exception;
    ....

}

public interface IBoundedEventGetter extends IEventGetter {

    public List<FooType> getFooList(String fooName, Date start, Date end)
        throws Exception;
    ....

}

+1 per non modificare l'interfaccia esistente (e probabilmente già testata / spedita).
Sebastian Godelet,

4

Nessun principio di progettazione è assoluto, quindi mentre per lo più concordo con le altre risposte, ho pensato di interpretare l'avvocato del diavolo e discutere alcune condizioni in base alle quali prenderei in considerazione l'accettazione della soluzione del tuo collega:

  • Se si tratta di un'API pubblica e prevedi che la funzionalità sia utile per gli sviluppatori di terze parti, anche se non la usi internamente.
  • Se ha notevoli benefici immediati, non solo quelli futuri. Se la data di fine implicita è Now(), l'aggiunta di un parametro rimuove un effetto collaterale, che presenta vantaggi per la memorizzazione nella cache e il test dell'unità. Forse consente un'implementazione più semplice. O forse è più coerente con altre API nel tuo codice.
  • Se la tua cultura di sviluppo ha una storia problematica. Se i processi o la territorialità rendono troppo difficile cambiare qualcosa di centrale come un'interfaccia, allora quello che ho visto accadere prima è che le persone implementano soluzioni alternative sul lato client invece di cambiare l'interfaccia, quindi stai cercando di mantenere una dozzina di date di fine ad hoc filtri anziché uno. Se questo genere di cose accade molto nella tua azienda, ha senso dedicare un po 'più impegno alla correzione futura. Non fraintendetemi, è meglio cambiare i processi di sviluppo, ma di solito è più facile a dirsi che a farsi .

Detto questo, nella mia esperienza il più grande corollario di YAGNI è YDKWFYN: non sai di quale forma avrai bisogno (sì, ho appena inventato quell'acronimo). Anche se la necessità di alcuni parametri limite può essere relativamente prevedibile, potrebbe assumere la forma di un limite di pagina, o un numero di giorni, o un valore booleano che specifica di utilizzare la data di fine da una tabella delle preferenze dell'utente o qualsiasi numero di cose.

Dato che non hai ancora il requisito, non hai modo di sapere quale tipo dovrebbe essere quel parametro. Spesso finisci per avere un'interfaccia scomoda che non è abbastanza adatta alle tue esigenze, o devi comunque cambiarla.


2

Non ci sono abbastanza informazioni per rispondere a questa domanda. Dipende da cosa getFooListeffettivamente fa e da come lo fa.

Ecco un esempio evidente di un metodo che dovrebbe supportare un parametro aggiuntivo, usato o no.

void CapitalizeSubstring (String capitalize_me, int start_index);

L'implementazione di un metodo che opera su una raccolta in cui è possibile specificare l'inizio ma non la fine della raccolta è spesso sciocca.

Devi davvero guardare al problema stesso e chiederti se il parametro è privo di senso nel contesto dell'intera interfaccia e davvero quanto onere impone il parametro aggiuntivo.


1

Temo che il tuo collega possa in realtà avere un punto molto valido. Sebbene la sua soluzione non sia in realtà la migliore.

Dalla sua interfaccia proposta è chiaro che

public List<FooType> getFooList(String fooName, Date start, Date end) throws Exception;

sta restituendo istanze trovate entro un intervallo di tempo. Se i client attualmente non utilizzano il parametro end, ciò non cambia il fatto che si aspettano istanze trovate entro un intervallo di tempo. Significa solo che attualmente tutti i client utilizzano intervalli aperti (dall'inizio alla eternità)

Quindi un'interfaccia migliore sarebbe:

public List<FooType> getFooList(String fooName, Interval interval) throws Exception;

Se si fornisce l'intervallo con un metodo di fabbrica statico:

public static Interval startingAt(Date start) { return new Interval(start, null); }

quindi i clienti non sentiranno nemmeno la necessità di specificare un orario di fine.

Allo stesso tempo, la tua interfaccia trasmette più correttamente ciò che fa, poiché getFooList(String, Date)non comunica che ciò si occupa di un intervallo.

Nota che il mio suggerimento deriva da ciò che il metodo attualmente fa, non da ciò che dovrebbe o potrebbe fare in futuro, e come tale il principio YAGNI (che è davvero molto valido) non si applica qui.


0

L'aggiunta di un parametro non utilizzato è confusa. Le persone potrebbero chiamare quel metodo supponendo che questa funzione funzioni.

Non lo aggiungerei. È banale aggiungerlo successivamente usando un refactoring e riparando meccanicamente i siti di chiamata. In una lingua tipicamente statica questo è facile da fare. Non è necessario aggiungerlo con entusiasmo.

Se c'è una ragione per avere quel parametro è possibile evitare la confusione: Aggiungi un'asserzione per l'attuazione di tale interfaccia per imporre che questo parametro sia passato come valore di default. Se qualcuno utilizza accidentalmente quel parametro almeno durante il test noterà immediatamente che questa funzione non è implementata. Questo elimina il rischio di far cadere un bug in produzione.

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.