Swappable modules with Dagger 2

2019-08-13 11:26发布

So I have some code that runs an algorithm, say with an AlgoRunner class. Now this AlgoRunner class can be implemented in multiple ways to run different algorithms using Algo classes. I want to use Dagger 2 to provide different implementations of the AlgoRunner class to a "Manager" class that passes input to AlgoRunner as well as other components that it manages.

Question

I have the following right now, but I'm not sure if this is the correct way, mainly because of that empty AlgoRunnerProvider module. Is that any other way to achieve what I'm trying to do? Or to simplify what I have?

Should I just create different components, OneAlgoRunnerComponent and TwoAlgoRunnerComponent and inject the Manager from each of those?

The class that constructs the Manager instance uses this component to inject the AlgoRunner into that instance so that the Manager can pass it the inputs.

@Component(
        modules = {
                AlgoRunnerProvider.class
        }
)
public interface AlgoRunnerComponent {
    void inject(Manager manager);
    AlgoRunner getAlgoRunner();
}

AlgoRunnerProvider Module

@Module
public class AlgoRunnerProvider {

    @Provides
    public AlgoRunner getAlgoRunner() {
        return null;
    }
}

OneAlgoRunnerProvider, that overrides the provides method in AlgoRunnerProvider. Could have a TwoAlgoRunnerProvider as well, that does the same thing and provides TwoAlgoRunner, as long as that extends AlgoRunner.

public class OneAlgoRunnerProvider extends AlgoRunnerProvider {
    private final OneAlgo algo;

    public OneAlgoRunnerProvider(OneAlgo algo) {
        this.algo = algo;
    }

    @Override
    public OneAlgoRunner getAlgoRunner() {
        return new OneAlgoRunner(algo);
    }
}

All this is used like this right now:

AlgoRunnerComponent build = DaggerAlgoRunnerComponent.builder()
                .algoRunnerProvider(new OneAlgoRunnerProvider(new OneAlgo()))
//              .algoRunnerProvider(new TwoAlgoRunnerProvider(new TwoAlgo()))
                .build();

        Manager manager = managerComponent.getManager();
        build.inject(manager);

        Truth.assertThat(manager.algoRunner).isInstanceOf(OneAlgoRunner.class);
//      Truth.assertThat(manager.algoRunner).isInstanceOf(OneAlgoRunner.class);

Thanks a lot!

1条回答
男人必须洒脱
2楼-- · 2019-08-13 12:25

The dagger framework is used to handle object creation for you. If you start doing some sort of initialization in one of your classes you wish to provide, there is probably something not as it is supposed to be (see getAlgoRunner()).

If you have different types that you want to provide at runtime, you want a factory of some sorts to create the correct object. Enter dagger.

You have multiple ways of achieving what you want. Basically, the module should handle the object creation:

@Module
public class AlgoRunnerProvider {

    @Provides
    public AlgoRunner getAlgoRunner() {
        // todo create the correct type
        return null;
    }
}

1. @Named annotation (or some other Qualifier)

If you know at compile time which class is going to need which type, you should use qualifiers.

@Named("Version1")
@Inject
AlgoRunner mRunner;

You then just can provide different implementations from your module:

@Provides
@Named("Version1")
public AlgoRunner getAlgoRunner() {
    return new Version1AlgoRunner();
}

@Provides
@Named("OtherVersion")
public AlgoRunner getAlgoRunner(Depends someOtherDependency) {
    return new OtherVersionAlgoRunner(someOtherDependency);
}

2. Switching at runtime

While you could always use the first option by creating multiple classes with different dependencies, you might want to be able to chose at runtime. For this, you need to pass in some argument to your module:

@Module
public class AlgoRunnerProvider {

    private final int mType;

    public AlgoRunnerProvider(int type) {
        mType = type;
    }

    @Provides
    public AlgoRunner getAlgoRunner() {
        if(mType == TYPE_A) {
            return new Version1AlgoRunner();
        } else {
            return new OtherVersionAlgoRunner();
        }
    }
}

With this variant you still have your creation logic inside your module, where your dependencies come from.


3. Use different modules

Another approach would be to use different modules. This will only be a clean solution if determined at compile time (different classes using different modules) and not some choosing logic at runtime in the same class.

If you start writing code like if typeA then moduleA else moduleB you should probably stop and do something else.

You can use the same component but create it using different modules for different classes by using good old inheritance. Every module just provides its implementation of AlgoRunner.

// one class using one module to get a specific behavior
public class MyClassVersionA {

    public void onCreate(Bundle saved) {
        component.myModule(new MyModuleVersionA()).build();
    }
}

// another class using a different module to get a different behavior
public class MyClassSomethingElse {

    public void onCreate(Bundle saved) {
        component.myModule(new MyModuleSomethingElse()).build();
    }
}

You would then just subclass your module accordingly like so

// did not test the following, something like this...
public abstract class MyModule {

    @Provides
    public AlgoRunner getAlgoRunner();

}

public class MyModuleVersionA extends MyModule {

    @Provides
    @Override
    public AlgoRunner getAlgoRunner() {
        return new Version1AlgoRunner();
    }
}
public class MyModuleSomethingElse extends MyModule {

    @Provides
    @Override
    public AlgoRunner getAlgoRunner() {
        return new SomeOtherAlgoRunner();
    }
}

There are probably even more possibilities, especially if starting to mix those approaches, but I think those 3 to be the basic tools that you can use.

查看更多
登录 后发表回答