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 Answers 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.
If I'm understanding your updated question correctly, we have:
app.engine
module which defines anapp.engine.Engine
class and exportsapp.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 anapp.view.View
interface, requires transitiveapp.engine
and exportsapp.view
.app
module which requiresapp.engine
andapp.view
and usesapp.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 View
s 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.
Explore related questions
See similar questions with these tags.
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 forEngine
? Could you perhaps add some example methods for each of those interfaces as well, to help understand your design?Engine
to simplify the case and added sampleView
interface methods. (please read post EDIT)