Curing Primitive Obsession
SummaryPrimitive obsession leaks domain logic. Tiny value types,...
Primitive obsession leaks domain logic. Tiny value types,...
Primitive obsession leaks domain logic. Tiny value types, like Java Records, wrap primitives to centralize validation, provide type safety, and improve readability. This prevents type-muddled errors and enables local reasoning by guaranteeing data validity through its type.
Curing Primitive Obsession
Primitive obsession is a code smell where primitive data types, such as Strings, Integers, and Booleans, are used to represent domain concepts that carry their own rules and constraints. This can lead to domain logic leaking across the codebase, as validation must be repeated at every call site. For instance, using a raw String to represent an email address or a product ID can cause ‘type-muddled’ errors, where a UserID is passed into a function expecting a ProductID.
Value Types as a Solution
Value types, specifically tiny types, can be used to wrap primitives and provide type safety. A tiny type is a small, immutable object whose identity is defined by its data rather than a unique identifier. By using tiny types, the compiler can prevent type-muddled errors, and validation logic can be centralized in the type’s constructor. This ensures that an object cannot exist in an invalid state.
For example, consider an EmailAddress type, which can be represented as a Java Record:
public record EmailAddress(String value) {
public EmailAddress {
if (value == null || !value.contains("@")) {
throw new IllegalArgumentException("Invalid email format");
}
}
}
In this example, the EmailAddress type wraps a String and provides a compact constructor for validation. This ensures that any EmailAddress object created will have a valid email format.
Benefits of Value Types
Using value types provides several benefits, including improved readability, reduced cognitive load, and better maintainability. By making method signatures intent-revealing, value types can reduce the need for manual validation and improve local reasoning. Additionally, value types can help prevent ‘Boolean Blindness’ regarding the validity of the data.
The following table compares the use of primitives versus value types:
| Aspect | Primitive (String) | Value Type (Record) |
|---|---|---|
| Validation | Manual/Repeated | Centralized in Constructor |
| Type Safety | Weak (Any string fits) | Strong (Only EmailAddress fits) |
| Readability | Low (register(String s)) | High (register(EmailAddress e)) |
| Maintenance | Expensive | Cost-Effective |
Best Practices for Implementing Value Types
When implementing value types, it is essential to follow best practices to ensure maximum benefit. These include:
- Using Java Records as the preferred vehicle for value types, as they provide built-in immutability and concise syntax.
- Centralizing validation logic in the constructor to ensure that an object cannot exist in an invalid state.
- Making method signatures intent-revealing by using the wrapped type, not the underlying primitive.
- Keeping value types shallowly immutable to prevent unexpected state changes in the domain model.
By following these best practices and using value types to wrap primitives, developers can improve the readability, maintainability, and safety of their codebase.