L'esclusione dei metodi concreti ha un odore di codice?


31

È vero che ignorare metodi concreti è un odore di codice? Perché penso che se hai bisogno di scavalcare metodi concreti:

public class A{
    public void a(){
    }
}

public class B extends A{
    @Override
    public void a(){
    }
}

può essere riscritto come

public interface A{
    public void a();
}

public class ConcreteA implements A{
    public void a();
}

public class B implements A{
    public void a(){
    }
}

e se B vuole riutilizzare a () in A, può essere riscritto come:

public class B implements A{
    public ConcreteA concreteA;
    public void a(){
        concreteA.a();
    }
}

che non richiede l'ereditarietà per sovrascrivere il metodo, è vero?



4
Sottovalutato perché (nei commenti sotto), Telastyn e Doc Brown concordano sull'esecuzione dell'override, ma danno risposte sì / no opposte alla domanda principale perché non sono d'accordo sul significato di "odore di codice". Quindi una domanda che intendeva riguardare la progettazione del codice è stata fortemente accoppiata a un pezzo di gergo. E penso inutilmente così, dal momento che la domanda riguarda specificamente una tecnica contro un'altra.
Steve Jessop,

5
La domanda non è taggata Java, ma il codice sembra essere Java. In C # (o C ++), dovresti usare la virtualparola chiave per supportare le sostituzioni. Quindi, il problema è reso più (o meno) ambiguo dai dettagli della lingua.
Erik Eidt,

1
Sembra che quasi tutte le funzionalità del linguaggio di programmazione finiscano inevitabilmente per essere classificate come "odori di codice" dopo abbastanza anni.
Brandon,

2
Sento che il finalmodificatore esiste esattamente per situazioni come queste. Se il comportamento del metodo è tale da non essere progettato per essere ignorato, contrassegnarlo come final. Altrimenti, aspettati che gli sviluppatori possano scegliere di sovrascriverlo.
Martin Tuskevicius,

Risposte:


34

No, non è un odore di codice.

  • Se una classe non è definitiva, consente di essere sottoclassata.
  • Se un metodo non è definitivo, consente di ignorarlo.

Spetta alle responsabilità di ogni classe considerare attentamente se la sottoclasse è appropriata e quali metodi possono essere ignorati .

La classe può definire se stessa o qualsiasi metodo come definitivo, oppure può imporre restrizioni (modificatori di visibilità, costruttori disponibili) su come e dove viene sottoclassata.

Il solito caso di metodi di sostituzione è un'implementazione predefinita nella classe base che può essere personalizzata o ottimizzata nella sottoclasse (specialmente in Java 8 con l'avvento dei metodi predefiniti nelle interfacce).

class A {
    public String getDescription(Element e) {
        // return default description for element
    }
}

class B extends A {
    public String getDescription(Element e) {
        // return customized description for element
    }
}

Un'alternativa per superiori comportamento è il modello di strategia , in cui il comportamento è astratto come interfaccia, e l'implementazione può essere impostato nella classe.

interface DescriptionProvider {
    String getDescription(Element e);
}

class A {
    private DescriptionProvider provider=new DefaultDescriptionProvider();

    public final String getDescription(Element e) {
       return provider.getDescription(e);
    }

    public final void setDescriptionProvider(@NotNull DescriptionProvider provider) {
        this.provider=provider;
    }
}

class B extends A {
    public B() {
        setDescriptionProvider(new CustomDescriptionProvider());
    }
}

Capisco che, nel mondo reale, ignorare un metodo concreto potrebbe essere l'odore del codice per alcuni. Ma, mi piace questa risposta perché sembra più specifica per me.
Darkwater23

Nel tuo ultimo esempio, non dovresti Aimplementare DescriptionProvider?
Scoperto il

@Spotted: A potrebbe implementare DescriptionProvider, e quindi essere il provider di descrizione predefinito. Ma l'idea qui è la separazione delle preoccupazioni: ha Abisogno della descrizione (anche alcune altre classi potrebbero), mentre altre implementazioni le forniscono.
Peter Walser,

2
Ok, sembra più il modello di strategia .
Scoperto il

@Spotted: hai ragione, l'esempio illustra il modello di strategia, non il modello di decorazione (un decoratore aggiungerebbe comportamento a un componente). Correggerò la mia risposta, grazie.
Peter Walser,

25

È vero che ignorare metodi concreti è un odore di codice?

Sì, in generale, l'override di metodi concreti è un odore di codice.

Poiché il metodo di base ha un comportamento ad esso associato che gli sviluppatori di solito rispettano, la modifica che porta a bug quando l'implementazione fa qualcosa di diverso. Peggio ancora, se cambiano il comportamento, l'implementazione precedentemente corretta potrebbe aggravare il problema. E in generale, è abbastanza difficile rispettare il principio di sostituzione di Liskov per metodi concreti non banali.

Ora, ci sono casi in cui questo può essere fatto ed è buono. I metodi semi-banali possono essere superati in modo abbastanza sicuro. Metodi di base che esistono solo come un "default sano" tipo di comportamento che è destinata ad essere sovrascritta hanno alcuni usi buoni.

Quindi, questo mi sembra un odore - a volte buono, di solito cattivo, date un'occhiata per essere sicuro.


29
La tua spiegazione va bene, ma giungo esattamente alla conclusione opposta: "no, ignorare i metodi concreti non è un odore di codice in generale" - è solo un odore di codice quando si ignorano i metodi che non erano destinati a essere ignorati. ;-)
Doc Brown,

2
@DocBrown Exactly! Vedo anche che la spiegazione (e in particolare la conclusione) si oppone alla dichiarazione iniziale.
Tersosauros,

1
@Spotted: il controesempio più semplice è una Numberclasse con un increment()metodo che imposta il valore sul successivo valore valido. Se lo esegui in una sottoclasse in EvenNumbercui ne vengono increment()aggiunti due anziché uno (e il setter applica l'uniformità), la prima proposta del PO richiede implementazioni duplicate di ogni metodo nella classe e la seconda richiede la scrittura di metodi wrapper per chiamare i metodi nel membro. Parte dello scopo dell'ereditarietà è che le classi dei bambini chiariscano in che modo differiscono dai genitori fornendo solo quei metodi che devono essere diversi.
Blrfl,

5
@DocBrown: essere un odore di codice non implica che qualcosa sia necessariamente negativo, solo che è probabile .
mikołak,

3
@docbrown - per me, questo va di pari passo con "favorire la composizione sull'eredità". Se stai scavalcando metodi concreti, sei già in una terra dall'odore minuscolo per avere eredità. Quali sono le probabilità che stai cavalcando qualcosa che non dovresti essere? Sulla base della mia esperienza, penso che sia probabile. YMMV.
Telastyn,

7

se è necessario sostituire metodi [...] concreti, è possibile riscriverlo come

Se può essere riscritto in questo modo, significa che hai il controllo su entrambe le classi. Ma poi sai se hai progettato la classe A per essere derivata in questo modo e se lo hai fatto, non è un odore di codice.

Se, d'altra parte, non hai il controllo sulla classe base, non puoi riscrivere in quel modo. Quindi o la classe è stata progettata per essere derivata in questo modo, nel qual caso basta andare avanti, non è un odore di codice, oppure no, nel qual caso hai due opzioni: cercare un'altra soluzione del tutto o andare avanti comunque , perché il lavoro ha la meglio sulla purezza.


Se la classe A fosse stata progettata per essere derivata, non avrebbe più senso avere un metodo astratto per esprimere chiaramente l'intento? Come chi ha la precedenza sul metodo può sapere che il metodo è stato progettato in questo modo?
Scoperto l'

@Spotted, ci sono molti casi in cui vengono fornite le cam di implementazioni predefinite e ogni specializzazione deve solo sostituirne alcune, sia per estendere la funzionalità sia per l'efficienza. Un esempio estremo sono i visitatori. L'utente sa come i metodi sono stati progettati dalla documentazione; deve essere lì per spiegare cosa ci si aspetta dall'override in entrambi i casi.
Jan Hudec,

Capisco il tuo punto di vista, tuttavia penso che sia pericoloso che l'unico modo in cui uno sviluppatore ha, per conoscere un metodo può essere ignorato, è leggere il documento perché: 1) se deve essere superato, non ho alcuna garanzia che sia sarà fatto. 2) se non deve essere sostituito, qualcuno lo farà per sua comodità 3) non tutti leggono il documento. Inoltre, non ho mai affrontato un caso in cui dovevo sovrascrivere un intero metodo, ma solo parti (e ho finito con un modello di modello).
Scoperto il

@Spotted, 1) se deve essere sostituito, dovrebbe essere abstract; ma ci sono molti casi in cui non deve essere. 2) se deve non essere sovrascritto, dovrebbe essere final(non virtuale). Ma ci sono ancora molti casi in cui i metodi possono essere ignorati. 3) se lo sviluppatore a valle non legge i documenti, si spareranno ai piedi, ma lo faranno indipendentemente dalle misure messe in atto.
Jan Hudec,

Ok, quindi la divergenza del nostro punto di vista sta solo in una parola. Tu dici che ogni metodo dovrebbe essere astratto o finale mentre io dico che ogni metodo deve essere astratto o finale. :) Quali sono questi casi quando è necessario (e sicuro) sostituire un metodo?
Scoperto il

3

Innanzi tutto, vorrei sottolineare che questa domanda è applicabile solo in alcuni sistemi di battitura. Ad esempio, in digitazione strutturali e di battitura anatra sistemi - una classe semplicemente avere un metodo con lo stesso nome e firma avrebbe significherebbe che classe era tipo compatibile (almeno per quel particolare metodo, nel caso di battitura Duck).

Questo è (ovviamente) molto diverso dal regno dei linguaggi tipicamente statici , come Java, C ++, ecc. Così come la natura della tipizzazione Strong , usata sia in Java e C ++, sia in C # e altri.


Immagino che tu provenga da un background Java, in cui i metodi devono essere esplicitamente contrassegnati come finali se il progettista del contratto intende che non vengano ignorati. Tuttavia, in lingue come C #, è l'opposto il valore predefinito. I metodi sono (senza alcuna decorazione, parole chiave speciali) " sigillate " (C # 's versione di final), e devono essere dichiarati esplicitamente come virtualse essi sono autorizzati ad essere sovrascritta.

Se un'API o una libreria è stata progettata in questi linguaggi con un metodo concreto che è sovrascrivibile, deve (almeno in alcuni casi) avere senso che venga sovrascritto. Pertanto, non è un odore di codice ignorare un finalmetodo non sigillato / virtuale / non concreto.     (Ciò presuppone ovviamente che i progettisti dell'API stiano seguendo le convenzioni e contrassegnando come virtual(C #) o contrassegnando come final(Java) i metodi appropriati per ciò che stanno progettando.)


Guarda anche


Nella tipizzazione duck, due classi hanno la stessa firma del metodo sufficiente per essere compatibili con il tipo, oppure è anche necessario che i metodi abbiano la stessa semantica, in modo tale che siano sostituibili a qualche classe base implicita?
Wayne Conrad,

2

Sì, è perché può portare a chiamare super -pattern. Può anche denaturare lo scopo iniziale del metodo, introdurre effetti collaterali (=> bug) e far fallire i test. Utilizzeresti una classe le cui unit test sono rosse (failling)? Non lo farò.

Vado ancora oltre dicendo che l'odore iniziale del codice è quando un metodo non è abstractfinal. Perché permette di alterare (sovrascrivendo) il comportamento di un'entità costruita (questo comportamento può essere perso o alterato). Con abstracte finall'intento non è ambiguo:

  • Riassunto: è necessario fornire un comportamento
  • Finale: si sta non autorizzato a modificare il comportamento

Il modo più sicuro per aggiungere un comportamento a una classe esistente è quello che mostra il tuo ultimo esempio: usare il motivo decorativo.

public interface A{
    void a();
}

public final class ConcreteA implements A{
    public void a() {
        ...
    }
}

public final class B implements A{
    private final ConcreteA concreteA;
    public void a(){
        //Additionnal behaviour
        concreteA.a();
    }
}

9
Questo approccio in bianco e nero non è poi così utile. Pensa Object.toString()a Java ... Se così fosse abstract, molte cose predefinite non funzionerebbero e il codice non verrebbe nemmeno compilato quando qualcuno non ha ignorato il toString()metodo! Se così fosse final, le cose andrebbero anche peggio: nessuna possibilità di consentire la conversione di oggetti in stringa, ad eccezione del modo Java. Mentre parla della risposta di @Peter Walser , le implementazioni predefinite (come Object.toString()) sono importanti. Soprattutto nei metodi predefiniti di Java 8.
Tersosauros,

@Tersosauros Hai ragione, se toString()fosse uno dei due abstracto finalsarebbe un dolore. La mia opinione personale è che questo metodo è rotto e sarebbe stato meglio non averlo affatto (come equals e hashCode ). Lo scopo iniziale delle impostazioni predefinite Java è consentire l'evoluzione dell'API con retrocompatibilità.
Avvistato l'

La parola "retrocompatibilità" contiene molte sillabe.
Craig,

@Spotted Sono molto deluso dall'accettazione generale dell'eredità concreta su questo sito, mi aspettavo di meglio: / La tua risposta dovrebbe avere molti più voti
TheCatWhisperer

1
@TheCatWhisperer sono d'accordo, ma d'altra parte mi ci è voluto così tanto tempo per scoprire i vantaggi dei sottotitoli dell'uso della decorazione rispetto all'eredità concreta (in effetti è diventato chiaro quando ho iniziato a scrivere i test prima) che non puoi incolpare nessuno per quello . La cosa migliore da fare è cercare di educare gli altri fino a quando non scoprono la Verità (scherzo). :)
Scoperto l'
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.