LY Corporation Tech Blog

We are promoting the technology and development culture that supports the services of LY Corporation and LY Corporation Group (LINE Plus, LINE Taiwan and LINE Vietnam).

This post is also available in the following languages. Japanese

Improving code quality - Session 59: Class facade with water

The original article was published on February 20, 2025.

Hello, I'm Munetoshi Ishikawa, a mobile client developer for the LINE messaging app.

This article is the latest installment of our weekly series "Improving code quality". For more information about the Weekly Report, please see the first article.

Class facade with water

Suppose there is a data model called FooValue and a class called FooValueStore that saves it. FooValueStore is defined within a package called repository and provides basic operations such as save, load, and delete.

// In repository package
class FooValueStore {
    fun save(id: FooId, value: FooValue) { /* ... */ }
    fun load(id: FooId): FooValue? { /* ... */ }
    fun delete(id: FooId) { /* ... */ }
    fun getAllIds(): List { /* ... */ }
}

// In caller package
class Caller(private val valueStore: FooValueStore) {
    fun doSomething() {
        // Uses `valueStore`
    }
}

Later, suppose a new feature is needed to delete all FooValue entries. To achieve this, a class called FooValueStoreClearer was created in the same repository package as FooValueStore. The clearAll method of this class uses delete and getAllIds to delete all FooValue entries. To use this class, an instance of FooValueStoreClearer must be maintained separately from FooValueStore.

// In repository package
class FooValueStoreClearer(private val valueStore: FooValueStore) {
    fun clearAll() {
        valueStore.getAllIds().forEach { valueStore.delete(it) }
    }
}

// In caller package
class Caller(
    private val valueStore: FooValueStore,
    private val clearer: FooValueStoreClearer
) {
    fun doSomething() {
        // Uses both valueStore and clearer
    }
}

Is there anything that should be improved about this structure?

Mirror to see one's own facade

The problem with this code is that to use FooValueStore and FooValueStoreClearer correctly, you need to understand their relationship. For example, to know whether Caller.valueStore and Caller.clearer.valueStore must be the same instance, you need to check the implementation of FooValueStore and FooValueStoreClearer. If they "must be the same instance," you would need to examine all constructor calls of Caller to ensure that this constraint is met. Additionally, whether FooValueStore and FooValueStoreClearer can be used in parallel cannot be confirmed without looking at the implementation.

This issue arises from trying too hard to minimize the responsibility of FooValueStore by separating the implementation of clearAll into another class. As a result, the caller Caller becomes more complex. To improve this, instead of forcing the responsibility of FooValueStore to remain small, it is better to focus on the simplicity of the interface exposed to the caller code. In this example, it is acceptable to implement clearAll directly within FooValueStore. (Of course, depending on the language, it can also be defined as an extension function.)

class FooValueStore {
    fun save(id: FooId, value: FooValue) { /* ... */ }
    fun load(id: FooId): FooValue? { /* ... */ }
    fun delete(id: FooId) { /* ... */ }
    fun getAllIds(): List { /* ... */ }

    fun clearAll() {
        getAllIds().forEach { delete(it) }
    }
}

class Caller(
    // Requires only `FooValueStore` here
    private val valueStore: FooValueStore
) {    
    fun clearAllValues() {
        valueStore.clearAll()
    }
}

By implementing the clearAll method within FooValueStore, Caller only needs to know about the functionality of FooValueStore. There is no need to worry about the relationship between two objects, making it easier to design in line with the principle of least knowledge.

In a nutshell

When determining the scope of a class's responsibility, focus on whether the interface is simple from the caller's perspective.

Keywords: interface, encapsulation, single responsibility principle

List of articles on techniques for improving code quality