Files
bstore-j/DEVELOPER_EXPERIENCE_IMPROVEMENTS.md

362 lines
9.6 KiB
Markdown

# Developer Experience Improvements for bstore-java
## Current State Analysis
### Current API Patterns
- Field access: `Product.name.set(product, "Alice")` / `Product.name.get(product, null)`
- Slot-based access: `dbo.set(nameSlot, "Alice")` / `dbo.get(nameSlot, null)`
- Positional access: `dbo.set(0, value)` / `dbo.get(0)` (already Java-like!)
- Field-based queries: `PersonDBO.AGE.gte(18)`
- Action chaining: `terminal.begin().load(PersonDBO).filterBy(...).execute()`
### Issues Identified
1. **Verbose field access**: `Product.name.set(product, "Alice")` is verbose
2. **No convenience methods**: Can't use `dbo.get(Product.NAME)` or `dbo.set(Product.NAME, "Alice")`
3. **No equals/hashCode**: Can't compare DBOs by value
4. **No cloning**: Can't easily copy DBO instances
5. **No map conversion**: Can't convert to/from `Map<String, Object>`
6. **No fluent API**: Missing `withField()` methods for method chaining
7. **No field lookup by name**: Entity doesn't have `getField(String name)` method
8. **toString() could be better**: Currently returns JSON, could have human-readable format
## Proposed Improvements
### 1. Convenience Methods for Field Access (High Impact) ✅
**Goal**: Allow `dbo.get(Product.NAME)` instead of `Product.NAME.get(dbo, null)`
**Implementation**:
```java
/**
* Get field value by Field instance.
*
* @param field Field to get value for
* @return Field value, or null if not set
*/
public Object get(Field field) {
return get(field, null);
}
/**
* Get field value by Field instance with default.
*
* @param field Field to get value for
* @param defaultValue Default value if field is null
* @return Field value, or defaultValue if not set
*/
public Object get(Field field, Object defaultValue) {
return get((Slot)field, defaultValue);
}
/**
* Set field value by Field instance.
*
* @param field Field to set value for
* @param value Value to set
* @return Self for chaining
*/
public DBO set(Field field, Object value) {
return (DBO)set((Slot)field, value);
}
```
**Benefits**:
- More intuitive: `person.get(PersonDBO.NAME)` vs `PersonDBO.NAME.get(person, null)`
- Consistent with positional access pattern
- Backward compatible (Field.get/set still work)
**Trade-offs**:
- Minimal - just convenience wrappers
### 2. Equals and HashCode (Medium Impact)
**Goal**: Value-based comparison
**Implementation**:
```java
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
DBO other = (DBO)obj;
// Compare entity types
if (type != other.type) return false;
if (type == null) return true;
// Compare by primary key if available
Field pk = type.getPk();
if (pk != null) {
Object thisPk = get(pk);
Object otherPk = other.get(pk);
if (thisPk != null && otherPk != null) {
return thisPk.equals(otherPk);
}
}
// Compare all values
if (values == null) return other.values == null;
if (other.values == null) return false;
if (values.length != other.values.length) return false;
for (int i = 0; i < values.length; i++) {
if (!java.util.Objects.equals(values[i], other.values[i])) {
return false;
}
}
return true;
}
@Override
public int hashCode() {
if (type == null) return System.identityHashCode(this);
// Hash by primary key if available
Field pk = type.getPk();
if (pk != null) {
Object pkValue = get(pk);
if (pkValue != null) {
return java.util.Objects.hash(type.getName(), pkValue);
}
}
// Hash by all values
return java.util.Objects.hash(type.getName(), java.util.Arrays.hashCode(values));
}
```
### 3. Cloning Support (Low Effort)
**Goal**: Standard Java pattern
**Implementation**:
```java
/**
* Create a shallow copy of this DBO.
*
* @return New DBO instance with copied values
*/
public DBO clone() {
DBO cloned = new DBO();
cloned.setType(type);
cloned.setTerminal(terminal);
cloned.status = status;
if (values != null) {
cloned.values = values.clone();
}
return cloned;
}
```
### 4. Map Conversion (Medium Impact)
**Goal**: Dictionary-like access
**Implementation**:
```java
import java.util.Map;
import java.util.HashMap;
/**
* Convert DBO to Map with field names as keys.
*
* @return Map with field names as keys and values as values
*/
public Map<String, Object> toMap() {
Map<String, Object> result = new HashMap<>();
if (type != null) {
FieldSlice slice = new FieldSlice(type);
while (slice.hasNext()) {
Field field = slice.next();
Object value = get(field);
if (value != null) {
result.put(field.getName(), value);
}
}
}
return result;
}
/**
* Create DBO instance from Map.
*
* @param cls DBO class
* @param data Map with field values
* @param terminal Optional terminal to attach
* @return New DBO instance
*/
public static <T extends DBO> T fromMap(Class<T> cls, Map<String, Object> data, Terminal terminal) {
try {
T instance = cls.getDeclaredConstructor().newInstance();
if (terminal != null) {
instance.setTerminal(terminal);
}
Entity entity = Entity.recall(cls);
if (entity != null && data != null) {
for (Map.Entry<String, Object> entry : data.entrySet()) {
Field field = entity.getField(entry.getKey());
if (field != null) {
instance.set(field, entry.getValue());
}
}
}
return instance;
} catch (Exception e) {
throw new RuntimeException("Failed to create DBO from map", e);
}
}
```
### 5. Fluent API (Low Effort)
**Goal**: Method chaining
**Implementation**:
```java
/**
* Set field value (fluent API).
*
* @param field Field to set
* @param value Value to set
* @return Self for chaining
*/
public DBO withField(Field field, Object value) {
set(field, value);
return this;
}
/**
* Set multiple fields from Map (fluent API).
*
* @param data Map with field values
* @return Self for chaining
*/
public DBO withFields(Map<String, Object> data) {
if (type != null && data != null) {
for (Map.Entry<String, Object> entry : data.entrySet()) {
Field field = type.getField(entry.getKey());
if (field != null) {
set(field, entry.getValue());
}
}
}
return this;
}
```
### 6. Entity Field Lookup by Name (Low Effort)
**Goal**: Find fields by name
**Implementation**:
```java
// In Entity class
/**
* Get field by name (case-insensitive).
*
* @param name Field name to find
* @return Field instance or null if not found
*/
public Field getField(String name) {
if (name == null) return null;
// Search own fields
for (Slot slot : getOwnSlots()) {
if (slot instanceof Field && slot.equals(name)) {
return (Field)slot;
}
}
// Search base entity
if (base != null) {
Field field = base.getField(name);
if (field != null) return field;
}
return null;
}
```
### 7. Better toString() (Low Effort)
**Goal**: Human-readable representation
**Implementation**:
```java
/**
* Return human-readable string representation.
*
* @return String like "PersonDBO(name=Alice, age=30)"
*/
public String toDisplayString() {
if (type == null) {
return getClass().getSimpleName() + "()";
}
StringBuilder sb = new StringBuilder(getClass().getSimpleName()).append("(");
FieldSlice slice = new FieldSlice(type);
boolean first = true;
while (slice.hasNext()) {
Field field = slice.next();
Object value = get(field);
if (value != null) {
if (!first) sb.append(", ");
sb.append(field.getName()).append("=").append(value);
first = false;
}
}
sb.append(")");
return sb.toString();
}
// Keep existing toString() for JSON representation
```
## Priority Recommendations
### Phase 1: Quick Wins (Low Effort, High Impact)
1.**Convenience methods** (`get(Field)`, `set(Field, value)`)
2.**Entity.getField(String)** - field lookup by name
3.**Fluent API** (`withField()`, `withFields()`)
### Phase 2: Standard Java Patterns (Medium Effort, High Value)
4.**Equals and hashCode** - value comparison
5.**Clone support** - object copying
6.**Map conversion** - `toMap()`, `fromMap()`
### Phase 3: Nice to Have
7.**Better toString()** - human-readable format (keep JSON as default)
## Implementation Notes
- All changes should be **backward compatible**
- Maintain existing `Field.get()`/`Field.set()` methods
- Add new methods alongside old ones
- Follow Java conventions (PEP 8 equivalent: Java Code Conventions)
- Use `@Override` where appropriate
- Add comprehensive Javadoc
## Example Usage After Improvements
```java
// Before (still works):
Product product = new Product();
Product.name.set(product, "Widget");
String name = (String)Product.name.get(product, null);
// After (new convenience methods):
Product product = new Product();
product.set(Product.NAME, "Widget"); // Convenience method
String name = (String)product.get(Product.NAME); // Convenience method
// Fluent API:
Product product = new Product()
.withField(Product.NAME, "Widget")
.withField(Product.PRICE, 19.99);
// Map conversion:
Map<String, Object> data = product.toMap();
Product product2 = DBO.fromMap(Product.class, data, terminal);
// Equality:
if (product1.equals(product2)) {
System.out.println("Same product");
}
// Cloning:
Product copy = product.clone();
```