Primitives are like old friends. They’re the first ones we turn to when writing code. And why not? They’re convenient and familiar.
But sometimes, familiarity breeds contempt. Overusing primitive types to represent everything - even seemingly simple abstractions like measuring units, postal codes, or email fields - can lead to a code smell known as Primitive Obsession. This can result in broken encapsulation, code duplication, and a whole host of other issues. But fear not! Value objects are here to save the day.
These custom classes provide features like equality checks and immutability, allowing you to represent domain ideas with greater clarity and precision. In this post, we’ll explore the dangers of Primitive Obsession and how to value objects that can help you write cleaner, more maintainable code.
Using primitive values can lead to broken encapsulation. Consider this: how often do you need to verify or transform a type in your application? Take temperature as an example. You might add validation when storing the value and again when updating it. But what about all the places where temperature is displayed? Without encapsulation, you’ll end up duplicating code and creating unnecessary complexity.
Encapsulation is a fundamental principle of object-oriented programming. It allows us to hide the internal details of an object and expose only what’s necessary through a well-defined interface. This makes our code more modular, maintainable, and reusable. But when we use primitive values to represent domain ideas, we lose the benefits of encapsulation. Instead of having a single place to manage the logic and behavior of a domain idea, we end up scattering it throughout our codebase.
This can lead to all sorts of problems. Code duplication makes our code harder to understand and maintain. It also increases the likelihood of introducing bugs and inconsistencies. By breaking encapsulation, we’re essentially making our code more fragile and less resilient to change.
But there’s a solution: value objects. By using custom classes to represent domain ideas, we can encapsulate their logic and behaviour in a single place. This makes our code more modular, maintainable, and reusable. And it helps us avoid the pitfalls of Primitive Obsession.
Value Objects to the Rescue
These custom classes come with a couple of important features.
- Value objects can answer equality checks.
Suppose you want to represent a currency as a tuple of <amount, currency>. Later, you might want to check if the two currencies are equal. Unless you’re careful with your code, you might get the wrong answer. That’s why value objects must implement a correct equality check method.
- Value objects should be immutable.
Delegating the equality check to the object can lead to aliasing bugs. As Martin Fowler defines it, “Aliasing occurs when the same memory location is accessed through more than one reference. Often this is a good thing, but frequently it occurs in an unexpected way, which leads to confusing bugs.” This can happen when dealing with value objects as we blur the lines between values and references.
Implementing value objects requires some extra work. To make your development faster, you might consider using a library that helps you with that. For example, if you’re using Python, you can use data classes to implement value objects.
Python Data Classes Example
Let’s take a closer look at how we can use Python data classes to implement value objects. In this example, we’ll create a
Currency value object that represents an amount of money in a specific currency. This will allow us to easily compare different currencies and ensure that our code is type-safe and easy to understand. Let’s get started!
from dataclasses import dataclass class Currency: amount: float currency: str def __eq__(self, other): if isinstance(other, Currency): return self.amount == other.amount and self.currency == other.currency return False # Example usage usd_1 = Currency(100, 'USD') usd_2 = Currency(100, 'USD') eur = Currency(100, 'EUR') print(usd_1 == usd_2) # True print(usd_1 == eur) # False
In this example, we define a
Currency class using the
dataclass decorator. We set the
frozen parameter to
True to make the class immutable. This means that once a
Currency object is created, its attributes cannot be modified.
We also override the
__eq__ method to implement a custom equality check. This method compares two
Currency objects and returns
True if their
currency attributes are equal, and
In the example usage, we create three
Currency objects: two with the same amount and currency, and one with a different currency. When we compare the first two objects using the
== operator, the result is
True, indicating that they are equal. When we compare the first object with the third object, the result is
False, indicating that they are not equal.
This is just one way to implement a currency value object using Python data classes. You can customize the class further by adding additional methods or attributes as needed.
In conclusion, Primitive Obsession is a code smell that can lead to issues such as broken encapsulation and code duplication. By using value objects, we can represent domain ideas with custom classes that provide features such as equality checks and immutability. This makes our code more modular, maintainable, and reusable.
In this post, we’ve explored the concept of Primitive Obsession and how value objects can help us write cleaner, more maintainable code. We’ve also looked at an example of how to implement a currency value object using Python data classes.
Remember, primitives, may be convenient and familiar, but overusing them can lead to problems. By using value objects, we can represent domain ideas more accurately and avoid the pitfalls of Primitive Obsession. So next time you’re tempted to use a primitive type to represent a domain idea, consider using a value object instead. Your code will thank you!