0

I'm looking for the most proper way to design a modular application with ServiceLoader.

--MAIN IDEA--

  • module app.view | exports app.view.View interface that defines UI api
  • module app.engine | exports app.engine.Engine interface that defines Engine (core functionality) api
  • module app | (entry point) requires and uses app.view, app.engine in order to combine them into actual program

The app will be open for extension, anyone can add new implementations of app.view.View interface.

--CONCERNS--

Since app.view will be exported through exports keyword, it will be accessible in every other modules, also in unrelated ones. This feels like a design mistake. Is it?

On the other hand, if I'd change exports to exports to I could force it to be accessible only in related modules, but it will not be extension friendly anymore.

Is my design idea proper for that kind of application? Should I ignore my concerns? If so, why?

--EDIT--

I removed ability to extend app.engine.Engine just to simplify the case.

Each app.view.View implementation will provide new javafx.scene.Scene (which is basically whole UI "skin", as I'm working with JavaFX).

View interface looks like this:

public interface View {

    String name();
    
    javafx.scene.Scene createScene(Engine engine); //takes engine and maps buttons and ui functions to actual engine

}
2
  • Could you clarify please, would each View implementation be a different screen in the same UI, or would each one be an alternative UI for the whole app? And the equivalent question for Engine? Could you perhaps add some example methods for each of those interfaces as well, to help understand your design?
    – just me
    Mar 11, 2023 at 13:11
  • @justme I removed extendibility of Engine to simplify the case and added sample View interface methods. (please read post EDIT) Mar 11, 2023 at 13:54

2 Answers 2

2

I will try to address each of you concerns.

Exporting the app.view module with the exports keyword may not necessarily be a design mistake, as it allows for greater flexibility and extensibility. However, it does mean that you need to be careful about the interfaces and classes that you expose in the app.view module. You should only expose what is necessary for other modules to interact with the app.view module.

Using the exports to keyword instead of exports can provide some security benefits, but it would also make it harder for third-party developers to extend your application with new View implementations. This could limit the potential for your application to grow and evolve over time.

Overall, your design idea for the application seems reasonable. You are using the ServiceLoader pattern to allow for extensible View implementations, and you have defined clear interfaces for the View and Engine modules. As long as you are careful about what you expose in the app.view module and keep your design modular, I would say that it will be easy to extend and maintain your application over time.

0

If I'm understanding your updated question correctly, we have:

  • app.engine module which defines an app.engine.Engine class and exports app.engine. There might be other classes in that package which the view or entry point needs to be able to access, and there might be some internal engine code in other not-exported packages within this module.
  • app.view module which defines an app.view.View interface, requires transitive app.engine and exports app.view.
  • app module which requires app.engine and app.view and uses app.view.View.

Now you'll need to provide some implementation(s) of the View interface (assuming the app will include one or more standard "skins" when you ship it). You haven't mentioned them explicitly in the question but I'm assuming they're in additional modules, e.g., app.myspecificview, which requires app.view and provides app.view.View with app.myspecificview.SpecificView.

In which case, I don't see much of a downside in app.view using exports instead of exports ... to, unless you actively want to prevent others creating new skins for your app. The only code that's being made visible by that is the View interface. It will only be accessible if someone requires app.view from their own module, and realistically, they'll only do that if they're planning to create a new skin.

Similarly, the exports in app.engine are only making visible the minimum that's needed to write a UI skin for the app (at least, if you're following Sebastian's answer), and that's unlikely to cause any problems. In the worst case scenario, you might make a breaking change which means someone has to update their skin to keep it compatible with the latest version of your app - but in my mind that's still better than disallowing them to create any skins at all.

There is one place that exports ... to could still be useful in this scenario, and that's to hide the parts of the engine that only the entry point needs to be able to access. You could retain your original design of app.engine.Engine as an interface containing only the parts that Views need to be able to get to - the implementation would be in another package which is exported only to the app module. This way, only the entry point could instantiate the engine, and it would be possible to add methods to the implementation if there are other functions that only the entry point needs.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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