Va bene per le interfacce contenere un metodo che restituisce il tipo concreto dell'implementazione sotto forma di costante?

25

Spesso mi trovo in una situazione in cui ho bisogno di un comportamento diverso di un componente che dipende dal tipo concreto di una diversa interfaccia.

Per illustrare la mia domanda, ho scritto un piccolo pezzo di codice che mostrerà cosa intendo. In questo esempio sto cercando di istanziare un oggetto cibo da un oggetto animale.

interface Animal {
    void growl();
    String getAnimalType(); //"DOG" or "FISH"
}

abstract class Food {}
Meat extends Food {}
Bread extends Food {}

class FoodFactory {
    Food createFoodForAnimal(Animal animal) {
        switch (animal.getAnimalType()) {
            case "DOG":
                Food food = new Meat();
                break;
            case "FISH":
                Food food = new Bread();
                break;
            default:
                return;
        }
    }
}


Animal dog = new Dog();
Animal fish = new Fish();
FoodFactory factory = new FoodFactory();
Food food = factory.createFoodForAnimal(dog)
Food food = factory.createFoodForAnimal(fish)

Per me questo sembra un po 'sporco, perché sembra un cast sotto mentite spoglie.

    
posta plunz 18.05.2016 - 19:01
fonte

6 risposte

21

Dillo, non chiedere potrebbe aiutarti qui.

    switch (animal.getAnimalType()) {
        case "DOG":
            Food food = new Meat();
            break;
        case "FISH":
            Food food = new Bread();
            break;
        default:
            return;
    }

Si noti che cos'è questo frammento di codice: un comportamento di attivazione. È un "odore di codice" in una soluzione orientata agli oggetti. Di solito, indica che c'è un oggetto sottostante che non hai scoperto. Qui, potrebbe essere più preciso dire che non hai delegato la responsabilità agli oggetti che hai già scoperto.

interface Animal {
    void growl();
    void orderDinner(DinnerOrder order);
}

class Dog implements Animal {
    //...

    void orderDinner(DinnerOrder order) {
        order.add(new Meat());
    }
}

class Fish implements Animal {
    //...

    void orderDinner(DinnerOrder order) {
        order.add(new Bread());
    }
} 

In realtà, probabilmente non ha senso specificare l'istanza del cibo che l'animale vuole - probabilmente non si preoccupa di una specifica entità Pane, tanto quanto Bread come un tipo . Quindi forse questo viene posticipato all'oggetto DinnerOrder .

Quindi potresti implementare DinnerOrder con metodi che consentono ad un animale di specificare il tipo di cibo che desidera, e libera l'animale dal conoscere i dettagli (incapsulamento).

class Fish implements Animal {
    //...

    void orderDinner(DinnerOrder order) {
        order.addBread();
    }
}

Il cablaggio fisso in un comando specifico non è facilmente generalizzato. Quindi potrebbe essere che tu voglia usare gli argomenti per specificare il cibo dopotutto. Ma come notato sopra, probabilmente non ci interessa quale entità Pane stia cenando stasera; in particolare, potremmo voler regalare agli animali un regalo speciale a Natale; così gli animali specificano ciò che vogliono, e quindi il fornitore fa del suo meglio.

class Fish implements Animal {
    //...

    void orderDinner(DinnerOrder order) {
        order.add(BREAD);
    }
}

Naturalmente, se pensi che il cibo preferito sia un tratto di un certo numero di animali, allora potresti cercare di trattare il cibo, non come una costante statica, ma come una proprietà di un'istanza.

class Fish implements Animal {
    //...

    void orderDinner(DinnerOrder order) {
        order.add(this.preferredFood);
    }
}

Questo è un pattern OO molto comune; un metodo che passa una copia del suo stato a un argomento per un'ulteriore elaborazione. Ad esempio, questo è il modo in cui DomainServices di solito funziona in un modello di dominio - il servizio viene passato a un aggregato, che fornisce il proprio stato al servizio per lavoro.

Talvolta vengono utilizzate specifiche per renderlo ancora più generale. Invece di passare una proprietà da interpretare a DinnerOrder, puoi passare un predicato che identifica quali alimenti hanno quella proprietà e quindi lasciare che il destinatario indaga sulle alternative disponibili.

Specification<Food> FISH_FOOD = new Specification {
    boolean isSatifiedBy(Food food) {
        return food.isA(BREAD); 
    }
}

class Fish implements Animal {
    //...

    void orderDinner(DinnerOrder order) {
        order.order(FISH_FOOD);
    }
}

Usato in questo modo, la Specifica è un esempio del modello di strategia; che potrebbe essere più adatto se volessi dare agli Animali un modo per classificare / scegliere quale delle opzioni disponibili preferirebbero ...

    
risposta data 18.05.2016 - 19:45
fonte
29

Ci sono due possibili approcci qui.

Il modo più semplice per risolverlo se ogni Animal sa da sé cosa devono essere nutriti. Quindi:

interface Animal {
  Food wantedFood();
}

Questo potrebbe risolvere completamente il tuo problema, e se è così, è una buona soluzione.

Ma mentre questo è semplice, questo è spesso insoddisfacente: la tua interfaccia Animal crescerà e crescerà con metodi marginalmente correlati. O forse non vogliamo che ogni animale si nutra da solo e preferisce un ZooKeeper separato per gestirlo. Un ZooKeeper sa come handle() ogni Animal . In che modo il ZooKeeper sa quale animale sta trattando? C'è una tecnica per questo chiamato double dispatch , dove l'animale è responsabile per chiedere a ZooKeeper di trattamento corretto. Sembra molto simile all'esempio sopra, in cui l'animale stesso sa quale cibo vogliamo, ma una volta che abbiamo uno ZooKeeper generico possiamo avere diversi ZooKeeper con comportamenti diversi:

interface ZooKeeper {
  handleDog(Dog dog);
  handleFish(Fish fish);
}

interface Animal {
  accept(ZooKeeper keeper);
}

class Dog implements Animal {
  accept(ZooKeeper keeper) { keeper.handleDog(this); }
}

class Fish implements Animal {
  accept(ZooKeeper keeper) { keeper.handleFish(this); }
}

class FeedingZooKeeper implements ZooKeeper {
  handeDog(Dog dog) { /* the dog gets meat */ }
  handeFish(Fish fish) { /* the fish gets bread */ }
}

Animal someAnimal = ...;
FeedingZooKeeper keeper = new FeedingZooKeeper();
someAnimal.accept(keeper); // either calls handleDog() or handleFish()

Ora possiamo aggiungere altri ZooKeepers senza dover modificare gli Animali.

Questo è generalmente noto come modello di visitatore e ci consente di implementare una funzione diversa per tipi diversi, anche se non sappiamo quale tipo di tipo otterremo in fase di esecuzione. Tuttavia, c'è un grande commercio qui: se lo zoo ottiene un nuovo animale, dobbiamo aggiornare l'interfaccia ZooKeeper e tutte le implementazioni di ZooKeeper con esso. Questo è un problema, perché rende difficile estenderlo, ma anche perché rende più difficile dimenticare di gestire un caso.

    
risposta data 18.05.2016 - 19:52
fonte
3

Nota: se il tuo codice reale assomiglia all'esempio Animal / Food, per favore vai con i suggerimenti nelle altre risposte che riguardano la tecnica del doppio dispatch / visitor.

La risposta alla domanda più generale, "È mai appropriato?", è un sì qualificato. Una delle volte in cui questo tipo di metodi è appropriato è quando si implementano tipi di dati algebrici in una lingua che non fornisce la sintassi di corrispondenza del modello. Mentre i tipi di dati algebrici sono più di una tecnica di programmazione funzionale, essi saltano fuori di volta in volta in codice orientato agli oggetti.

L'idea di base è piuttosto semplice, i tipi di dati algebrici sono tipi compositi. La chiave che rende la restituzione di un enum per identificarli un'idea ragionevole è che esiste un numero fisso di implementazioni dell'interfaccia, che per la natura del tipo non verrà mai espansa.

Un buon esempio è se hai bisogno di un tipo che permetta ad un metodo di accettare o restituire uno di due tipi. Chiameremo questo tipo Or . Un'implementazione Java minima molto potrebbe essere simile a questa:

public enum OrSubType {
  LEFT,RIGHT
}

interface Or<L,R> {
  OrSubType type();
  L left();
  R right(); 
}

final class Left<L,R> extends Or<L,R> {
  final private L value;

  public Left(L value) {
    this.value = value;
  }
  public OrSubType type() { return LEFT; }
  public L left() { return value; }
  public R right() { 
    throw new IllegalStateException(
      "Attempted to get a Right from a Left(" + value + ")"
    );
  }
}

final class Right<L,R> extends Or<L,R> {
  final private R value;

  public Right(R value) {
    this.value = value;
  }
  public OrSubType type() { return RIGHT; }
  public R right() { return value; }
  public L left() { 
    throw new IllegalStateException(
      "Attempted to get a Left from a Right(" + value + ")"
    );
  }
}

Questo è un bel po 'di codice in più, ma ti permetterà di fare cose del genere:

Or<Value,ErrorCode> result = doOperationThatCanFail();
switch(result.type()) {
  case LEFT: 
    doSomethingWithResult(result.left());
    break;
  case RIGHT:
    displayErrorMessageForCode(result.right());
    break;
}

Se la lingua in questione ha una corrispondenza di pattern come sintassi di prima classe, spesso viene fornita una guida per semplificare l'utilizzo. In Scala la classe di esempio sopra sarebbe semplicemente questa:

sealed trait Or[L,R] {
  def left: L
  def right: R
}

object Or {
  case class Left[L](var left: L) extends Or[L,Nothing] {
    def right: Nothing = throw new IllegalStateException(
      s"Attempted to get a Right from a Left($left)"
    )
  }

  case class Right[R](var right: R) extends Or[Nothing,R] {
    def left: Nothing = throw new IllegalStateException(
      s"Attempted to get a Left from a Right($left)"
    )
  }
}

Anche l'utilizzo del campione è semplificato:

val result: Or[Value,ErrorCode] = doOperationThatCanFail()
result match {
  case Or.Left(value) => doSomethingWithResult(value);
  case Or.Right(errorCode) => displayErrorMessageForCode(errorCode);
}
    
risposta data 19.05.2016 - 08:13
fonte
1

Questo mi sembra molto simile all'iniezione di dipendenza, l'unica differenza è che una dipendenza viene impostata solo una volta e un animale può essere alimentato più volte. Le mie preferenze (in ordine) sarebbero ...

  1. Lascia che gli animali creino il loro cibo (possibile solo se il cibo è facile da creare)
  2. Crea una fabbrica di alimenti specifica per tipo (ad es. MeatFactory per Dog ) e trasmetterla all'animale quando la crei, utilizzando l'iniezione di dipendenza, se necessario. Quindi può esserci solo un metodo eat() in cui l'animale afferra il cibo dalla fabbrica e si abbassa.
  3. In caso contrario, e ci sono alcune risorse / ingredienti comuni necessari per creare cibo, quindi trasferiscili nel metodo feed() .
  4. Se non c'è una vera comunanza tra i cibi, quindi crea un secchio di alimenti (un frigorifero, o un contenitore DoI se preferisci), e dagli agli animali per prendere il cibo che vogliono mangiare.

Pensandoci un po 'di più suppongo che dipenderà dallo scopo primario dell'animale. Se lo scopo principale non è realmente legato al cibo (ad esempio, intrattenere persone, allevamento, ecc.) E il cibo è solo qualcosa di cui ha bisogno per sopravvivere / fare il suo lavoro, allora le opzioni di cui sopra sono ciò che vorrei andare.

Tuttavia, se lo scopo principale dell'animale è distruggere il cibo (o elaborarlo, ad esempio creando fertilizzanti), allora quanto sopra può essere una buona idea, ma potrebbero esserci altri modelli.

Potresti avere un router di generi, indirizzando gli alimenti agli animali corretti. La cosa che salta alla mente è il meccanismo di routing dell'URL utilizzato da tecnologie come JAX-RS. Hanno un registro di servizi che si associano a diversi URL. Una cosa del genere potrebbe essere implementata da un Set di Animal in cui ogni animale ha un metodo canEat() e devi solo attraversare ogni animale nel tuo serraglio finché non trovi quello che può mangiare il cibo che devi mangiare. O potresti farlo con un HashMap . Questi saranno abbastanza simili al tuo progetto originale ma leggermente più estensibili (dato che non ci sono istruzioni switch da aggiornare).

    
risposta data 19.05.2016 - 06:21
fonte
0

Se l'insieme di costanti è un po 'piccolo senza molte potenzialità per crescere esponenzialmente più grandi, lo considero accettabile. Consiglio vivamente di utilizzare un tipo di ritorno enum, piuttosto che una stringa, per consentire al compilatore di rilevare gli errori prima del runtime.

Il tuo approccio iniziale può sembrare meno che desiderabile in parte perché stai introducendo un accoppiamento non necessario tra le classi FoodFactory e Animal. Considera l'alternativa sottostante, in cui il parametro createFoodForAnimal è stato modificato da Animal a AnimalType. Questo approccio offre un paio di vantaggi:

1) FoodFactory può ora essere testato senza bisogno di farlo     istanziare un animale.

2) La chiarezza del codice è leggermente migliorata

public enum AnimalType {
  FISH, LIGER
}

class FoodFactory {
    Food createFoodForAnimal(AnimalType animalType) {
        Food food;
        switch (animalType) {
            case LIGER:
                food = new UnicornMeat();
                break;
            case FISH:
                food = new Bread();
                break;
            default:
                food = new Spam();
        }
        return food;
    }
}

Avendo alcune delle altre risposte, vorrei anche aggiungere che mantenere la logica decisionale separata dalla classe Animal o ZooKeeper può migliorare notevolmente la flessibilità del tuo sistema. Ad esempio, si consideri necessario apportare una modifica in cui tutti gli animali mangiano il pane in un determinato momento della giornata. È possibile apportare una modifica molto meno invadente a FoodFactory di quanto non sia possibile per tutte le classi Animal.

    
risposta data 19.05.2016 - 08:36
fonte
0

Puoi pensare in termini di comportamenti componibili usando DI. Hai già in mano tutti i pezzi esistenti, modificali un po ':

// Your FoodFactory interface
public Food createFood();

// Factories, each of them returns a specific kind of food
meat = new MeatFactory();
fish = new FishFactory();
poop = new PoopFactory();

// Inject the wanted factory to the animal to have the desired behaviour
cat = new Cat(fish);
dog = new Dog(poop);
tiger = new Tiger(meat);
lion = new Lion(meat);
  • È chiuso per la modifica, a differenza del tuo primo esempio.
  • Diversi animali possono utilizzare la stessa fabbrica di alimenti, senza codice aggiuntivo
  • "leghi" i tuoi oggetti come preferisci, costruendo il comportamento che desideri, in un modo relativamente flessibile.
  • L'animale chiede alla fabbrica il cibo senza sapere di cosa si tratta
  • La fabbrica di cibo non conosce gli animali.

Extra : puoi rendere le tue fabbriche alimentari più intelligenti se necessario:

cat = new Cat(
    new RotatingFood(fish, meat)
);

dog = new Dog(
    new RandomFood([meat, poop, fish, shoe])
);

// RandomFood & RotatingFood both implement FoodFactory and use food factories
    
risposta data 13.06.2016 - 14:52
fonte

Leggi altre domande sui tag