This section provides detailed technical exploration of Codion’s core architectural components and patterns.
1. Architecture Deep Dive: Observable Pattern
1.1. Overview
The Observable pattern is the foundation of Codion’s reactive architecture. It provides a unified approach to change notification throughout the framework, from UI components to domain models. The pattern consists of four core abstractions:
-
Observer - Manages listeners and consumers for change notifications
-
Observable - Combines a value accessor with change observation
-
Value - Mutable observable wrapper for any object
-
State - Specialized boolean observable with null-to-false coercion
-
Event - Push-only notification mechanism
1.2. Core Architecture
1.2.1. Observer Interface
The Observer interface is the foundation for all change notification:
public interface Observer<T> {
// Strong references - prevent garbage collection
boolean addListener(Runnable listener);
boolean addConsumer(Consumer<? super T> consumer);
// Weak references - allow garbage collection
boolean addWeakListener(Runnable listener);
boolean addWeakConsumer(Consumer<? super T> consumer);
// Removal methods
boolean removeListener(Runnable listener);
boolean removeConsumer(Consumer<? super T> consumer);
}
Key design decisions:
-
Two notification types:
-
Runnable
listeners for simple notifications -
Consumer<T>
for data propagation
-
-
Weak reference support: Prevents memory leaks in long-lived UI components
1.2.2. Observable Interface
Observable combines value access with observation:
public interface Observable<T> extends Observer<T> {
@Nullable T get();
default T getOrThrow() {
T value = get();
if (value == null) {
throw new NoSuchElementException("No value present");
}
return value;
}
Observer<T> observer(); // For read-only access
}
This separation allows exposing read-only observables while keeping mutation control private.
1.3. Value Implementation
1.3.1. Value Interface
Value is the primary mutable observable:
public interface Value<T> extends Observable<T> {
enum Notify {
SET, // Notify on every set() call
CHANGED // Notify only when value changes
}
void set(@Nullable T value);
void clear();
void map(UnaryOperator<T> mapper);
// Linking support
void link(Value<T> originalValue); // Bidirectional
void link(Observable<T> observable); // Unidirectional
// Validation
boolean addValidator(Validator<? super T> validator);
}
1.3.2. Nullable vs Non-Null Values
Codion provides two value types:
// Nullable - can hold null
Value<String> nullable = Value.nullable();
nullable.set(null); // OK
// Non-null - uses null substitute
Value<String> nonNull = Value.nonNull("default");
nonNull.set(null); // Sets to "default"
nonNull.isNull(); // Always false
1.3.3. Value Linking
Values can be linked for automatic synchronization:
Value<Integer> primary = Value.value(10);
Value<Integer> secondary = Value.value(0);
// Bidirectional link
secondary.link(primary); // secondary becomes 10
primary.set(20); // both become 20
secondary.set(30); // both become 30
// Unidirectional link
Value<String> display = Value.value("");
Observable<String> source = getDataSource();
display.link(source); // display follows source changes
1.4. State Implementation
State is optimized for boolean values:
public interface State extends ObservableState, Value<Boolean> {
@NonNull Boolean get(); // Never null
ObservableState observable(); // Read-only view
}
1.4.1. State Negation
The not()
method creates an inverse view of a state:
State enabled = State.state(true);
ObservableState disabled = enabled.not();
enabled.get(); // true
disabled.get(); // false
enabled.set(false);
disabled.get(); // true
// Common UI patterns
State processing = State.state();
JButton button = Components.button()
.enabled(processing.not()) // Disabled while processing
.build();
// Combining with other states
State.Combination canEdit = State.and(
loggedIn,
processing.not(),
hasPermission
);
1.4.2. State Combinations
States can be combined using boolean logic:
State canSave = State.state();
State hasChanges = State.state();
State isValid = State.state();
// AND combination
State.Combination saveEnabled = State.and(canSave, hasChanges, isValid);
// OR combination
State.Combination anyProgress = State.or(loading, saving, validating);
// Dynamic combination
State.Combination dynamic = State.combination(Conjunction.AND);
dynamic.add(condition1);
dynamic.add(condition2);
dynamic.remove(condition1);
1.4.3. State Groups
State groups implement radio-button behavior:
State.Group viewMode = State.group();
State listView = State.state();
State tableView = State.state();
State treeView = State.state();
viewMode.add(listView, tableView, treeView);
tableView.set(true); // Others become false
listView.set(true); // tableView becomes false
1.5. Event Implementation
Event provides push-only notifications:
public interface Event<T> extends Runnable, Consumer<T>, Observer<T> {
void run(); // Trigger without data
void accept(@Nullable T data); // Trigger with data
Observer<T> observer(); // Read-only access
}
Usage patterns:
// Simple event
Event<Void> refreshRequested = Event.event();
refreshRequested.addListener(this::refresh);
refreshRequested.run();
// Data event
Event<String> errorOccurred = Event.event();
errorOccurred.addConsumer(this::showError);
errorOccurred.accept("Connection failed");
// Both listeners and consumers are notified
Event<Integer> progress = Event.event();
progress.addListener(() -> updateProgressBar());
progress.addConsumer(percent -> setProgress(percent));
progress.accept(75); // Both are called
1.6. Thread Safety
The observable implementations use synchronization for thread safety:
-
Listener Management: Adding/removing listeners is synchronized
-
Value Updates: Setting values is atomic
-
Notification Order: Listeners are notified in registration order
-
Exception Handling: Unhandled exceptions in listeners prevent further notifications
Example from DefaultEvent:
private void notifyListeners() {
synchronized (listeners) {
for (Runnable listener : listeners) {
listener.run(); // Exception here stops the loop
}
}
}
Important: If a listener throws an exception, subsequent listeners will not be notified. Always handle exceptions within your listeners:
event.addListener(() -> {
try {
riskyOperation();
} catch (Exception e) {
LOG.error("Error in listener", e);
}
});
1.7. Memory Management
1.7.1. Weak References
Weak listeners/consumers prevent memory leaks:
public class DetailPanel {
private final State visible = State.state();
public void attachToMaster(Observable<Entity> selection) {
// Weak reference prevents this panel from keeping
// the selection model alive if panel is discarded
selection.addWeakConsumer(this::showDetails);
}
}
1.8. Performance Characteristics
1.8.1. Notification Strategies
Choose the appropriate notification strategy:
// CHANGED: Only when value changes (default)
Value<Integer> counter = Value.builder()
.nonNull(0)
.build(); // Uses CHANGED by default
counter.set(1); // Notifies
counter.set(1); // No notification
// SET: Always notify, even if value unchanged
Value<String> status = Value.builder()
.nonNull("")
.notify(Notify.SET)
.build();
status.set("OK"); // Notifies
status.set("OK"); // Still notifies with SET
1.9. Best Practices
-
Use appropriate abstraction:
-
State
for booleans -
Value
for mutable observables -
Event
for actions -
Observable
for read-only exposure
-
-
Prefer weak references for UI components to prevent memory leaks
-
Use validators for domain constraints:
Value<Integer> age = Value.builder() .nonNull(0) .validator(a -> a >= 0 && a <= 150) .build();
-
Link values instead of manual synchronization:
// Instead of: source.addConsumer(value -> target.set(value)); // Use: target.link(source);
-
Expose read-only views:
public class Model { private final State processing = State.state(); public ObservableState processing() { return processing.observable(); } }
1.10. Integration Examples
1.10.1. UI Component Binding
// Swing component binding
JTextField textField = new JTextField();
Value<String> model = Value.value("");
// Bidirectional binding
textField.getDocument().addDocumentListener(new DocumentAdapter() {
protected void documentChanged() {
model.set(textField.getText());
}
});
model.addConsumer(textField::setText);
1.10.2. Model State Management
public class EntityEditModel {
private final State modified = State.state();
private final State valid = State.state();
private final State.Combination canSave = State.and(modified, valid);
private final Value<Entity> entity = Value.value();
public EntityEditModel() {
entity.addConsumer(e -> validateEntity());
}
public ObservableState canSave() {
return canSave;
}
}
1.10.3. Event-Driven Architecture
public class Application {
private final Event<Void> shutdownRequested = Event.event();
private final Event<Exception> errorOccurred = Event.event();
public void initialize() {
shutdownRequested.addListener(this::performShutdown);
errorOccurred.addConsumer(this::logError);
errorOccurred.addConsumer(this::notifyUser);
}
}
1.11. Summary
Codion’s Observable pattern provides:
-
Unified change notification across the framework
-
Type-safe value observation with validation
-
Memory-safe weak references for UI components
-
Thread-safe implementation for concurrent access
-
Composable state management for complex UI logic
This pattern is fundamental to Codion’s reactive architecture, enabling automatic UI updates, clean separation of concerns, and maintainable application state management.