0

In a project, I have a task scheduling service, which is allowed to execute certain public methods from other services. Each service decides itself, which methods it want to make available for the task scheduling service by annotating them with @Operation. The task scheduling service simply will scan all given services for this annotation to get aware of methods it is allowed to be configured to execute. This means a user which wants to create a task will have a list of @Operation methods presented.

Let's take the example following example of an update service (one service among a few)

public interface UpdateService extends Service{

    @Operation
    public void update(String updateFileName);

    public List<String> getAvailableUpdateFileNames();

    @Operation
    public void ...

    ...
}

The way the UpdateService interface is declared, a user may create a task over the task service executing the update method. By letting the user freely configure a task with any string is mostly certain erroneous. The interface, in this case, therefore should present a list of predefined values, which can be used with the update method.

As it is, the update service also knows a getter method which reveals all available update. This would be ideal as a source for the predefined values.

That's why I came with the following approach:

public interface UpdateService extends Service{

    @Operation(sourceClass=UpdateService.class,sourceMethod="getAvailableUpdateFileNames")
    public void update(String updateFileName);

    public List<String> getAvailableUpdateFileNames();
}

The @Operation interface is introduced to a couple parameters. The task service still scans all given service interfaces for @Operation annotations, to present a defined list to the user.

The Task manager also understands the arguments:

  • sourceClass: This class has to contain the source method
  • sourceMethod: This method returns values which can be used for the annotated interface function.

With the help of reflection, the task service executes the sourceMethod and will offer the returned values generically as predefined values for the task configuration of the update(String updateFileName) method in the user interface. The user can select such a value and schedule the update method with this selected value as an argument.

This example is very basic and currently would only work if the annotated method would have 1 argument. Although it would be completely generic and decoupled.

Is there a better way to solve this issue? Or isn't this approach that bad at all?

  • This seems slightly over-designed. Why not use an interface rather then the @operation annotation? And if you want to be sure the arguments are values produced by a certain other method, just wrap them in a type. – JacquesB Aug 15 '17 at 11:48
  • @JacquesB It will be a spring environment and the Task service will only know the Service interface but not the UpdateService (Autowire). Through reflection, it still could achieve the wanted result. – Herr Derb Aug 15 '17 at 12:25
  • 1
    @HerrDerb: It still seem to me you are reinventing interfaces. You should probably clarify what problem you are trying to solve and why interfaces would not work. I don't see how an annotation is less of a hard dependency than an interface. – JacquesB Aug 15 '17 at 14:21
  • 1
    I really don't see why you would want to have an annotation instead of an operations() function that returns a list of Operations, that has method() and arguments() functions. Interfaces/classes are much more flexible than Annotations, especially since annotations have no behavior. – NickL Sep 8 '17 at 20:41
  • 1
    @NickL Hm you are right. Actually, I did something similar for the configuration context. I should add an interface Operational to each service offering operations. In this interface I will have a simple getOperations() method. The Operation methods then can contain action objects implementing the actually function. Makes absolutly sense – Herr Derb Sep 9 '17 at 7:47
1

The design you presented has issues

  • You're using an annotation where an additional method of your Service interface should probably be used instead. Something like an execute() method that can call getAvailableUpdateFileNames and then invoke update. You're already implementing Service. Why design this complex reflection-based system to invoke update?
  • The workaround to the problem you've designed with a parameterized annotation is short-sighted. It may solve your immediate issues with UpdateService, but you'll likely encounter another service that you want to behave slightly differently (like having two parameters as you've acknowledged), and you've set a precedent for introducing annotation parameters to solve these problems.
  • Annotations, while fancy and widely-used, introduce several problems that affect your code's readability, maintainability, and testability. I would not use them when another design is clearly present.

If you want to separate the concerns of updating and the service that executes it, I suggest collaboration.

enter image description here

Where UpdateService.execute is implemented like:

List<String> fileNames = updater.getAvailableFileNames();
updater.update(fileNames.get(0));

This design has the benefit that it can execute any method on any object instead of just those with a single parameter. The concerns are separated, and each piece can be unit-tested independently.

  • As the UpdateService is only a example, I might have another service that introduces multiple @Operation methods. Also I don't want to execute those methods in a row. I would like to get the getAvailableFileNames() first to present it to the user, which then invokes update with a selected file name – Herr Derb Aug 16 '17 at 6:02
  • I refactored the question. – Herr Derb Aug 16 '17 at 6:29
0

It seems to me you are reinventing interfaces. Annotations have their uses, but here you are coupling methods (update() and getAvailableUpdateFileNames()) through annotations. This is going down a rabbit hole, since you would also need to verify the types of the collection returned by the getter matches the type expected by the operation. This will be non-trivial, and is exactly what interfaces are intended to solve.

I understand you want a single class to implements multiple operations. I think you should reconsider this requirement. If an operation were a single method it might work, but since an operation is a bundle of interdependent methods, I think it would be breaking the single responsibility principle.

I suggest you define an Operation interface. Then you could have a Service class expose a collection of Operations.

  • Yes I would need to check if the getter method returns a collection of the argument type. As this is not type safe, the verification could be done with a test. What about the option to have a annotation for the setter @Operation(id=1) and a annotation for the getter @OperationDataSource(id=1,argIndex=0)? I'd still have the problem with the extra type check I need to cover, but the annotations would be separated in concerns. – Herr Derb Aug 18 '17 at 12:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.