Luny
Back

Clean Architecture Demo

I noticed a lot of clean architecture specifications don't have any demonstration on what it would look like in practice. Taking the chance of learning it in a course of Software Architecture, I think I would like to write up a demo just like so!

Published on
Updated on

4 min read

As you may recall, we have gone over Clean Architecture by Uncle Bob during one of the lectures, and as of that, our assignment is to provide a (very simple) demonstration of said architecture in any of our projects.

Very simple?

The “very simple” part is what I added myself.

Reminding

Clean Architecture by Uncle Bob splits the software into 4 separate layers:

  1. Entities: which contain business level entities that are pretty resilient to requirement changes.
  2. Use cases: the layer that dictates the flow of data, and what to make of the data of the business entities. Basically the logic level of the software.
  3. Adapters: this layer includes controllers (which takes in inputs, data from layer 4 and convert it into data that layer 2 would understand), presenters (which takes in data from the layer 2, convert it into a form that layer 4 can display on UIs or save in databases or any form that a driver can understand). You can imagine this as a bridge between use cases and frameworks.
  4. Frameworks and Drivers: the layer that includes various outside-word related items, such as databases, UIs and devices.

A few notes about the architecture is that:

Demonstration

Entities (Layer 1)

Let’s keep it simple. We want to keep track of Mario’s stats (including HP, heart points, also known as health in other games, and FP, flower points, also known as mana or elixir in other games). Mario will be our only entity here.

class Mario (
    var hp: Int,
    var maxHp: Int,
    var fp: Int,
    var maxFp: Int,
)

Use Cases (Layer 2)

For the sake of the example, we’ll be setting up two simple use cases, to even start setting up the battlefield system for Mario, we need two preliminary system use cases: GetMarioStatus and CreateMario, respectively allowing us to see what Mario’s stats are at and being able to create a Mario object.

class CreateMarioUseCase {

    fun execute(hp: Int, fp: Int): Mario {
        return Mario(hp, hp, fp, fp)
    }

}
class GetMarioStatusUseCase {

    fun execute(mario: Mario) = buildString {
        append("${mario.hp}/${mario.maxHp}HP")
        append("  ")
        append("${mario.fp}/${mario.maxFp}FP")
    }

}

Notice, how our return data types and parameters are only those data types known to the use case layer, which is only the entities layer’s names and functions.

Interface Adapters (Layer 3)

In this layer, I created a MarioController which allows us to basically “control” Mario, in a real development project, this would also consist of letting Mario attack, defend, use items and flee from battle, etc. If you’re working with a Spring Boot application for example, the controller would be the code calling up business methods with data retrieved from parameters and bodies.

There’s also a MarioPresenter, which also allows us to view and present Mario’s data, essentially converting data from the use case layer to something that an outside driver can understand.

class MarioController {

    private lateinit var mario: Mario

    fun createPresenter() = MarioPresenter(mario)

    fun createMario() {
        val createMarioUseCase = CreateMarioUseCase()
        mario = createMarioUseCase.execute(10, 5)
    }

}
class MarioPresenter(val mario: Mario) {

    fun getMarioStatus(): String {
        val getMarioStatusUseCase = GetMarioStatusUseCase()
        return getMarioStatusUseCase.execute(mario)
    }

}

Notice, that in our controllers and presenters, we invoke use cases instead of creating or accessing Mario’s Data on our own. The reason for this is that usually if you have a need to switch out a layer, as long as these use cases hold on to the same signatures, you can change the implementation or configuration as much as you want.

Frameworks and Drivers (Layer 4)

The final outermost layer here, I simulated with the Console as the “UI” part of the application, mainly with the println function from Kotlin. The issue here is that the println function only understands String, it shouldn’t have to mind what the Mario object is and how to manage it. For a Spring Boot Application, this usually is the view engine, as you need to parse the data properly so the view engine can understand.

    private lateinit var marioController: MarioController
    private lateinit var marioPresenter: MarioPresenter

    fun setup() {
        marioController = MarioController()
    }

    fun start() {
        println("Setting up Mario Controller")
        marioController.createMario()
        marioPresenter = marioController.createPresenter()

        println("Getting Mario Status")
        println(marioPresenter.getMarioStatus())
    }

Here in layer 4, we don’t care about the details below, we just care that we can have a controller to act with Mario, and the presenter to retrieve the data in a way that the UI (basically Strings for println) can understand.