How to provide a plug-in model where different plu

2019-02-10 15:19发布

The code I'm writing (MyService) includes the ability for each customer to plug-in their own calculator at a particular point in the processing. This is to allow customization of business rules. At the point the calculation is done we know a variety of things, some of which may be relevant to the calculation. MyService will be created for a particular set of input parameters and run once.

My plan is to use dependency injection to give MyService a calculator in the constructor. This allows different users to plug-in their own calculator. The calculator will return an amount representing the surcharge payable for this particular run of MyService. Others will be implementing a variety of calculators and I'll need to be able to update my code without breaking theirs. ie. keep backwards compatability.

The problem I have is that the various calculator implementations will require different parameters. These parameters can not be constructor injected into the calculator at the time MyService is created because they are not known some processing in MyService occurs.

The calculator will only be called once within a particular instantiation of MyService. Therefore at one extreme all parameters could be passed in the constructor and there is a method with no parameters that returns the answer. At the other, all parameters are passed in the method invocation.

AlwaysZeroCalculator might just return 0 so no parameters are required. PercentageCalculator needs amount in order to apply a percentage. A yet more complicated one needs the amount and customerNumber. We can assume that whatever the calculator might need is known to MyService at runtime (or itself can be injected into the calculator implementation, like a hibernate Session).

How can I implement this?

Here are some options and problems:

  • Make all calculators implement an interface that includes all parameters as method arguments. But, if something extra is added then they all need to change which will inevitably turn this into the second option.
  • Make different interfaces (ICalculator, ICalculatorWithAmount, ICalculatorWithAmountAndCustomerNumber, etc). MyService will need to see which interface the calculator it has implements, cast it to that interface, and then call the appropriate calculate(..) method.
  • Introduce a parameter object that includes anything any of them care about. This means even the simplest calculator depends on everything.
  • Make different interfaces and different versions of MyService that expect one of those interfaces.
  • Inject a calculatorFactory instead of a calculator. The factory would take all possible parameters and create a calculator with only the right ones. This seem to just move the problem to somewhere else without solving it.
  • Pass some horrible hashmap to the calculator and type safety, declaration of dependencies be damned

Is there a better way?

4条回答
Explosion°爆炸
2楼-- · 2019-02-10 15:39

Assuming you're using spring ... Perhaps the factory-bean and factory-method attributes of the bean tag may be useful.

Pseudo code ...

<bean id="calculator1"
 factory-bean="calculatorFactory"
     factory-method="getAlwaysZeroCalculator">
         <!--AlwaysZeroCalculator args go here --> 
</bean>
<bean id="calculator2"
 factory-bean="calculatorFactory"
     factory-method="getPercentageCalculator ">
         <!--PercentageCalculator args go here --> 
</bean>
查看更多
做个烂人
3楼-- · 2019-02-10 15:48

These are two problems:

  • using a calculator
  • instantiating a calculator

The first one is easy - all calculators do the same thing, can have the same interface. The latter is the one to solve, since it requires the different parameter for each calculator instance.

From what you listed is the factory the best solution. And it is not moving the problem elsewhere, it is solving the problem. Each factory knows its calculator and knows what parameters it needs. So the factory depends on calculator. Creating a calculator using a factory only creates a dependency on creating a calculator and not its parameters or the calculator itself, it can be part of the factory interface that all the factories implement.

Providing parameters for the factory can be solved by property injection using whatever means you choose - Spring definitely not bad in that.

The application in turn only know how to use a calculator so depends on the general calculator interface and not any particular calculator implementation.

查看更多
别忘想泡老子
4楼-- · 2019-02-10 15:52

This calculator uses an abstract FunctionAdapter to evaluate functions by name. Each concrete subclass implements an eval() method that takes a variable number of arguments.

查看更多
兄弟一词,经得起流年.
5楼-- · 2019-02-10 15:53

I always struggle to get runtime dependencies to work with Dependency Injection in an elegant way.

However, I would go for a variation on your "Make different interfaces" plan.

Each calculator would define it's own interface, say IAlwaysZeroCalculatorParameters, IPercentageCalculatorParameters etc.

Your MyService class can then implement all these interfaces, and then pass itself in to each calculator.

There is very little framework overhead to achieve this, and it means that each calculator expresses concisely what it needs. It works very well for me in similar situations, although in your particular case (where you don't have control over the plugins), it means you must release MyService every time someone implements a new calculator (so that it can implement the new interface). If required I would probably get round this with some Duck Typing.

查看更多
登录 后发表回答