Qual è il modo più pratico per aggiungere funzionalità a questo pezzo di codice?

3

Sto scrivendo una libreria open source che gestisce le griglie esagonali. Riguarda principalmente la HexagonalGrid e la Hexagon class. Esiste una classe HexagonalGridBuilder che crea la griglia che contiene oggetti Hexagon . Quello che sto cercando di ottenere è di consentire all'utente di aggiungere dati arbitrari a ogni Hexagon . L'interfaccia ha questo aspetto:

public interface Hexagon extends Serializable {

    // ... other methods not important in this context

    <T> void setSatelliteData(T data);

    <T> T getSatelliteData();
} 

Fin qui tutto bene. Sto scrivendo un'altra classe, tuttavia chiamata HexagonalGridCalculator , che aggiunge alla libreria alcuni pezzi fantasiosi di calcolo come calcolare il percorso più breve tra due Hexagon s o calcolare la linea di vista intorno a Hexagon . Il mio problema è che per quelli ho bisogno che l'utente fornisca alcuni dati per gli oggetti Hexagon come il costo di passare attraverso Hexagon , o un boolean indicatore che indica se l'oggetto è < strong> trasparente / passabile o non.

La mia domanda è: come dovrei implementarla?

La mia prima idea era di scrivere un'interfaccia come questa:

public interface HexagonData {

    void setTransparent(boolean isTransparent);

    void setPassable(boolean isPassable);

    void setPassageCost(int cost);
}

e far sì che l'utente lo realizzi, ma poi mi viene in mente che se aggiungo altre funzionalità in un secondo momento, tutto il codice si interromperà per coloro che stanno utilizzando la vecchia interfaccia.

Quindi la mia prossima idea è aggiungere annotazioni come

@PassageCost , @IsTransparent e @IsPassable

che può essere aggiunto ai campi e quando sto facendo il calcolo posso cercare le annotazioni nella satelliteData fornita dall'utente. Questo sembra abbastanza flessibile se prendo in considerazione la possibilità di modifiche successive ma utilizza la riflessione. Non ho parametri di riferimento per l'utilizzo delle annotazioni, quindi sono un po 'al buio qui.

Penso che nel 90-95% dei casi l'efficienza non sia importante poiché la maggior parte degli utenti non usa una griglia in cui ciò è significativo, ma posso immaginare che qualcuno stia cercando di creare una griglia con una dimensione di 5.000.000.000 X 5.000.000.000 .

Quindi su quale percorso dovrei iniziare a camminare? O ci sono alternative migliori?

Nota: queste idee non sono ancora implementate, quindi non ho prestato troppa attenzione ai nomi di qualità.

    
posta Adam Arold 12.11.2013 - 01:24
fonte

6 risposte

2

Per un problema mal definito come questo, preferisco scrivere prima un'applicazione reale, senza particolare riferimento a quale sarà l'interfaccia. Ovviamente, dovresti pensare a come sarà l'interfaccia man mano che procedi, ma c'è un singolo client, quindi sei libero di riorganizzarti se necessario.

Quando avvii la seconda applicazione che utilizzerà l'interfaccia, avvia i metodi di astrazione necessari per la seconda applicazione da quelli forniti dal primo. Iterate. La seconda applicazione forzerà inevitabilmente il primo a essere modificato per supportare un'interfaccia comune. Il terzo costringerà il secondo e il primo a cambiare. Ecc.

Quando arrivi al punto in cui aggiungere una nuova applicazione non richiede più la modifica delle interfacce, allora sei pronto per pubblicare la tua interfaccia.

    
risposta data 12.11.2013 - 01:41
fonte
2

My first idea was to write an interface [...] but then it came to my mind that if I add any other functionality later all code will break for those who are using the old interface.

Utilizza invece una classe astratta:

public interface Hexagon<T extends HexagonData>{
    T getData();
    void setData(T data);
}

public abstract class HexagonData{
    public boolean isTransparent(){ return false; }
    public boolean isPassable(){ return true; }
    public int getPassageCost(){ return 0; }
}

Non utilizzare annotazioni a meno che non si abbia effettivamente bisogno dei metadati relativi al membro che è stato annotato.

    
risposta data 12.11.2013 - 04:36
fonte
1

Se vuoi aggiungere dataelements / properties / variables al tuo Hexagon senza modificare gli altri HexagonHandlers come HexagonalGridCalculator puoi aggiungere proprietà dinamiche come questa:

public class Hexagon implements Serializable {

    // ... other methods not important in this context

    void setProperty(int id, object data);

    object getProperty(int id, object notFoundValue);
} 

e definire le costanti per gli elementi dati / proprietà / variabili

const int SatelliteData = 1;

Se in un secondo momento desideri disporre di dati aggiuntivi, definisci nuove costanti

const int Transparent = 2;
const int Passable = 3;
const int tPassageCost = 4;

che non influisce sugli altri gestori.

    
risposta data 12.11.2013 - 15:11
fonte
1

Propongo un approccio sfaccettato simile a quello usato nella libreria standard C ++.

interface Hexagon {
  void setData(int id, T data); //these should only be used from Facet class
  Object getData(int id);

  sealed class IFacet<T> {
    private static int count = 0;
    private int _id = count++;
    public T getData(Hexagon hexagon) {
       return (T) hexagon.getData(_id);
    }
    public void setData(Hexagon hexagon, T data) {
       hexagon.setData(_id, data);
    }
  }
}

//Usages:

class CostManager {
  public readonly Facet<int> PASSAGE_COST = new Facet<int>();

  static void incrementCost(Hexagon hex) {
    PASSAGE_COST.setData(hex, PASSAGE_COST.getData(hex)+1);
  }
}

class TransparencyManager {
  public readonly Facet<boolean> TRANSPARENT = new Facet<boolean>();
  static void disable(Hexagon hex) {
    TRANSPARENT.setData(hex, false);
  }

}

Ogni Facet si rivolge esattamente a una proprietà di Hexagon (dato che usano _id univoco). Se Hexagon.setData () e Hexagon.getData () vengono sempre utilizzati attraverso una classe Facet, i dati memorizzati avranno sempre il tipo di Facet corrispondente. Ciò impedirà all'utente di provare a scrivere dati di tipo sbagliato su un dato ID.

Per illustrare il tipo di errori che questo approccio protegge da considerare un esempio basato sulla risposta @ k3b:

  static void configure(Hexagon hex) {
     hex.setData(DataType.TRANSPARENCY, "very transparent"); // compiler accepts this just fine, strings are objects, aren't they?
  }

  static void processHexagon(Hexagon hex) {
    boolean data = (Boolean)hex.getData(DataType.TRANSPARENCY); // compiler accepts this, Boolean are objects too
  }

  static void doSomeJob(Hexagon hex) {
     configure(hex);
     // working
     // more work
     // ...
     processHexagon(hex); //oops, you've got runtime error
  }

Ora considera lo stesso codice con Facets:

  static void configure(Hexagon hex) {
     TRANSPARENT.setData(hex, "very transparent"); // compiler slaps you
  }

  static void processHexagon(Hexagon hex) {
    boolean data = TRANSPARENT.getData(hex); // no downcast!
  }

  static void doSomeJob(Hexagon hex) {
     configure(hex);
     // working
     // more work
     // ...
     processHexagon(hex); //no runtime errors, just compile-time ones
  }

Come puoi vedere, il codice è ancora meno dettagliato, poiché ora non è necessario eseguire downcast.

    
risposta data 12.11.2013 - 15:15
fonte
0

Lo costruirei come un DTO. Avere interfacce con getter e setter per i dati necessari e eseguire l'elaborazione in sistemi esterni. Forse fornire un esadecimale predefinito che potrebbero estendere se lo volessero. Il risultato sarebbe più un sistema di componenti di entità che di object oriented, ma è una specie di punto dei sistemi di componenti la flessibilità da estendere in modi che non si aspettavano in anticipo.

    
risposta data 12.11.2013 - 16:05
fonte
-1

Per quanto ne so, tu vuoi fornire un'API che permetta agli utenti della tua libreria di costruire oggetti complessi ( esagoni ), con una serie di attributi che potenzialmente possono essere estesi in futuro ( costo trasparente, passabile, di passaggio ecc.)

I problemi di progettazione del genere sono presi di mira da Modello di generatore :

the builder pattern uses another object, a builder, that receives each initialization parameter step by step and then returns the resulting constructed object at once... Builders are good candidates for a fluent interface...

Per il tuo caso, l'implementazione potrebbe essere la seguente (supponendo che newAttribute venga aggiunta nella libreria delle versioni successive):

public class HexagonBuilder {

    public Hexagon build() { /*...*/ }

    public HexagonBuilder transparent(boolean isTransparent) { /*...*/; return this }

    public HexagonBuilder passable(boolean isPassable) { /*...*/; return this }

    public HexagonBuilder passageCost(int cost) { /*...*/; return this }

    public HexagonBuilder newAttribute(Object attribute) { /*...*/; return this }
}

Come puoi vedere, il vecchio codice si compilerebbe bene con la versione più recente della tua libreria, perché la sola differenza è compatibile - semplicemente non invoca il metodo newAttribute .

Nota che per il vecchio codice non solo compila, ma funziona anche senza problemi con la nuova versione della libreria, dovrai fornire un comportamento predefinito appropriato per il codice client che non imposta un nuovo attributo.

Per motivi di completezza, prendi in considerazione altri schemi di creazione - < em> Factory, Prototype etc, anche se per i dettagli forniti nella domanda fino ad ora, Builder sembra la soluzione migliore.

    
risposta data 12.11.2013 - 17:27
fonte

Leggi altre domande sui tag