Uncle Bob’s Clean Code – Chapter 6: Objects and Data Structures.

This is one of the more profound chapters because it challenges a common misconception: that “everything should be an object.” Uncle Bob makes a sharp distinction between Objects and Data Structures, and mixing them up is a common source of architectural pain.


1. The Asymmetry

  • Data Structures: Expose their data and have no meaningful functions. (e.g., a Kotlin data class or a simple DTO).

  • Objects: Hide their data behind abstractions and expose functions that operate on that data.

The Trade-off

The choice between the two is a matter of how you expect the system to grow:

Approach Strength Weakness
Data Structures (Procedural) Easy to add new functions without changing the data structures. Hard to add new data structures because all functions must change.
Objects (OO) Easy to add new classes without changing existing functions. Hard to add new functions because all classes must change.

2. The Law of Demeter

This is the “Principle of Least Knowledge.” A module should not know about the innards of the objects it manipulates.

Specifically, a method $f$ of a class $C$ should only call methods of:

  1. $C$ itself.

  2. An object created by $f$.

  3. An object passed as an argument to $f$.

  4. An object held in an instance variable of $C$.

The “Train Wreck” Violation:

In Kotlin/Java, we often see code like this:

val outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath()

This is a “train wreck” because the calling code knows too much about the internal structure of ctxt, options, and scratchDir.


3. Data Transfer Objects (DTO)

Uncle Bob introduces the DTO—a class with public variables and no functions. This is the quintessential data structure. In modern Kotlin, this is almost always a data class.

Clean Code Example: Avoiding the Train Wreck

The “Dirty” Way (Violates Demeter):

fun initializeFiles(context: Context) {
    // Reaching three levels deep into the structure
    val path = context.options.scratchDirectory.absolutePath
    File(path).mkdirs()
}

The “Clean” Way (Object-Oriented):

To show the “Clean” way effectively, we need to transition from a Data Structure (where we pull data out) to an Object (where we request a service).

In the “Dirty” version, the caller is a “meddler” navigating through a tree of objects. In the “Clean” version, we follow the “Tell, Don’t Ask” principle.

The “Clean” Object-Oriented Implementation

Instead of the Context exposing its internals, we give it the responsibility to provide the final result we actually need.

// 1. The Data Structure (Internal detail, hidden from the caller)
data class Options(val scratchDirectory: File)

// 2. The Object (Hides its data, exposes behavior)
class Context(private val options: Options) {

    /**
     * Chapter 6: The Law of Demeter.
     * We don't return 'options' so the caller can dig through it.
     * Instead, we provide a high-level service.
     */
    fun createScratchDirectory(): File {
        val scratchDir = options.scratchDirectory
        if (!scratchDir.exists()) {
            scratchDir.mkdirs()
        }
        return scratchDir
    }
}

// 3. The Caller (Clean and decoupled)
class FileManager(private val context: Context) {
    
    fun setupEnvironment() {
        // The caller knows NOTHING about 'Options' or 'scratchDirectory'
        // It only knows that the context can provide a directory.
        val directory = context.createScratchDirectory()
        
        println("Ready to work in: ${directory.absolutePath}")
    }
}

Why this is superior according to Chapter 6:

  1. No Train Wrecks: We replaced context.options.scratchDirectory.absolutePath with a single call to context.createScratchDirectory().

  2. Information Hiding: If we decide to change how we store options (maybe from a class to a database or a remote config), we only change the Context class. The FileManager (the caller) never breaks.

  3. True Abstraction: We aren’t just hiding variables with getters; we are hiding the structure of the data.

Important Distinction: DTOs vs. Objects

If Context were simply a Data Structure (like a JSON response from an API), Uncle Bob says it is perfectly fine to expose its variables. The “Train Wreck” rule primarily applies to Objects that are supposed to have behavior.

Reflection Point:

In Kotlin, we love data class. However, if you start adding complex business logic (like the discount logic we discussed earlier) inside a data class, you are turning a Data Structure into a Hybrid. Uncle Bob warns that hybrids are the “worst of both worlds”—they are hard to add new functions to and hard to add new data structures to.

Loading more content...