Files
bstore-j/DEVELOPER_EXPERIENCE_IMPROVEMENTS.md

9.6 KiB

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:

/**
 * 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:

@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:

/**
 * 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:

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:

/**
 * 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:

// 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:

/**
 * 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)

  1. Equals and hashCode - value comparison
  2. Clone support - object copying
  3. Map conversion - toMap(), fromMap()

Phase 3: Nice to Have

  1. 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

// 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();