This is not the commonly asked question about the difference between api
and implementation
, and hopefully will be more advance and interesting from the point of view of architecting a multi-module project.
Let say I have the following modules in an application
library
base
feature1
feature2
app
Now the relations between the modules are:
base
wraps library
feature1
and feature2
make use (depends) on base
app
puts together feature1
and feature2
Everything in this multi module structure should be able to work using Gradle's implementation
dependencies and there's no need to use the api
clause anywhere.
Now, let say feature1
needs to access an implementation detail of base
included in library
.
In order to make library
available to feature1
we have two options as far as I can tell:
Change implementation
for api
in base
to leak the dependency to modules that depend on base
Add library
as an implementation
dependency to feature1
without having base
leak the dependency on library
Of course the example has been simplified for the sake of the question, but you understand how this can became a configuration hell with a big number of modules with 4 or 5 levels of dependencies.
We could create a base-feature
intermediate module that can wrap base
and provide another level of abstraction for feature1
to consume without leaking library
, but let's leave that solution out of the scope of this problem to focus on the setup of the dependencies.
Some trade-offs that I detected on the above options:
Option 1) pros
build.gradle
's files, as no need to repeat implementation
clausesapi
clause and see the changes propagated to all consumer modulesOption 1) cons
Option 2) pros
Option 2) cons
implementation
clause have to be modified. Even though I believe this is a good thing because it keeps track exactly of how a change modified the project, I see how it can take more time.Now the questions:
Is there any trade-offs in terms of compilation of this multi-module scenario?
Is a module leaking a dependency "faster" to be compiled for consumer modules?
Does it make a substantial difference in build times?
What other side effects, pros/cons am I missing ?
Thanks for your time.
In the first scenario, LibraryD is compiled by using api . If any change is implemented inside LibraryD, gradle needs to recompile LibraryD, LibraryB and all other modules which import LibraryB as any other module might use implementation of LibraryD.
A Dependency represents a dependency on the artifacts from a particular source. A source can be an Ivy module, a Maven POM, another Gradle project, a collection of Files, etc... A source can have zero or more artifacts.
In a multi-project gradle build, you have a rootProject and the subprojects. The combination of both is allprojects. The rootProject is where the build is starting from. A common pattern is a rootProject has no code and the subprojects are java projects.
Reposting from the Gradle forum thread.
What you describe is a fairly common discussion about layered architecture systems, also known as "strict" vs "loose" layering, or "open" vs "closed" layers. See this (hopefully free for you too) chapter from Software Architecture Patterns for some semiotics which is unlikely to help you much with your choice
From my point of view, if a module needs to break layering, I'd model the project structure to expose this in the most direct and visible way. In this case it means adding library
as implementation dependency of feature1
. Yes it makes the diagram uglier, yes it forces you to touch few more files on upgrade, and that is the point - your design has a flaw and it is now visible.
If few modules need to break the layer encapsulation in the same way, I may consider adding a separate base module exposing that functionality, with a name such as base-xyz
. Adding a new module is a big thing, not because of the technical work, but because our brain can handle only so many "things" at a time (chunking). I believe the same would hold for Gradle "variants" when they become available, but I can't claim that yet as I haven't tried them hands on.
If all clients of the base
module need to access library
(i.e. because you use classes or exceptions from library
in your public signatures) then you should expose library
as API dependency of base
. The downside of that is that library
becomes part of the public API of base
, and it is probably bigger than you would like, and not under your control. Public API is something you are responsible for, and you want to keep it small, documented, and backwards compatible.
At this point you may be thinking about jigsaw modules (good), osgi (err... don't), or wrapping the parts of lib that you need to expose in your own classes (maybe?)
Wrapping only for the sake of breaking dependencies is not always a great idea. For one it increases the amount of code you maintain and (hopefully) document. If you start doing small adaptations in the base
layer, and the library
is a well known library, you introduce (value added) inconsistencies - one needs to always be on guard whether their assumptions for lib still hold. Finally, often the thin wrappers end up leaking the library design, so even if they wrap the API - that still forces you to touch the client code when you replace/upgrade lib, at which point you may have been better off using lib directly.
So, as you can see, is about trade-offs and usability. The CPU doesn't care where your module boundaries lie, and all developers are different - some cope better with large amount of simple things, some cope better with small number of highly abstract concepts.
Don't obsess about the best (as in What Would Uncle Bob Do) design when any good design would work. The amount of extra complexity that is justified for the sake of introducing order is a fuzzy quantity, and is something that you are in charge of deciding. Make you best call and don't be afraid to change it tomorrow :-)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With