How do you structure your interface+impl. split? ...
# gradle
u
How do you structure your interface+impl. split? A
foo
folder with
api
and
impl
modules? Or
foo-api
and
foo-impl
in root? Or
foo
for api and
foo-impl
Or somehow nest the impl module inside api module? Also, what's your naming scheme for the interfacy module, api? contract? something else?
e
regardless of what you name the folders, you should aim to keep the Gradle module names unique https://github.com/gradle/gradle/issues/847
u
yea .. im just probing the world, what do folks use, because im getting paralysis by analysis
f
this is highly subjective, but this is how I like to do it. I have 5 top level folders (core, data, domain, testing, ui), then I have subfolders for logically related features, and then subfolders for api and impl. Not all modules have both api and impl, here's a tree of one sample project
Copy code
├── app
├── core
│   ├── time
│   │   ├── time-api
│   │   └── time-impl
│   └── type
│       ├── either
│       └── weather
├── data
│   └── repository
│       ├── city
│       │   ├── cityrepo-api
│       │   └── cityrepo-impl
│       ├── favorite
│       │   ├── favoriterepo-api
│       │   └── favoriterepo-impl
│       ├── recents
│       │   ├── recentsrepo-api
│       │   └── recentsrepo-impl
│       └── weather
│           ├── weatherrepo-api
│           └── weatherrepo-impl
├── domain
│   └── interactor
│       ├── city
│       │   ├── cityinteractor-api
│       │   └── cityinteractor-impl
│       └── weather
│           ├── weatherinteractor-api
│           └── weatherinteractor-impl
├── testing
│   ├── fake
│   └── macrobenchmark
└── ui
    ├── feature
    │   ├── favorites
    │   ├── home
    │   ├── search
    │   └── weather
    └── shared
        ├── assets
        ├── composable
        │   ├── common
        │   └── weather
        ├── deviceclass
        ├── mvi
        ├── route
        ├── styles
        └── weathericon
u
that is kinda-sorta what I have, but one thing that bugs me, is the asymetrical nature of this, I mean, why do I include both
:core:type:either
and then
:domain:interactor:city:cityinterfactory-api
probably the question is ..should the the consumer care that it's just a api?
shouldn't that be a implementation detail of the module? that it's just interfaces?
e
if there's only one implementation I do not see any point in having separate `api`/`impl` modules
f
well, not everything is split into api and impl as I was saying, in this particular example, the
either
module is just a wrapper for a success/failure, so it's a single module
f there's only one implementation I do not see any point in having separate `api`/`impl` modules
testing
and build performance, if you change impls nothing outside that impl will recompile as everything else depends on the api
e
compile avoidance has gotten better
u
well, the point of the split is mostly compilation avoidance, 90% of the work is on the impl, and that way only the module+app module gets recompiled (as only app depends on impl)
hm, isnt compllation avoidance at a module level granularity?
u
I presume you then have
internal
implementations?
e
yes
u
I'm not sure if dagger works with that .. does it?
f
I presume you then have
internal
implementations?
yes
dagger works with internal impls
u
but not sure if anvil does as well, will have to check
anyways, regardless, how does sufixless interfaces module sounds? i.e. only to suffix the impl module? this way consumer doesnt have to know this detail .. "just give me the type, I don't care if you have implementations in the module hidden or in a different module"
f
this is a styling thing, it makes no difference, choose whatever works best for you
e
that's more confusing IMO, but it doesn't really matter
u
noo it matters, its design 😄
why do you find it more confusing?
f
you can also use a name for the api, for instance
weatherrepo
and then have a
remoteweatherepo
and a
localweatherrepo
modules as impls, for instance
u
yea that's kinda in the same spirit
e
weatherrepo-api
,
weatherrepo-remote
and
weatherrepo-local
naming would be more conventional
if there is a
weatherrepo
it would normally be an umbrella that pulls in both the api and implementation
f
yes, I agree with that, that's what I go for
u
okay maybe let's step back, why the split in
weatherrepo
, and not in
core:type:weather
?
how is it different
is it maybe "when there is DI involved"?
e
having both
core:type:weather
and
core:impl:weather
will cause issues with Gradle and Maven artifact names
f
the type module here is just defining some enums and common types that are used accross the app, there is no api/impl
e
unless you put them in separate groups, which is unusual within a single build (and still confusing for distribution)
u
@Francesc do you use dagger/anvil/hilt?
f
dagger with hilt
this is the repo I used for the tree above: https://github.com/fvilarino/Weather-Sample
u
so your key is probably "where there is dagger included" (as what modules split)?
f
well, for the data, core and domain folders, that maps, but at the UI layer there is no such split
u
so what is the reasoning 😄 I hate that it's just gut feeling probably
f
it's abstraction, when you want to isolate clients from implementation details (whether you are using a remote source or a local source, for instance), you would create an API so that you can then change implementations as needed without creating a cascade of side effects across all clients
u
if you have two implementations sure, but this split is mostly done for things with a single implementation as @ephemient noted
f
yes, but today I use retrofit, tomorrow I might use ktor, that isolates the changes to 1 module. Also, having an API allows me to easily create fakes for tests
u
would that change, if your impls are internal? consumers won't feel it
e
well fakes would be more than one implementation. but I don't see why that split makes it any easier
export fakes from the test fixtures of that module
f
APIs can't be internal, that's the point of an API
u
I started with a split as a way to break apart a monolith, tbh it's the only way
f
do test fixtures work now? last I checked they needed work due to kapt
u
what about the apis? I mean the impl, you can swap out retrofit for ktor even if it's a single module
guess it hinges on the fact whether compilation avoidance works or not
f
yes, but you do not want your clients to know the internal details of your implementations, they should be agnostic. I guess this is a bit more philosophical than anything else
u
how do you mean "know" .. visibility would be internal
e
internal
implementations aren't exposed to clients (unless they break the abstraction boundaries, which is always possible on JVM)
u
I mean I do splits, and I like em, but .. I don't know when to do them and when not to..feels arbitrary
e
anyhow, even if they were
public
, compile avoidance works as long as the ABI doesn't change
u
unless I'm isolating android, this way android plugin won't infect everything
f
yes, when I say "know" I mean that you can poke at the details. I think of modules as separate libraries, the same way that when you use a 3rd party library, you only see the API, you do not see the actual implementation. I like to treat my modules the same way, I can go poke at the impl if I want, but I should not have to
u
yep, android infection is a +1 for the split
not sure how to read that .. with a 3rd party libs, they do precisely just a single module, say retrofit, no?
f
that's what you see, you only see the public API and you do not care what happens when you call those APIs, you just care about what the API does, not how it does it
u
yes of course, but retrofit doesn't provide
retrofit-api
module, is what I meant
f
well, that's just naming conventions
u
are you sure their implementations are in a different artifact?
f
no, I do not imply that, I don't know how it's architected, but it's a public library so you can check
u
I think I got lost 😄 maybe a step back .. if compilation avoidance worked 100%, would you still split interface and implementation into separate modules?
f
I would, I find it much cleaner to have separate modules for the API and IMPL and it makes testing also easier
u
even if you have a single implementation of a given interface?
I mean, aren't we just inventing visibility?
f
personally, yes
as I said earlier, I treat my modules like 3rd party libraries
e
I don't. you're always able to dig into the implementation at runtime, there's nothing preventing you from cheating aside from promising not to. visibility is enough to prevent you from accidentally violating that
u
I don't understand the 3rd party library argument, how is that a plus for the split? almost no library is ever split
e
testing works fine with implementations that happen to be on the classpath but are invisible
and yeah, in my experience, 3rd party libraries are largely single-implementation, with only specific frameworks like slf4j being exceptions
f
when I say I treat it like a 3rd party library, I mean that I can look at the API and know everything I need to know to use that module, I don't need to know anything about the implementation to use it
e
how is that any different with a first party library
f
it's not, but unlike a 3rd party library, I can see the impl. I just want to ensure that the API is self explanatory
e
I can see the impl of all the 3rd party libraries I use
u
btw what about @Module classes for dagger, can those be internal?
e
doesn't mean I have to, but I can
f
if they're public, but that's besides the point
btw what about @Module classes for dagger, can those be internal?
yes
u
how do you attach them to the component then ..hilt?
e
if you're writing your components in Kotlin, the modules it uses need to be public
f
you need to depend on them on your
:app
module
e
right, hilt will autowire them (in Java, so
internal
has no effect)
u
I don't fllow. Don't you need to add the module class reference to the @Component modules declaration?
okay so hilt, sure
I'm going to try anvil, fingers crossed
if I can't do that, then split everything 😄 I won't expose a FooModule publicly to features
e
eh, I don't see why that's necessary
u
why, don't you package the @Modules with your implementations?
e
my project just has some custom lint rules that prevent us from using the DI classes outside of where we expect them to be
u
well..sure, but by that logic, why can put everything public, and write lint rules for what is meant to be private 😄
e
sure we can, in fact our iOS codebase does that because of Xcode bugs making large numbers of modules unusable
u
lol
f
that would pollute your scope though, everything shows in autocomplete
u
I'll ride AppCode till its dead
e
but for the most part we split our modules by how we logically work on them
u
by split you don't mean the api+impl split?
e
yeah, stuff like module-per-feature
u
yea that's a given .. I wonder why I started with the api+impl split ... refactoring a monolith + android isolation say for providing location, you wouldnt be able to do that witha unsplit module if location was a single module, then it would need android dependency, infecting every feature, turning it android .. and that's where compilation speed goes to die
Copy code
com.squareup.anvil.compiler.api.AnvilCompilationException: Back-end (JVM) Internal error: sk.o2.net.IpAddressProviderImpl is binding a type, but the class is not public. Only public types are supported.
so internal classes don't even work with anvil, ugh.. a non started for collapsing the split 😄