Smell Detail

Primitive Obsession

基本类型偏执

Bloaters

Overuse of basic primitive types instead of creating small objects to represent domain concepts.

Define

Introduction

Primitive Obsession arises when primitive data types (int, string, etc.) are used excessively to represent domain concepts that deserve their own types. This reduces expressiveness and makes the code harder to maintain.

Key Identifiers

  • Heavy use of primitive types to model rich concepts
  • Validation logic duplicated and scattered
  • Lack of type safety (easy to swap arguments of the same primitive type)
  • Code lacks self-explanatory intent

Refactoring Suggestions

  1. Introduce Value Objects: Create small domain classes to represent concepts
  2. Encapsulate Validation: Put validation rules inside those value objects
  3. Increase Type Safety: Prevent parameter mix-ups by using distinct types
  4. Enhance Readability: Make intent explicit through domain-specific names

Explanation

Create value objects like Email, Phone, and Address instead of using raw strings. This centralizes validation and increases both safety and readability.

Code Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
class User {
private String firstName;
private String lastName;
private String email; // Should be Email value object
private String phone; // Should be PhoneNumber value object
private String street; // Should be Address value object
private String city;
private String zipCode; // Should be ZipCode value object
private String country;
private String currency; // Should be Currency value object
private double salary; // Should be Money value object
private String employeeId; // Should be EmployeeId value object
private String socialSecurityNumber; // Should be SSN value object
// Validation logic scattered and duplicated
public boolean isValidEmail(String email) {
return email != null && email.contains("@") && email.contains(".");
}
public boolean isValidPhone(String phone) {
return phone != null && phone.length() >= 10 && phone.matches("\\d+");
}
public boolean isValidZipCode(String zipCode) {
return zipCode != null && zipCode.matches("\\d{5}(-\\d{4})?");
}
public boolean isValidEmployeeId(String employeeId) {
return employeeId != null && employeeId.matches("EMP\\d{6}");
}
public boolean isValidSSN(String ssn) {
return ssn != null && ssn.matches("\\d{3}-\\d{2}-\\d{4}");
}
// Methods that show lack of type safety
public void updateContact(String email, String phone) {
// Easy to accidentally swap these parameters!
if (isValidEmail(email)) {
this.email = email;
}
if (isValidPhone(phone)) {
this.phone = phone;
}
}
public void updateAddress(String street, String city, String zipCode, String country) {
// Too many string parameters - easy to mix up
this.street = street;
this.city = city;
if (isValidZipCode(zipCode)) {
this.zipCode = zipCode;
}
this.country = country;
}
public void updateEmployeeInfo(String employeeId, String ssn, double salary, String currency) {
// More primitive obsession - mixing different concepts
if (isValidEmployeeId(employeeId)) {
this.employeeId = employeeId;
}
if (isValidSSN(ssn)) {
this.socialSecurityNumber = ssn;
}
this.salary = salary;
this.currency = currency;
}
public String getFullAddress() {
return street + ", " + city + " " + zipCode + ", " + country;
}
public String getFormattedSalary() {
return currency + " " + String.format("%.2f", salary);
}
// Business logic mixed with validation
public boolean canReceivePromotion() {
return isValidEmployeeId(employeeId) &&
salary > 50000 &&
socialSecurityNumber != null;
}
// Demonstration of problems with primitive obsession
public static void demonstrateProblems() {
User user = new User();
// Easy to mix up parameters of same type
user.updateContact("123-456-7890", "[email protected]"); // Oops! Swapped email and phone
// Long parameter lists with similar types
user.updateAddress("USA", "New York", "123 Main St", "10001"); // Wrong order!
// Validation logic scattered and repeated
String email1 = "[email protected]";
String email2 = "[email protected]";
// Need to validate emails everywhere they're used
if (user.isValidEmail(email1) && user.isValidEmail(email2)) {
// Do something with emails
}
}
// Getters and setters with no validation
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; } // No validation!
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; } // No validation!
public String getEmployeeId() { return employeeId; }
public void setEmployeeId(String employeeId) { this.employeeId = employeeId; } // No validation!
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
}
// Example of business logic that suffers from primitive obsession
class PayrollService {
// Methods with too many primitive parameters
public double calculateTax(double salary, String currency, String country, String state) {
// Complex logic using primitives instead of value objects
double taxRate = 0.0;
if ("USA".equals(country)) {
if ("CA".equals(state)) {
taxRate = 0.13;
} else if ("NY".equals(state)) {
taxRate = 0.12;
}
} else if ("Canada".equals(country)) {
taxRate = 0.15;
}
// Currency conversion logic mixed in
if ("EUR".equals(currency)) {
salary = salary * 1.1; // Simplified conversion
}
return salary * taxRate;
}
// Another method showing primitive obsession
public void sendPayslip(String email, String employeeId, double salary, String currency) {
// Validation scattered here too
if (email.contains("@") && employeeId.matches("EMP\\d{6}")) {
System.out.println("Sending payslip to " + email);
System.out.println("Employee: " + employeeId);
System.out.println("Salary: " + currency + " " + salary);
}
}
}