Table of Contents
Intro
Decoupled code seems to be a lost art in Android development these days. Most developers have heard the software best practice of decoupling code but few truly understand it and fewer actually implement it. In order to understand some of the more complicated topics I plan to write about, knowing what and how to decouple code is a fundamental.
First, decoupled code is actually a misnomer. Loosely coupled code is actually what developers want to strive for. If your code is actually decoupled, then it does not do anything since it does not call any other code and no other code calls it. Developers use “decoupled” for brevity over “loosely coupled code.” Decoupled code is just easier to say and talk about rather than qualifying it every time with loosely coupled code. This article will be using the idiom of decoupled code since that is what I say on a regular basis.
Let’s start off with some completely decoupled code:
class Foo {
fun getSomeNumber(): Int {
// business logic that does something
return 42
}
}
Now if you noticed, this code is completely decoupled. There are no imports and does not use any other code at all. In reality, this is completely useless code if it uses nothing and no other classes use it. This decoupled code could be an interface, class, sealed class/interface, data class or anything else in Kotlin (or your preferred programming language). If no one uses it and it uses no other code then it is decoupled.
Coding to an interface
Most developers when talking about how to decouple code will say to code to an interface. How exactly do developers do this? Let’s add an interface to our code base:
interface Bar {
fun getNumber(): Int
}
And an implementation of Bar:
class BarImpl: Bar {
override fun getNumber(): Int {
return Random.Default.nextInt()
}
}
Foo and Bar still are completely decoupled since neither knows about each other. Let us now intentionally couple them:
class Foo {
private val bar: BarImpl()
fun getSomeNumber(): Int {
// business logic that does something with bar
val barValue = bar.getNumber()
return 42 * barValue
}
}
Foo is now forever more coupled with the interface, Bar and the concrete implementation, BarImpl. Foo will always need to know where Bar and BarImpl live and depend on them. So we just coded to an interface and it does not really get us anything. Our code is still coupled so why bother coding to an interface? And what if we don’t want Foo to know about BarImpl and instead just know about Bar? Let’s see what we can do about that and begin to actually decouple our code.
Dependency Injection
An extremely simple introduction to Dependency Injection is just that dependencies are passed in via Constructor, Method, or Field. Here are all three examples using Foo and Bar code:
// Constructor Injection
class Foo (private val bar: Bar){}
// Method Injection
class Foo {
private var bar: Bar? = null // notice the "var" keyword as well and have to handle it being null unless you used lateinit var
fun setBar(bar: Bar) { this.bar = bar) }
}
// Field Injection
class Foo {
lateinit var bar: Bar // notice no private modifier and var again
}
// Some other class
class ClientClass {
fun doSomething() {
val foo = Foo()
val bar = BarImpl()
foo.bar = bar // setting the field here e.g. field is now injected
}
}
// end Field Injection example
Out of all the above, you should only ever use Constructor Injection unless you are literally forced to do something else (more to come in a future blog post). Some developers think the purpose of dependency injection is to have “testable code.” That is a side effect of dependency injection, not its purpose. The reason for dependency injection is to decouple your dependencies, your code and the creation of those objects from the code that uses it. So now that we have Bar injected, what does this get us?
// Constructor Injection
class Foo (private val bar: Bar){
fun getSomeNumber(): Int {
// business logic that does something with bar
val barValue = bar.getNumber()
return 42 * barValue
}
}
Looking at the code, I hope you see that there is a class missing that we have previously used. That is BarImpl. Foo no longer has any knowledge about BarImpl and therefore it can live in a completely different library, package, module, etc. BarImpl is still coupled with Bar and will need to know where it lives. But Foo only depends on Bar now. We have successfully decoupled the implementation details that live in BarImpl from Foo, the class that uses it. We can now create an infinite number of Bar implementations. Foo can focus on its business logic and we can create however many Bar implementations that we need to meet our requirements. With Foo no longer knowing anything about BarImpl and depending solely on our Bar interface, what does that allow us to do?
Dependency Inversion
Let’s say that our real code is modularized by features. We now have an app module and a foo module with all code related to our Foo feature. This foo module has no dependencies or knowledge of the external world such as a database or the internet. How can our Foo feature call our database without it knowing about it? How could it potentially call a REST endpoint? That’s where dependency inversion comes in. Our app module knows about our database and the internet. Therefore, we can create a Bar implementation at the app level and pass it into Foo via dependency injection. Then Foo can call Bar’s getNumber() and the concrete implementation would call the database.
// app module
class DatabaseBar(private val database: Database): Bar {
override fun getNumber() {
// yes this is Disk IO so should either be run in a thread or coroutine.
database.executeSql("select count(1) from users where is_active = 1")
}
}
// Foo module - same code from previous example
class Foo (private val bar: Bar){
fun getSomeNumber(): Int {
// business logic that does something with bar
val barValue = bar.getNumber()
return 42 * barValue
}
}
When we pass in DatabaseBar into our Foo class, the Foo class can then call our database without having any knowledge that the database even exists (no dependency on it and therefore foo module is decoupled from the database).
Some other potential real life examples besides a Database implementation, are a REST implementation, GraphQL implementation, SharedPreferences implementation or test implementations for our unit and integration tests. The possibilities are endless. We went from Foo having one and only one internal implementation to having an unlimited number of possibilities. What happens if we want to limit the number of possibilities?
Factory pattern
If we want to limit how many implementations of an interface we have, there’s a few ways to do it. One of the easier ways to do it is to create Factory methods for Foo that do not allow clients to pass in any implementation of Bar that they want. The clients are limited to only three in this example: RandomBar, EverythingAnswerBar, and EvenBar. No other implementations can be used within Foo since the constructor is now internal (only accessible via the module) and there are three factory methods to create Foo and no other way to pass Bar implementations into Foo.
class Foo internal constructor(private val bar: Bar){
fun getSomeNumber(): Int {
// business logic that does something with bar
val barValue = bar.getNumber()
return 42 * barValue
}
// Factories for creating Foo objects
companion object {
fun createRandomFoo() = Foo(RandomBar())
fun createEverythingAnswerBar() = Foo(EverythingAnswerBar())
fun createEvenBar() = Foo(EvenBar())
}
}
class RandomBar: Bar {
override fun getNumber(): Int {
return Random.Default.nextInt()
}
}
class EverythingAnswerBar: Bar {
override fun getNumber(): Int {
return 42
}
}
class EvenBar: Bar {
override fun getNumber(): Int {
return Random.Default.nextInt() * 2
}
}
With the Factory pattern, we have now prevented more implementations of Bar than we control within our module. There are tradeoffs here if you go with this pattern such as your foo module may now have to depend on a database or some other dependency in order to create implementations in the foo module.
Summary
To sum up, decoupled code allows quite a few options when it comes to architecting code. You can code to a public interface using dependency injection that allows an unlimited number of implementations. You can inject a dependency that calls a database or REST endpoint and your feature code will not have any dependency on either. Or you can potentially limit how many implementations can be injected. All, by simply, writing decoupled code.
Hopefully the next time you write a class, you will think about what needs to live inside the class and what you should decouple from it. Just think about how much power you now have in your code base by decoupling code!
Happy coding!
Leave a Reply