Guice: Zwei Probleme

Hallo,

ich bin gerade ein bisschen mit Guice am rumspielen und auf zwei Probleme gestoßen, die ich nicht so recht geknackt bekomme. Ich brechs mal auf den Kern der Problematik runter: Ich habe ein Spielobjekt, das zwei Spieler übergeben bekommt. Die Namen der Spieler werden zuvor gesetzt (beispielsweise in die GUI eingetragen)


    private Player p1;
    private Player p2;

    public Game(Player p1, Player p2) {
        this.p1 = p1;
        this.p2 = p2;
    }
}
    private String name;

    public Player(String name) {
        this.name = name;
    }
}

Wie bekomme ich hin, dass mir zwei unterschiedliche Playerobjekte gegeben werden? Providerklassen würden abhilfe schaffen, wenn die Spieler nicht noch einen zuvor gesetzten Namen brächten.

Zweiter Fall: Das Spiel hat eine Liste von Spielphasen. Es gibt auch eine Phase-Klasse. Phasen unterscheiden sich durch ein Enum, das den Typ speichert sowie eine Liste von Events, die zu Rundenbeginn gefeuert werden.

    private PhaseType type;
    private List<PhaseEvent> listEvents

    public Phase(PhaseType type, List<PhaseEvent> listEvents) {
        this.type = type;
        this.listEvents = listEvents;
    }
}

Die Typen werden nach festen Regeln vergeben, sind also von vorn herein bekannt (genau wie die Events).

Hoffe, hier kennt sich jemand bisschen mit Guice aus und kann mir helfen.

Bei den Spielern könnte die Named-Annotation helfen. Der Name muss natürlich im Guice-Modul auch entsprechend gesetzt werden: https://github.com/google/guice/wiki/BindingAnnotations

Ungefähr so:

bind(Player.class).annotatedWith(Names.named("player1")).toInstance(new Player("Alice"));
bind(Player.class).annotatedWith(Names.named("player2")).toInstance(new Player("Bob"));

Natürlich können die Namen auch später gesetzt werden, Hauptsache es ist durch die Annotation immer klar, welcher Spieler welcher ist.

Beim zweiten bin ich mir nicht ganz sicher, was du meinst. Eventuell helfen Multibindings ( https://github.com/google/guice/wiki/Multibindings ) - die allerdings nur ein Set liefern. Du müsstest dir dann noch überlegen, wie du das dann geordnet bekommst.

bind(Player.class).annotatedWith(Names.named("player1")).toInstance(new Player("Alice"));
äh, was für ein Framework ist das welches solchen Code evtl. erfordert, auf jeden Fall anscheinend zuläßt?
für einen simplen Anfang mit zwei Player-Objekten?

hoffentlich sind die sonstigen Vorteile gigantisch,
aber eigentlich rechtfertigt nichts so eine Krücke im Programm…

normale Programmierung mit Player p1 = new Player("Alice"); nicht aus den Augen verlieren!

Guice


Aber ich würde meine Player-Klasse überdenken. Den Namen würde ich über einen Setter setzen und nicht unbedingt über den Konstruktor. Dann würde ich auch versuchen auf das „toInstance“ zu verzichten, sonste nimmst du dir die Möglichkeit, dir Abhängigkeiten injecten zu lassen. Wenn du das berücksichtigst, dann könnte deine „Guice-Config“ dafür so aussehen:

bind(Player.class).annotatedWith(Names.named("player2")).asEagerSingleton();

Moin,

ich verstehe seine Frage so das er eigentlich AssistedInject sucht.

Gruß
Fancy

Factories finde ich eigentlich besser :wink:

[QUOTE=SlaterB]bind(Player.class).annotatedWith(Names.named("player1")).toInstance(new Player("Alice"));
äh, was für ein Framework ist das welches solchen Code evtl. erfordert, auf jeden Fall anscheinend zuläßt?
für einen simplen Anfang mit zwei Player-Objekten?

hoffentlich sind die sonstigen Vorteile gigantisch,
aber eigentlich rechtfertigt nichts so eine Krücke im Programm…

normale Programmierung mit Player p1 = new Player("Alice"); nicht aus den Augen verlieren![/QUOTE]

Das Beispiel war auf den Kern der Problematik reduziert. Zwei Objekte desselben Typs mit unterschiedlichen Attributen. Klar geht auch ein einfaches new Player(…). Beschäftigte mich aber gerade mit Dependency Injection und in dem Zusammenhang mit Guice.

@Fancy AssistedInjects sehen schon mal ganz gut aus. Beantwortet mir zumindest mal die Frage, wie man Konstruktoren behandelt, die sowohl vom Injector bedient werden müssen, als auch selbst Attribute deklarieren.

@Landei An die NamedAnnotations hab ich auch gedacht, das Binding von TS erscheint mir dann aber sinnvoller. Ein Ziel von Guice ist es doch, von der “händischen” Objekterzeugung abzusehen und das komplett des Modules zu überlassen oder? Allgemein verwirrt mich noch die Handhabe von Klassen, die zur Objekterzeugung z.B. Benutzereingaben übergeben bekommen. Oder ist es unüblich diese im Konstruktor zu setzen und bevorzugt durch Setter - wie von TS vorgeschlagen - zu setzen?

Kommt drauf an, ob Player1 und Player2 wie Singletons behandelt werden sollen. Für denn Fall dass ja, dann bringt einem hier AssistedInjection nichts.

Bei Konstruktorinjection muss man aufpassen. Man neigt imho gerade am Anfang dazu sich allen möglichen Scheiß darüber geben zu lassen. Ich würde dem Konstruktor aber wirklich nur das geben, was er zum erstellen des Objektes tatsächlich benötigt. Ist dies Abhängig von einer Benutzereingabe, dann wäre (wie Fancy schon gesagt hat) AssistedInjection die Lösung für dein Problem.

Zwei Instanzen eines Singletons ist etwas wo ich wild mit den Armen wedelnd im Kreis renne. Vielleicht gibt es Anwendungen wo das wirklich sinnvoll ist, mir widerstrebt das aber irgendwie.

Auch würde ich so viele Attribute unveränderlich machen wie es sinnvoll ist. Wenn der Name des Players später nicht mehr geändert werden soll, dann sollte imho die Player Klasse auch keinen Setter dazu anbieten.

Konkret würde ich das obige etwa so schreiben:

foobar.player.Player:



public interface Player {

}```

foobar.player.PlayerFactory:
```package foobar.player;


public interface PlayerFactory {

    Player create(String name);

}```

foobar.player.PlayerImpl:
```package foobar.player;


import javax.inject.Inject;

import com.google.inject.assistedinject.Assisted;


final class PlayerImpl implements Player {

    private final String name;


    @Inject
    private PlayerImpl(@Assisted final String name) {

        this.name = name;

    }


    @Override
    public String toString() {

        return "PlayerImpl [name=" + name + "]";

    }


}```

foobar.player.PlayerModule:
```package foobar.player;


import com.google.inject.AbstractModule;
import com.google.inject.assistedinject.FactoryModuleBuilder;


public final class PlayerModule extends AbstractModule {

    @Override
    protected void configure() {

        install(new FactoryModuleBuilder().implement(Player.class, PlayerImpl.class).build(PlayerFactory.class));

    }

}```

foobar.main.Main:
```package foobar.main;


import java.util.Scanner;
import java.util.logging.Logger;

import javax.inject.Inject;

import com.google.inject.Guice;

import foobar.player.Player;
import foobar.player.PlayerFactory;
import foobar.player.PlayerModule;


final class Main {

    private final Player player1;
    private final Player player2;


    @Inject
    private Main(final Logger logger, final PlayerFactory playerFactory) {

        final Scanner scanner = new Scanner(System.in);

        System.out.println("Name of player one: ");
        final String name1 = scanner.nextLine();
        this.player1 = playerFactory.create(name1);

        System.out.println("Name of player two: ");
        final String name2 = scanner.nextLine();
        this.player2 = playerFactory.create(name2);

        scanner.close();

        logger.info("player1 = " + player1);
        logger.info("player2 = " + player2);

    }


    public static void main(final String[] args) {

        Guice.createInjector(new PlayerModule()).getInstance(Main.class);

    }

}```

Gruß
Fancy

@Fancy : Danke für das ausführliche Beispiel. Aber noch eine Frage zu dem Player Interface: In diesem (simplen) Beispiel erfüllt es keinen Zweck und auch in richtigen Anwendungen mag es nicht immer ein Interface für eine Klasse geben, die als Parameter übergeben wird. Gibt es also einen besonderen Grund warum du es hier doch aufgeführt hast?

Ich habe mir dein Beispiel zum Anlass genommen, noch etwas vertrauter mit AssistedInjections zu werden und es ein bisschen ausgebaut:
[SPOILER]```
import java.util.Scanner;
import java.util.logging.Logger;

import com.google.inject.Guice;
import com.google.inject.Inject;

public class Main {

public static void main(String[] args) {
	Guice.createInjector(new GameModule()).getInstance(Main.class);
}

@Inject
public Main(final Logger logger, final GameFactory gameFactory) {
	final Scanner scanner = new Scanner(System.in);

	System.out.println("Name of player one: ");
	final String playerName1 = scanner.nextLine();

	System.out.println("Name of player two: ");
	final String playerName2 = scanner.nextLine();

	scanner.close();

	Game game = gameFactory.create(playerName1, playerName2);
	game.start();

	logger.info("Game is now running");
}

}```

import com.google.inject.assistedinject.Assisted;

public interface GameFactory {

	public Game create(@Assisted("playerName1") final String playerName1,
			@Assisted("playerName2") final String playerName2);

}```

import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;

public class Game {

private final Player p1;
private final Player p2;

@Inject
public Game(final PlayerFactory playerFactory,
		@Assisted("playerName1") final String playerName1,
		@Assisted("playerName2") final String playerName2) {
	this.p1 = playerFactory.create(playerName1);
	this.p2 = playerFactory.create(playerName2);
}

public void start() {
	System.out.println("Game started!");
	System.out.println("Player 1 = " + p1.toString());
	System.out.println("Player 1 = " + p2.toString());
}

}```

import com.google.inject.AbstractModule;
import com.google.inject.assistedinject.FactoryModuleBuilder;

public class GameModule extends AbstractModule {

	@Override
	protected void configure() {
		install(new FactoryModuleBuilder().build(GameFactory.class));
		install(new FactoryModuleBuilder().build(PlayerFactory.class));
	}

}```

public interface PlayerFactory {

public Player create(String name);

}```

import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;

public class Player {

	private final String name;

	@Inject
	public Player(@Assisted final String name) {
		this.name = name;
	}

	@Override
	public String toString() {
		return name;
	}
}```[/SPOILER]

Sobald eine Klasse etwas sinnvolles macht, hat sie Methoden die aufgerufen werden können. Diese gehören dann in das Interface.

Normalerweise lege ich in jedes Package mindestens ein öffentliches Interface, mindestens eine package-private Klasse mit privatem Konstruktor die das Interface implementiert und genau ein Guice-Modul (und ggf. die Factories). Dann ist das Package in sich abgeschlossen und bietet seine Funktionalität nur über sauber definierte Schnittstellen nach außen an. Implementierungsdetails können das Package so nicht verlassen.

Gruß
Fancy

Okay, danke für die Erklärung. Ich konnte übrigens auch das zweite Problem, das ich oben etwas schlecht geschildert habe, mit Hilfe der AssistedInjections lösen. Insofern ist das Thema für mich erstmal erledigt