Clean Code: Objects vs. Data Structures
In the world of clean code, it's useful to distinguish between objects and data structures.
- Data Structures are containers for data. They don't have complex behavior; their purpose is to store and expose data. Think of a simple
struct
or a plain old object. - Objects, on the other hand, are about behavior. They hide their internal data and expose functions that operate on that data.
Let's see why this distinction matters.
Encapsulation is Key for Objects
Objects should protect their internal state. Instead of letting other parts of the code directly poke at their data, they should provide methods to interact with it.
// Bad: This is more of a data structure, exposing its internal `temperature`.
class TemperatureSensor {
public temperature: number;
constructor(temperature: number) {
this.temperature = temperature;
}
}
const sensor = new TemperatureSensor(25);
sensor.temperature = 999; // Anyone can change it to an invalid value.
// Good: This is an object. It protects its data.
class TemperatureSensor {
private _temperature: number;
constructor(temperature: number) {
this._temperature = temperature;
}
get temperature(): number {
return this._temperature;
}
set temperature(value: number) {
if (value < -100 || value > 150) {
throw new Error('Invalid temperature');
}
this._temperature = value;
}
}
const safeSensor = new TemperatureSensor(25);
console.log(safeSensor.temperature); // Uses getter
safeSensor.temperature = 30; // The setter validates the new value.
Data Transfer Objects (DTOs)
When you just need to move data around, a simple data structure is perfect. These are often called Data Transfer Objects (DTOs). They have no behavior; they are just bags of data.
// Good: A simple interface for a DTO.
interface ProductDTO {
id: string;
name: string;
price: number;
}
// It's just data, so we can create it and pass it around.
const product: ProductDTO = { id: 'p1', name: 'Coffee', price: 5 };
Immutability
For data structures, it's often a good idea to make them immutable. This means that once they are created, they cannot be changed. This prevents a whole class of bugs related to unexpected state changes.
// Good: An immutable Cart object.
class ReadonlyCart {
constructor(private readonly items: ReadonlyArray<string> = []) {}
addItem(item: string): ReadonlyCart {
// It returns a *new* cart, instead of modifying the old one.
return new ReadonlyCart([...this.items, item]);
}
getItems(): ReadonlyArray<string> {
return this.items;
}
}
const cart = new ReadonlyCart();
const updatedCart = cart.addItem('Book');
console.log(cart.getItems()); // [] (original cart is unchanged)
console.log(updatedCart.getItems()); // ['Book']
To Sum Up
- Use objects when you want to enforce rules and behavior around data.
- Use data structures (like DTOs) when you just need to carry data around.
- Prefer immutability for your data structures to make your code more predictable.
Getting this balance right helps create a clean separation between the parts of your code that manage business logic and the parts that just move data.