Chapter 1: The Hidden Dangers of Primitive Obsession
In virtually every codebase, there exists a pattern that, when left unchecked, complicates comprehension and exposes your domain model to vulnerabilities. This phenomenon is known as "primitive obsession."
Consider this: Do you find yourself reading more code than you actually write? For many developers, the answer is yes. Understanding someone else's code can be a true measure of its quality and design.
Primitive types, while common, often lead to several issues:
- They obscure the actual requirements of your code.
- They increase the likelihood of violating the DRY (Don't Repeat Yourself) principle.
The following code snippet illustrates some of these concerns:
class MediumMember:
def __init__(self, mail: str):
self.mail = self.validate_email(mail)
@staticmethod
def validate_email(mail: str) -> str:
# Validate the email format with RegEx
pass
The MediumMember class requires a valid email address, represented as a primitive string. While the constructor demands a string input, it fails to communicate the necessary validation checks, leaving the reader with little context. The name of the parameter, mail, offers limited insight, and any mistakes might only surface during execution, resulting in an exception.
Section 1.1: The Problem of Duplicated Logic
Another significant drawback appears when a second class requires similar email validation logic.
class GoldMember:
def __init__(self, mail: str):
self.mail = self.validate_email(mail)
@staticmethod
def validate_email(mail: str) -> str:
# Duplicate email validation logic
pass
At first glance, the GoldMember class seems fine; however, it also requires the email to be validated, leading to the dreaded duplication of logic.
You might think of refactoring this into a separate EmailServices class, but that would not be an ideal solution.
Section 1.2: The Consequences of Primitive Obsession
The obsession with using primitive types results in several negative outcomes:
- The true requirements of the code remain unclear.
- The risk of violating the DRY principle increases, as validation logic may be repeated across multiple classes.
Chapter 2: Solutions to Primitive Obsession
Eliminating primitive obsession necessitates encapsulating validation logic within dedicated classes.
Instead of relying on a primitive string, you can create an Email class with a private constructor. This will require the use of a static factory function to instantiate the object, centralizing the validation of the email string.
class Email:
def __init__(self, mail: str):
self.mail = mail
@staticmethod
def create(mail: str) -> 'Email':
# Central email validation logic
pass
When creating a GoldMember, you can now use:
gold_member = GoldMember(Email.create("[email protected]"))
This approach allows you to replace the primitive type String with a complex Email type, enhancing the clarity and quality of your code.
To better understand how to map domain concepts into classes rather than relying on primitive types, consider reading “Domain-Driven Design: Tackling Complexity in the Heart of Software.”
Using Implicit and Explicit Operators
To facilitate the assignment of complex types to primitive types, you can implement implicit operators. This allows you to directly assign an Email object to a string.
class Email:
...
def __str__(self):
return self.mail
Additionally, an explicit operator can be created to convert a string into an Email object, ensuring that both directions of assignment are seamless.
@classmethod
def from_string(cls, mail: str) -> 'Email':
return cls(mail)
By adhering to these principles, you can create a robust codebase.
To further your development journey, explore eight enduring guidelines designed to help you succeed in coding and advance in your career.