Skip to content
:tech
← Articles

AI-translated. Read the original in KoreanEmbedded slides, videos and images remain in their original language.

A Junior Engineer Meets Clean Architecture

Damon Gong
Damon Gong

Software Engineer

A Junior Engineer Meets Clean Architecture

Hello, I'm Damon, a new engineer at Buzzvil. In this post, I will describe a design problem I encountered as a junior developer and share how guidance from my colleague JD helped me improve the design.

Why good design matters

In Clean Architecture, Robert C. Martin defines good design as reducing the people, cost, and time required to develop and maintain a system. Building a system matters, but maintaining it afterward is just as important. Software requirements keep changing, and new features are added over time.

Hardware and software maintenance costs (SE, Maintenance, Hans & Vilet, 2008)

As the graph shows, software maintenance costs are comparable to—or higher than—development costs. That is how important maintenance is. Design concepts such as Clean Architecture, DDD, and SOLID can reduce the tangible costs of development and maintenance through better design.

Designing with SRP, OCP, and DIP

SOLID is a well-known set of principles for good design. I will use SRP, OCP, and DIP as examples to explain how they can help.

Here is what they mean:

  • SRP (Single Responsibility Principle): A class should have only one responsibility.
  • OCP (Open/Closed Principle): Software components should be open for extension but closed for modification.
  • DIP (Dependency Inversion Principle): When creating dependencies, depend on things that are less likely to change rather than things that change easily.

The starting point

I was developing a unit/UI testing library and had written the following code.

  • The TestRunner class receives utility classes needed for testing through dependency injection.
  • RUN_TEST is a function that executes the test block passed as an argument.
  • The TestUtil class runs before and after the test inside RUN_TEST.

TestUtils.kt

interface TestUtils {
    fun runBeforeTest()
    fun runAfterTest()
}

TestRunner.kt

class TestRunner(
    private val testUtils : List<TestUtils>
) { 
    fun RUN_TEST(block : () -> Unit) {
        testUtils.forEach(TestUtil::runBeforeTest)
        
        //RUN TEST
    
        testUtils.forEach(TestUtil::runAfterTest)
    }
}

The scenario

  • The client wanted a feature that displayed test execution time in the system log.
  • I added the following class to meet that requirement.

ExecutionTimeSystemLogger.kt

class ExecutionTimeSystemLogger() : TestUtil {
    private var startTime : Long = 0
    
    override fun runBeforeTest() {
        startTime = System.nanoTime()
    }
    
    override fun runAfterTest() {
        val endTime = System.nanoTime()
        val executionTIme = endTime - startTime
        println("TIME: $executionTime")
    }
}

This design fully satisfies the client's requirement. However, after reviewing my code, JD left the following feedback.

  1. It violates the Single Responsibility Principle (SRP).
    • It is responsible for calculating execution time.
    • It is responsible for outputting the result.
  2. It violates the Open/Closed Principle (OCP).
    • If the client also wants output through the file system, the existing code must change.
  3. It violates the Dependency Inversion Principle (DIP).
    • TestRunner depends on the concrete class ExecutionTimeSystemLogger.

These problems make software difficult to change. For example, what would happen if this code were in use and the client asked to write logs to a file instead of the system log? I would probably create an ExecutionTimeFileLogger. Much of that class would duplicate ExecutionTimeSystemLogger. If the time calculation or output format changed, both classes would need to be updated. As more output destinations were added beyond the system log and files, maintaining the system would become increasingly difficult.

Building flexible software that meets requirements

SOLID is one of the established ways to handle situations like this and design flexible software. With JD's guidance, I improved the code to follow these principles.

Logger.kt

interface Logger {
    fun log(message: String)
}

SystemLogger.kt

class SystemLogger() : Logger {
    override fun log(message: String) {
        println(message)
    }
}

ExecutionTimer.kt

class ExecutionTimer(
    private val logger: Logger
) : TestUtil {
    private var startTime : Long = 0

    override fun runBeforeTest() {
        startTime = System.nanoTime()
    }
    
    override fun runAfterTest() {
        val endTime = System.nanoTime()
        val executionTIme = endTime - startTime
        val message = "TIME: $executionTIme"
        logger.log(message)
    }
}

This improved the design in the following ways.

  • ExecutionTimeSystemLogger is separated into ExecutionTimer and Logger, each responsible for its own work.
    • Each class has one responsibility.
  • SystemLogger implements Logger. If the client wants logs written to a file instead of the system log, we can simply implement FileSystemLogger.
    • The design is open for extension.
  • ExecutionTimer depends on Logger. Because Logger is unlikely to change, the likelihood of having to change ExecutionTimer as a result is also lower.
    • Depend on what stays stable, rather than what changes easily.

Now we can respond to the following client request by adding FileSystemLogger as shown below.

“Please add a feature to write execution times to a file.”

class FileSystemLogger() : Logger {
    override fun log(message: String) {
        //File System Logging..
    }
}

With this design, changes to the execution-time calculation or output format do not affect the new logger. Another important benefit is that separating ExecutionTimeSystemLogger into ExecutionTimer and Logger makes Logger reusable elsewhere.

Closing thoughts

I have shared how a new engineer at Buzzvil improved code with guidance from a colleague. I will be back with another interesting post. And if you join Buzzvil as a junior engineer, many of our developers will explain in detail through code reviews how to design software more flexibly!

Apply for an engineering role at Buzzvil

Have a coffee chat with a Buzzvil tech recruiter

All articles
Article

How Buzzvil Uses AWS

Buzzvil operates the Korean, Japanese, and Taiwanese versions of Honeyscreen, our rewarded lockscreen app, along with the BuzzAd ad network and Buzzscreen lockscreen SDK. Just six…