Quando utilizzare i generici nella progettazione dell'interfaccia


11

Ho alcune interfacce che intendo implementare da terzi in futuro e fornisco personalmente un'implementazione di base. Userò solo un paio per mostrare l'esempio.

Attualmente, sono definiti come

Articolo:

public interface Item {

    String getId();

    String getName();
}

ItemStack:

public interface ItemStackFactory {

    ItemStack createItemStack(Item item, int quantity);
}

ItemStackContainer:

public interface ItemStackContainer {

    default void add(ItemStack stack) {
        add(stack, 1);
    }

    void add(ItemStack stack, int quantity);
}

Ora, Iteme ItemStackFactoryposso assolutamente prevedere che alcuni terzi debbano estenderlo in futuro. ItemStackContainerpotrebbe anche essere esteso in futuro, ma non in modi che posso prevedere, al di fuori della mia implementazione predefinita fornita.

Ora sto cercando di rendere questa libreria il più robusta possibile; questo è ancora nelle prime fasi (pre-pre-alfa), quindi potrebbe essere un atto di over engineering (YAGNI). È questo un posto appropriato per usare i generici?

public interface ItemStack<T extends Item> {

    T getItem();

    int getQuantity();
}

E

public interface ItemStackFactory<T extends ItemStack<I extends Item>> {

    T createItemStack(I item, int quantity);
}

Temo che ciò potrebbe finire per rendere le implementazioni e l'uso più difficili da leggere e comprendere; Penso che sia la raccomandazione di evitare di nidificare i generici ove possibile.


1
Kinda ha l'impressione di esporre i dettagli dell'implementazione in entrambi i modi. Perché il chiamante deve lavorare con e conoscere / preoccuparsi delle pile piuttosto che delle quantità semplici?
cHao,

È qualcosa a cui non avevo pensato. Ciò fornisce alcune informazioni utili su questo problema specifico. Mi assicurerò di apportare le modifiche al mio codice.
Zimo

Risposte:


9

Usi generici nella tua interfaccia quando è probabile che anche la tua implementazione sia generica.

Ad esempio, qualsiasi struttura di dati che può accettare oggetti arbitrari è un buon candidato per un'interfaccia generica. Esempi: List<T>e Dictionary<K,V>.

Qualsiasi situazione in cui si desidera migliorare la sicurezza dei tipi in modo generalizzato è un buon candidato per i generici. A List<String>è un elenco che funziona solo su stringhe.

Qualsiasi situazione in cui si desidera applicare SRP in modo generalizzato ed evitare metodi specifici del tipo è un buon candidato per i generici. Ad esempio, un PersonDocument, PlaceDocument e ThingDocument possono essere sostituiti con a Document<T>.

Il modello Abstract Factory è un buon caso d'uso per un generico, mentre un normale Metodo Factory creerebbe semplicemente oggetti che ereditano da un'interfaccia comune e concreta.


Non sono davvero d'accordo con l'idea che le interfacce generiche debbano essere abbinate a implementazioni generiche. Dichiarare un parametro di tipo generico in un'interfaccia e quindi perfezionarlo o crearne un'istanza in varie implementazioni è una tecnica potente. È particolarmente comune in Scala. Interfacce cose astratte; le classi li specializzano.
Benjamin Hodgson,

@BenjaminHodgson: significa solo che stai producendo implementazioni su un'interfaccia generica per tipi specifici. L'approccio generale è ancora generico, anche se in qualche modo meno.
Robert Harvey,

Sono d'accordo. Forse ho frainteso la tua risposta: sembravi sconsigliare questo tipo di design.
Benjamin Hodgson,

@BenjaminHodgson: un metodo Factory non verrebbe scritto su un'interfaccia concreta , piuttosto che su una generica? (sebbene una Fabbrica astratta sarebbe generica)
Robert Harvey,

Penso che siamo d'accordo :) Probabilmente scriverei l'esempio del PO come qualcosa di simile class StackFactory { Stack<T> Create<T>(T item, int count); }. Posso scrivere un esempio di ciò che intendevo "perfezionando un parametro di tipo in una sottoclasse" in una risposta se si desidera un maggiore chiarimento, sebbene la risposta sarebbe solo tangenzialmente correlata alla domanda originale.
Benjamin Hodgson,

8

Il tuo piano su come introdurre la generalità nella tua interfaccia sembra essere corretto per me. Tuttavia, la tua domanda se sarebbe una buona idea o meno richiede una risposta un po 'più complicata.

Nel corso degli anni ho intervistato un certo numero di candidati per posizioni di ingegneria del software per conto di aziende per cui ho lavorato, e ho capito che la maggior parte delle persone in cerca di lavoro là fuori sono a proprio agio con l' uso di classi generiche, (ad esempio, il classi di raccolta standard), ma non con la scrittura di classi generiche. La maggior parte di loro non ha mai scritto una sola lezione generica in carriera e sembra che si perderebbe completamente se gli si chiedesse di scriverne una. Quindi, se imponi l'uso dei generici a chiunque desideri implementare la tua interfaccia o estendere le tue implementazioni di base, potresti scoraggiare le persone.

Ciò che puoi provare, tuttavia, è fornire versioni sia generiche che non generiche delle tue interfacce e delle tue implementazioni di base, in modo che le persone che non fanno generici possano vivere felicemente nel loro mondo ristretto e pieno di caratteri, mentre le persone che i generici possono trarre beneficio dalla loro più ampia comprensione della lingua.


3

Ora, Iteme ItemStackFactoryposso assolutamente prevedere che alcuni terzi debbano estenderlo in futuro.

C'è la tua decisione presa per te. Se non fornisci generici, dovranno eseguire l'upgrade alla loro implementazione Itemogni volta che usano:

class MyItem implements Item {
}
MyItem item = new MyItem();
ItemStack is = createItemStack(item, 10);
// They would have to cast here.
MyItem theItem = (MyItem)is.getItem();

Dovresti almeno renderlo ItemStackgenerico nel modo che suggerisci. Il vantaggio più profondo dei generici è che non hai quasi mai bisogno di lanciare nulla e offrire codice che costringe gli utenti a lanciare è IMHO un reato.


2

Wow ... ho una visione completamente diversa di questo.

Posso assolutamente prevedere alcune necessità di terzi

Questo è un errore. Non dovresti scrivere il codice "per il futuro".

Inoltre, le interfacce sono scritte dall'alto verso il basso. Non guardi una classe e dici "Ehi, questa classe potrebbe implementare totalmente un'interfaccia e quindi posso usare quell'interfaccia anche da qualche altra parte". Questo è un approccio sbagliato, perché solo la classe che utilizza un'interfaccia sa veramente quale dovrebbe essere l'interfaccia. Invece dovresti pensare "In questo punto la mia classe ha bisogno di una dipendenza. Fammi definire un'interfaccia per quella dipendenza. Quando avrò finito di scrivere questa classe, scriverò un'implementazione di quella dipendenza." Se qualcuno potrà mai riutilizzare la tua interfaccia e sostituire l'implementazione predefinita con la propria è del tutto irrilevante. Potrebbero non farlo mai. Ciò non significa che l'interfaccia fosse inutile. Fa sì che il tuo codice segua il principio di inversione del controllo, il che rende il tuo codice molto estensibile in molti punti "


0

Penso che usare i generici nella tua situazione sia troppo complesso.

Se si sceglie la versione generica delle interfacce, il design indica che

  1. ItemStackContainer <A> contiene un singolo tipo di stack, A. (ovvero ItemStackContainer non può contenere più tipi di stack.)
  2. ItemStack è dedicato a un tipo specifico di articolo. Quindi non esiste alcun tipo di astrazione di tutti i tipi di pile.
  3. È necessario definire una ItemStackFactory specifica per ciascun tipo di articoli. È considerato troppo fine come modello di fabbrica.
  4. Scrivi codice più difficile come ItemStack <? estende l'articolo>, diminuendo la manutenibilità.

Queste ipotesi e restrizioni implicite sono cose molto importanti da decidere con attenzione. Quindi, specialmente nella fase iniziale, questi progetti prematuri non dovrebbero essere introdotti. Si desidera un design semplice, di facile comprensione e comune.


0

Direi che dipende principalmente da questa domanda: se qualcuno ha bisogno di estendere la funzionalità di Iteme ItemStackFactory,

  • sovrascriveranno semplicemente i metodi e quindi le istanze delle loro sottoclassi verranno usate proprio come le classi di base, si comportano in modo diverso?
  • o aggiungeranno membri e useranno le sottoclassi in modo diverso?

Nel primo caso, non c'è bisogno di farmaci generici, nel secondo probabilmente c'è.


0

Forse, puoi avere una gerarchia di interfacce, dove Foo è un'interfaccia, Bar è un'altra. Ogni volta che un tipo deve essere entrambi, è un FooAndBar.

Per evitare un desgin eccessivo, crea il tipo FooAndBar solo quando è necessario e mantieni un elenco di interfacce che non estenderanno mai nulla.

Questo è molto più comodo per la maggior parte dei programmatori (la maggior parte come il loro controllo del tipo o piace non affrontare mai la tipizzazione statica di alcun tipo). Pochissime persone hanno scritto la loro lezione di generici.

Anche come nota: le interfacce riguardano quali possibili azioni possono essere eseguite. In realtà dovrebbero essere verbi e non sostantivi.


Questa potrebbe essere un'alternativa all'utilizzo dei farmaci generici, ma la domanda originale era quando usare i farmaci generici rispetto a ciò che hai suggerito. Puoi migliorare la tua risposta per suggerire che i generici non sono mai necessari, ma l'uso di più interfacce è la risposta a tutti?
Jay Elston,
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.