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 classor 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:
-
$C$ itself.
-
An object created by $f$.
-
An object passed as an argument to $f$.
-
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:
-
No Train Wrecks: We replaced
context.options.scratchDirectory.absolutePathwith a single call tocontext.createScratchDirectory(). -
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
Contextclass. TheFileManager(the caller) never breaks. -
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.