Without knowing too much about the specific of your code, I'll offer three general approaches.
(Also, I have never used Kotlin. I hope Java samples are enough for you to figure things out.)
First Approach
It sounds like you need some non-trivial logic to determine which Database implementation is the right one to use. This is a classic case for a ProviderBinding. Instead binding Database
to a specific implementation, you bind Database
to a class that is responsible providing instances (a Provider). For example, you might have this class:
public class MyDatabaseProvider.class implements Provider<Database> {
@Inject
public MyDatabaseProvider.class(Provider<SQLiteDatabase> sqliteProvider, Provider<H2Database> h2Provider) {
this.sqliteProvider = sqliteProvider;
this.h2Provider = h2Provider;
}
public Database get() {
// Logic to determine database type goes here
if (isUsingSqlite) {
return sqliteProvider.get();
} else if (isUsingH2) {
return h2Provider.get();
} else {
throw new ProvisionException("Could not determine correct database implementation.");
}
}
}
(Side note: This sample code gets you a new instance every time. It is fairly straightforward to make this also return a singleton instance.)
Then, to use it, you have two options. In your module, you would bind Database
not to a specific implementation, but to your DatabaseProvider
. Like this:
protected void configure() {
bind(Database.class).toProvider(MyDatabaseProvider.class);
}
The advantage of this approach is that you don't need to know the correct database implementation until Guice tries to construct an object that requires Database
as one of its constructor args.
Second Approach
You could create a DatabaseRoutingProxy
class which implements Database
and then delegates to the correct database implementation. (I've used this pattern professionally. I don't think there's an "official" name for this design pattern, but you can find a discussion here.) This approach is based on lazy loading with Provider
using the Providers that Guice automatically creates(1) for every bound type.
public class DatabaseRoutingProxy implements Database {
private Provider<SqliteDatabse> sqliteDatabaseProvider;
private Provider<H2Database> h2DatabaseProvider;
@Inject
public DatabaseRoutingProxy(Provider<SqliteDatabse> sqliteDatabaseProvider, Provider<H2Database> h2DatabaseProvider) {
this.sqliteDatabaseProvider = sqliteDatabaseProvider;
this.h2DatabaseProvider = h2DatabaseProvider;
}
// Not an overriden method
private Database getDatabase() {
boolean isSqlite = // ... decision logic, or maintain a decision state somewhere
// If these providers don't return singletons, then you should probably write some code
// to call the provider once and save the result for future use.
if (isSqlite) {
return sqliteDatabaseProvider.get();
} else {
return h2DatabaseProvider.get();
}
}
@Override
public QueryResult queryDatabase(QueryInput queryInput) {
return getDatabase().queryDatabase(queryInput);
}
// Implement rest of methods here, delegating as above
}
And in your Guice module:
protected void configure() {
bind(Database.class).to(DatabaseRoutingProxy.class);
// Bind these just so that Guice knows about them. (This might not actually be necessary.)
bind(SqliteDatabase.class);
bind(H2Database.class);
}
The advantage of this approach is that you don't need to be able to know which database implementation to use until you actually make a database call.
Both of these approaches have been assuming that you cannot instantiate an instance of H2Database or SqliteDatabase unless the backing database file actually exists. If it's possible to instantiate the object without the backing database file, then your code becomes much simpler. (Just have a router/proxy/delegator/whatever that takes the actual Database
instances as the constructor args.)
Third Approach
This approach is completely different then the last two. It seems to me like your code is actually dealing with two questions:
- Does a database actually exist? (If not, then make one.)
- Which database exists? (And get the correct class to interact with it.)
If you can solve question 1 before even creating the guice injector that needs to know the answer to question 2, then you don't need to do anything complicated. You can just have a database module like this:
public class MyDatabaseModule extends AbstractModule {
public enum DatabaseType {
SQLITE,
H2
}
private DatabaseType databaseType;
public MyDatabaseModule(DatabaseType databaseType) {
this.databaseType = databaseType;
}
protected void configure() {
if (SQLITE.equals(databaseType)) {
bind(Database.class).to(SqliteDatabase.class);
} else if (H2.equals(databaseType)) {
bind(Database.class).to(H2Database.class);
}
}
}
Since you've separated out questions 1 & 2, when you create the injector that will use the MyDatabaseModule
, you can pass in the appropriate value for the constructor argument.
Notes
- The Injector documentation states that there will exist a
Provider<T>
for every binding T
. I have successfully created bindings without creating the corresponding provider, therefore Guice must be automatically creating a Provider for configured bindings. (Edit: I found more documentation that states this more clearly.)