forked from jabrena/cursor-rules-java
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy path103-java-data-oriented-programming.mdc
330 lines (279 loc) · 8.89 KB
/
103-java-data-oriented-programming.mdc
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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
---
description: Extension to java rules to apply data oriented programming
globs: .java
alwaysApply: true
---
# Data Oriented Programming Pillars for Java
## 1. Separate Code from Data
- Use records for data structures
- Keep behavior in separate utility classes
- Avoid mixing state and behavior in the same class
- Use static methods for operations on data
- Design data structures to be self-contained
- Make data classes focused on representing state only
```java
// Bad: Mixing code and data
class User {
private String name;
private int age;
public void validateAge() {
if (age < 0) throw new IllegalArgumentException("Age cannot be negative");
}
}
// Good: Separate data structure and behavior
record UserData(String name, int age) {}
class UserValidator {
public static void validateAge(UserData user) {
if (user.age() < 0) throw new IllegalArgumentException("Age cannot be negative");
}
}
```
## 2. Data Should Be Immutable
- Use records whenever possible
- Make fields final
- Avoid setters
- Return defensive copies of collections
- Use immutable collections (List.of(), Set.of(), etc.)
- Create new instances instead of modifying existing ones
```java
// Bad: Mutable data
class MutableConfig {
private String host;
private int port;
public void setHost(String host) { this.host = host; }
public void setPort(int port) { this.port = port; }
}
// Good: Immutable data using record
record ServerConfig(String host, int port) {}
```
## 3. Use Pure Functions to Manipulate Data
- Functions should depend only on their input parameters
- Avoid side effects
- Make functions stateless
- Return new instances instead of modifying inputs
- Use static methods for data transformations
- Keep functions focused on a single responsibility
```java
// Bad: Impure function with side effects
class PriceCalculator {
private double taxRate;
public double calculateTotal(double price) {
return price * (1 + taxRate); // Depends on instance state
}
}
// Good: Pure function
class PriceOperations {
public static double calculateTotal(double price, double taxRate) {
return price * (1 + taxRate); // Only depends on inputs
}
public static List<Double> applyDiscount(List<Double> prices, double discount) {
return prices.stream()
.map(price -> price * (1 - discount))
.collect(Collectors.toUnmodifiableList());
}
}
```
## 4. Keep Data Flat and Denormalized
- Avoid deep nesting of data structures
- Use references (IDs) instead of nested objects
- Store data in flat collections or maps
- Keep relationships explicit through identifiers
- Design for easy querying and manipulation
- Consider performance implications of data structure
```java
// Bad: Deeply nested structure
class Department {
private List<Employee> employees;
class Employee {
private List<Project> projects;
class Project {
private List<Task> tasks;
}
}
}
// Good: Flat structure with references
record TaskData(String id, String name, String assigneeId) {}
record ProjectData(String id, String name, List<String> taskIds) {}
record EmployeeData(String id, String name, List<String> projectIds) {}
record DepartmentData(String id, String name, List<String> employeeIds) {}
// Store in flat maps
Map<String, TaskData> tasks;
Map<String, ProjectData> projects;
Map<String, EmployeeData> employees;
Map<String, DepartmentData> departments;
```
## 5. Keep Data Generic Until Specific
- Start with generic data structures
- Use Map<String, Object> for flexible data
- Convert to specific types only when needed
- Implement type-safe converters
- Validate data during conversion
- Document expected data structure
```java
// Bad: Too specific too early
record UserProfile(
String email,
String password,
String firstName,
String lastName,
LocalDate birthDate
) {}
// Good: Generic until needed
record GenericUser(Map<String, Object> attributes) {}
// Convert to specific type only when needed
class UserConverter {
public static UserProfile toUserProfile(GenericUser user) {
return new UserProfile(
(String) user.attributes().get("email"),
(String) user.attributes().get("password"),
(String) user.attributes().get("firstName"),
(String) user.attributes().get("lastName"),
(LocalDate) user.attributes().get("birthDate")
);
}
}
```
## 6. Data Integrity through Validation Functions
- Implement validation as pure functions
- Return validation results instead of throwing exceptions
- Compose multiple validations
- Use Optional for validation results
- Keep validation rules separate from data
- Make validation rules reusable
```java
// Define validation rules as pure functions
class Validators {
public static Optional<String> validateEmail(String email) {
return !email.contains("@")
? Optional.of("Invalid email format")
: Optional.empty();
}
public static Optional<String> validateAge(int age) {
return age < 0 || age > 150
? Optional.of("Age must be between 0 and 150")
: Optional.empty();
}
}
// Compose validations
record UserValidation(UserData user) {
public List<String> validate() {
List<String> errors = new ArrayList<>();
Validators.validateEmail(user.email()).ifPresent(errors::add);
Validators.validateAge(user.age()).ifPresent(errors::add);
return errors;
}
}
```
## 7. Flexible and Generic Data Access
- Design generic interfaces for data access
- Use type parameters for flexibility
- Implement thread-safe storage
- Support filtering and querying
- Make storage implementation replaceable
- Consider caching strategies
```java
// Generic data access interface
interface DataStore<T> {
Optional<T> find(String id);
List<T> findAll(Predicate<T> filter);
void store(String id, T data);
}
// Implementation using generic data structures
class InMemoryDataStore<T> implements DataStore<T> {
private final Map<String, T> storage = new ConcurrentHashMap<>();
@Override
public Optional<T> find(String id) {
return Optional.ofNullable(storage.get(id));
}
@Override
public List<T> findAll(Predicate<T> filter) {
return storage.values().stream()
.filter(filter)
.collect(Collectors.toUnmodifiableList());
}
@Override
public void store(String id, T data) {
storage.put(id, data);
}
}
```
## 8. Explicit and Traceable Data Transformation
- Make transformations visible and traceable
- Use pure functions for transformations
- Chain transformations explicitly
- Log transformation steps
- Handle errors gracefully
- Document transformation flow
```java
// Define transformations as pure functions
class UserTransformations {
public static UserDTO toDTO(UserData user) {
return new UserDTO(
user.id(),
user.name(),
user.email()
);
}
public static UserData fromDTO(UserDTO dto) {
return new UserData(
dto.id(),
dto.name(),
dto.email()
);
}
}
// Chain transformations with clear tracing
class UserProcessor {
public static UserViewModel process(UserData user) {
return Stream.of(user)
.map(UserTransformations::toDTO)
.map(UserEnricher::addMetadata)
.map(UserValidator::validate)
.map(UserViewModelMapper::toViewModel)
.peek(vm -> log.info("Transformed user: {}", vm))
.findFirst()
.orElseThrow();
}
}
```
## 9. Unidirectional Data Flow
- Define clear stages for data processing
- Make data flow explicit and visible
- Handle errors at each stage
- Validate data between stages
- Make stages composable
- Keep stages independent and testable
```java
// Define clear data flow stages
interface DataFlowStage<IN, OUT> {
OUT process(IN input);
}
// Implement stages
class ValidationStage implements DataFlowStage<UserData, ValidatedUser> {
@Override
public ValidatedUser process(UserData input) {
List<String> errors = new UserValidation(input).validate();
return new ValidatedUser(input, errors.isEmpty(), errors);
}
}
class EnrichmentStage implements DataFlowStage<ValidatedUser, EnrichedUser> {
@Override
public EnrichedUser process(ValidatedUser input) {
if (!input.isValid()) {
throw new IllegalStateException("Cannot enrich invalid user");
}
return new EnrichedUser(input.user(), fetchAdditionalData(input.user()));
}
}
// Compose flow
class UserDataFlow {
private final List<DataFlowStage<?, ?>> stages;
public UserViewModel process(UserData input) {
Object result = input;
for (DataFlowStage stage : stages) {
result = stage.process(result);
}
return (UserViewModel) result;
}
}
```