d9e5668cec
Fixes and improvements from exhaustive audit: Consistency fixes in SKILL.md: - 'Pipe & Filter' → 'Pipe and Filter' (one stray '&' found and corrected) - 'Singleton for factory instance' → clarified to 'register factory as singleton-scoped via DI container' (less misleading wording) - Documentation Format section updated with note that SKILL.md itself is the authoritative source for related-pattern combinations Coverage fix — Related Patterns sections: - Added '## Related Patterns' to ALL 94 pattern files (was 0/94) - Each section lists 3–6 related patterns with relationship descriptions - Covers: why they're related, when to prefer one vs the other, and which are often confused SOLID principles → Creational → Structural → Behavioral → Architectural → Concurrency → Functional → Resilience → Data Access → Messaging → Testing → Error Handling → Microservice — all 13 categories covered Code verification: - Python: 0 failures (all 85 testable blocks pass) - Go: 0 failures (all 76 testable blocks pass) - JavaScript: 0 failures (all 78 testable blocks pass) - All 239 code blocks verified correct after edits Final skill state: - 108 files, 36,524 lines across 13 reference categories - 94/94 pattern files have Related Patterns sections - 2,815-line SKILL.md with 67 decision trees, 23 scenarios, 0 broken references, 0 naming inconsistencies
7.6 KiB
7.6 KiB
Active Object Pattern
Problem
A client calls methods on an object, but those methods involve expensive or blocking operations. Executing them synchronously blocks the caller. Manually managing threads for each call is complex and error-prone.
Solution
The Active Object pattern decouples method invocation from method execution. Method calls are turned into request objects placed on a queue. A scheduler (running in its own thread) dequeues requests and executes them. Results are returned via futures/promises. The caller never blocks on execution.
When to Use
- When method calls should be non-blocking for the caller
- When execution order must be controlled (priority queues, scheduling)
- When you want to serialize access to a resource without explicit locking
- Middleware, proxy layers, or command-based architectures
When to Avoid
- Simple synchronous operations with negligible latency
- When the indirection and queuing overhead is not justified
- Real-time systems where queuing introduces unacceptable delay
Pseudocode
class ActiveObject:
queue = RequestQueue()
scheduler = Thread(run=process_loop)
function async_method(args):
future = Future()
queue.put(Request(method, args, future))
return future
function process_loop():
while running:
request = queue.get()
result = request.method(request.args)
request.future.set_result(result)
// Usage
obj = ActiveObject()
f1 = obj.async_method("compute", data)
f2 = obj.async_method("transform", data)
print(f1.get())
print(f2.get())
Python
import threading
import queue
from concurrent.futures import Future
from typing import Callable, Any
class ActiveObject:
def __init__(self, name: str):
self.name = name
self._queue: queue.Queue = queue.Queue()
self._running = True
self._thread = threading.Thread(target=self._scheduler, daemon=True)
self._thread.start()
def _scheduler(self) -> None:
"""Continuously dequeue and execute requests."""
while self._running:
try:
func, args, future = self._queue.get(timeout=0.1)
try:
result = func(*args)
future.set_result(result)
except Exception as e:
future.set_exception(e)
except queue.Empty:
continue
def invoke(self, func: Callable, *args: Any) -> Future:
"""Submit a method request; returns a Future."""
future: Future = Future()
self._queue.put((func, args, future))
return future
def shutdown(self) -> None:
self._running = False
self._thread.join()
# Domain methods (executed by scheduler thread)
def compute_sum(a: int, b: int) -> int:
import time
time.sleep(0.1) # simulate work
result = a + b
print(f"[Scheduler] compute_sum({a}, {b}) = {result}")
return result
def format_greeting(name: str) -> str:
import time
time.sleep(0.05)
result = f"Hello, {name}!"
print(f"[Scheduler] format_greeting({name}) = {result}")
return result
def main() -> None:
active = ActiveObject("Worker")
# Non-blocking invocations
f1 = active.invoke(compute_sum, 10, 20)
f2 = active.invoke(compute_sum, 3, 7)
f3 = active.invoke(format_greeting, "World")
print("[Main] Requests submitted, waiting for results...")
# Retrieve results (blocks only when result not yet ready)
print(f"[Main] Result 1: {f1.result()}")
print(f"[Main] Result 2: {f2.result()}")
print(f"[Main] Result 3: {f3.result()}")
active.shutdown()
print("[Main] Active object shut down.")
if __name__ == "__main__":
main()
Go
package main
import (
"fmt"
"time"
)
// Request holds a function to execute and a channel for the result
type Request struct {
Execute func() interface{}
Result chan interface{}
}
// ActiveObject processes requests sequentially
type ActiveObject struct {
queue chan Request
done chan struct{}
}
func NewActiveObject(bufferSize int) *ActiveObject {
ao := &ActiveObject{
queue: make(chan Request, bufferSize),
done: make(chan struct{}),
}
go ao.scheduler()
return ao
}
func (ao *ActiveObject) scheduler() {
for req := range ao.queue {
result := req.Execute()
req.Result <- result
}
close(ao.done)
}
// Invoke submits a request and returns a channel for the result
func (ao *ActiveObject) Invoke(fn func() interface{}) chan interface{} {
resultCh := make(chan interface{}, 1)
ao.queue <- Request{Execute: fn, Result: resultCh}
return resultCh
}
func (ao *ActiveObject) Shutdown() {
close(ao.queue)
<-ao.done
}
func main() {
ao := NewActiveObject(10)
// Submit requests (non-blocking)
f1 := ao.Invoke(func() interface{} {
time.Sleep(100 * time.Millisecond)
result := 10 + 20
fmt.Printf("[Scheduler] compute_sum(10, 20) = %d\n", result)
return result
})
f2 := ao.Invoke(func() interface{} {
time.Sleep(50 * time.Millisecond)
result := 3 + 7
fmt.Printf("[Scheduler] compute_sum(3, 7) = %d\n", result)
return result
})
f3 := ao.Invoke(func() interface{} {
time.Sleep(50 * time.Millisecond)
result := "Hello, World!"
fmt.Printf("[Scheduler] format_greeting = %s\n", result)
return result
})
fmt.Println("[Main] Requests submitted, waiting for results...")
fmt.Printf("[Main] Result 1: %v\n", <-f1)
fmt.Printf("[Main] Result 2: %v\n", <-f2)
fmt.Printf("[Main] Result 3: %v\n", <-f3)
ao.Shutdown()
fmt.Println("[Main] Active object shut down.")
}
JavaScript
class ActiveObject {
constructor(name) {
this.name = name;
this.queue = [];
this.processing = false;
}
invoke(fn) {
return new Promise((resolve, reject) => {
this.queue.push({ fn, resolve, reject });
this._processNext();
});
}
async _processNext() {
if (this.processing || this.queue.length === 0) return;
this.processing = true;
while (this.queue.length > 0) {
const { fn, resolve, reject } = this.queue.shift();
try {
const result = await fn();
resolve(result);
} catch (err) {
reject(err);
}
}
this.processing = false;
}
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function computeSum(a, b) {
await sleep(100);
const result = a + b;
console.log(`[Scheduler] computeSum(${a}, ${b}) = ${result}`);
return result;
}
async function formatGreeting(name) {
await sleep(50);
const result = `Hello, ${name}!`;
console.log(`[Scheduler] formatGreeting(${name}) = ${result}`);
return result;
}
async function main() {
const active = new ActiveObject("Worker");
// Submit requests (returns promises immediately)
const f1 = active.invoke(() => computeSum(10, 20));
const f2 = active.invoke(() => computeSum(3, 7));
const f3 = active.invoke(() => formatGreeting("World"));
console.log("[Main] Requests submitted, waiting for results...");
const [r1, r2, r3] = await Promise.all([f1, f2, f3]);
console.log(`[Main] Result 1: ${r1}`);
console.log(`[Main] Result 2: ${r2}`);
console.log(`[Main] Result 3: ${r3}`);
console.log("[Main] Active object shut down.");
}
main();
Related Patterns
- Future/Promise — active object method calls return futures; callers await results without blocking on execution.
- Thread Pool — the active object's scheduler runs on a thread pool worker thread.
- Command — each method invocation is turned into a command object enqueued in the active object's queue.
- Proxy — the active object proxy intercepts synchronous method calls and converts them to async requests.