Background
In my previous blog of the Adaptive Object Model (AOM) pattern, I focused on dynamic schema evolution and metadata-driven architectures. However, there’s a complementary pattern that addresses a different but equally important challenge: how to compose behavior dynamically at runtime without modifying existing objects. I first saw this pattern in Voyager ORB’s “Dynamic Aggregation” and San Francisco Design Patterns: Blueprints for Business Software (Part-IV Dynamic Behavioral Patterns) in early 2000s, which has profound implications for building extensible systems. The facets pattern, also known as dynamic aggregation or extension objects, allows secondary objects (facets) to be attached to primary objects at runtime, effectively extending their capabilities without inheritance or modification. Unlike AOM, which focuses on schema flexibility, facets address behavioral composition – the ability to mix and match capabilities based on runtime requirements.

Facets Pattern
The facets pattern emerged from several key observations about real-world software systems:
- Interface Segregation: Not every object needs every capability all the time. A User object might need audit trail capabilities in some contexts, caching in others, and validation in yet others.
- Runtime Composition: The specific mix of capabilities often depends on runtime context – user permissions, configuration settings, or environmental factors that cannot be determined at compile time.
- Separation of Concerns: Cross-cutting concerns like logging, security, and persistence should be composable without polluting domain objects.
Voyager ORB’s implementation demonstrated these principles elegantly:
// Voyager ORB example - attaching an account facet to an employee
IEmployee employee = new Employee("joe", "234-44-2678");
IFacets facets = Facets.of(employee);
IAccount account = (IAccount) facets.of(IAccount.class);
account.deposit(2000);
The beauty of this approach is that the Employee class knows nothing about accounting capabilities, yet the object can seamlessly provide financial operations when needed.

Modern Implementations
Let’s explore how this pattern can be implemented in modern languages, taking advantage of their unique strengths while maintaining the core principles.
Rust Implementation: Type-Safe Facet Composition
Rust’s type system and trait system provide excellent foundations for type-safe facet composition:
use std::collections::HashMap;
use std::any::{Any, TypeId};
use std::sync::RwLock;
// Core facet trait that all facets must implement
pub trait Facet: Any + Send + Sync {
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
}
// Faceted object that can have facets attached
pub struct FacetedObject {
facets: RwLock<HashMap<TypeId, Box<dyn Facet>>>,
core_object: Box<dyn Any + Send + Sync>,
}
impl FacetedObject {
pub fn new<T: Any + Send + Sync>(core: T) -> Self {
Self {
facets: RwLock::new(HashMap::new()),
core_object: Box::new(core),
}
}
// Attach a facet to this object
pub fn attach_facet<F: Facet + 'static>(&self, facet: F) -> Result<(), String> {
let type_id = TypeId::of::<F>();
let mut facets = self.facets.write()
.map_err(|_| "Failed to acquire write lock")?;
if facets.contains_key(&type_id) {
return Err(format!("Facet of type {:?} already attached", type_id));
}
facets.insert(type_id, Box::new(facet));
Ok(())
}
// Execute an operation that requires a specific facet (safe callback pattern)
pub fn with_facet<F: Facet + 'static, R>(
&self,
operation: impl FnOnce(&F) -> R
) -> Result<R, String> {
let facets = self.facets.read()
.map_err(|_| "Failed to acquire read lock")?;
let type_id = TypeId::of::<F>();
if let Some(facet) = facets.get(&type_id) {
if let Some(typed_facet) = facet.as_any().downcast_ref::<F>() {
Ok(operation(typed_facet))
} else {
Err("Failed to downcast facet".to_string())
}
} else {
Err(format!("Required facet not found: {:?}", type_id))
}
}
// Execute a mutable operation on a facet
pub fn with_facet_mut<F: Facet + 'static, R>(
&self,
operation: impl FnOnce(&mut F) -> R
) -> Result<R, String> {
let mut facets = self.facets.write()
.map_err(|_| "Failed to acquire write lock")?;
let type_id = TypeId::of::<F>();
if let Some(facet) = facets.get_mut(&type_id) {
if let Some(typed_facet) = facet.as_any_mut().downcast_mut::<F>() {
Ok(operation(typed_facet))
} else {
Err("Failed to downcast facet".to_string())
}
} else {
Err(format!("Required facet not found: {:?}", type_id))
}
}
// Check if a facet is attached
pub fn has_facet<F: Facet + 'static>(&self) -> bool {
let facets = self.facets.read().unwrap();
let type_id = TypeId::of::<F>();
facets.contains_key(&type_id)
}
// Get the core object
pub fn get_core<T: 'static>(&self) -> Option<&T> {
self.core_object.downcast_ref::<T>()
}
}
// Example domain object
#[derive(Debug)]
pub struct Employee {
pub name: String,
pub id: String,
pub department: String,
}
impl Employee {
pub fn new(name: &str, id: &str, department: &str) -> Self {
Self {
name: name.to_string(),
id: id.to_string(),
department: department.to_string(),
}
}
}
// Account facet for financial operations
#[derive(Debug)]
pub struct AccountFacet {
balance: f64,
account_number: String,
}
impl AccountFacet {
pub fn new(account_number: &str) -> Self {
Self {
balance: 0.0,
account_number: account_number.to_string(),
}
}
pub fn deposit(&mut self, amount: f64) -> Result<f64, String> {
if amount <= 0.0 {
return Err("Deposit amount must be positive".to_string());
}
self.balance += amount;
Ok(self.balance)
}
pub fn withdraw(&mut self, amount: f64) -> Result<f64, String> {
if amount <= 0.0 {
return Err("Withdrawal amount must be positive".to_string());
}
if amount > self.balance {
return Err("Insufficient funds".to_string());
}
self.balance -= amount;
Ok(self.balance)
}
pub fn get_balance(&self) -> f64 {
self.balance
}
pub fn get_account_number(&self) -> &str {
&self.account_number
}
}
impl Facet for AccountFacet {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
// Audit trail facet for tracking operations
#[derive(Debug)]
pub struct AuditFacet {
entries: Vec<AuditEntry>,
}
#[derive(Debug, Clone)]
pub struct AuditEntry {
timestamp: std::time::SystemTime,
operation: String,
details: String,
}
impl AuditFacet {
pub fn new() -> Self {
Self {
entries: Vec::new(),
}
}
pub fn log_operation(&mut self, operation: &str, details: &str) {
self.entries.push(AuditEntry {
timestamp: std::time::SystemTime::now(),
operation: operation.to_string(),
details: details.to_string(),
});
}
pub fn get_audit_trail(&self) -> &[AuditEntry] {
&self.entries
}
pub fn get_recent_entries(&self, count: usize) -> &[AuditEntry] {
let start = if self.entries.len() > count {
self.entries.len() - count
} else {
0
};
&self.entries[start..]
}
}
impl Facet for AuditFacet {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
// Permission facet for access control
#[derive(Debug)]
pub struct PermissionFacet {
permissions: HashMap<String, bool>,
role: String,
}
impl PermissionFacet {
pub fn new(role: &str) -> Self {
let mut permissions = HashMap::new();
// Define role-based permissions
match role {
"admin" => {
permissions.insert("read".to_string(), true);
permissions.insert("write".to_string(), true);
permissions.insert("delete".to_string(), true);
permissions.insert("financial_operations".to_string(), true);
},
"manager" => {
permissions.insert("read".to_string(), true);
permissions.insert("write".to_string(), true);
permissions.insert("financial_operations".to_string(), true);
},
"employee" => {
permissions.insert("read".to_string(), true);
},
_ => {}
}
Self {
permissions,
role: role.to_string(),
}
}
pub fn has_permission(&self, permission: &str) -> bool {
self.permissions.get(permission).copied().unwrap_or(false)
}
pub fn grant_permission(&mut self, permission: &str) {
self.permissions.insert(permission.to_string(), true);
}
pub fn revoke_permission(&mut self, permission: &str) {
self.permissions.insert(permission.to_string(), false);
}
pub fn get_role(&self) -> &str {
&self.role
}
}
impl Facet for PermissionFacet {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
// Composite operations that work across facets
pub struct EmployeeOperations;
impl EmployeeOperations {
pub fn perform_financial_operation<F>(
employee_obj: &FacetedObject,
mut operation: F,
) -> Result<String, String>
where
F: FnMut(&mut AccountFacet) -> Result<f64, String>,
{
// Check permissions first
let has_permission = employee_obj.with_facet::<PermissionFacet, bool>(|permissions| {
permissions.has_permission("financial_operations")
}).unwrap_or(false);
if !has_permission {
return Err("Access denied: insufficient permissions for financial operations".to_string());
}
// Get employee info for logging
let employee_name = employee_obj.get_core::<Employee>()
.map(|emp| emp.name.clone())
.unwrap_or_else(|| "Unknown".to_string());
// Perform the operation
let result = employee_obj.with_facet_mut::<AccountFacet, Result<f64, String>>(|account| {
operation(account)
})?;
let balance = result?;
// Log the operation if audit facet is present
let _ = employee_obj.with_facet_mut::<AuditFacet, ()>(|audit| {
audit.log_operation("financial_operation", &format!("New balance: {}", balance));
});
Ok(format!("Financial operation completed for {}. New balance: {}", employee_name, balance))
}
pub fn get_employee_summary(employee_obj: &FacetedObject) -> String {
let mut summary = String::new();
// Core employee information
if let Some(employee) = employee_obj.get_core::<Employee>() {
summary.push_str(&format!("Employee: {} (ID: {})\n", employee.name, employee.id));
summary.push_str(&format!("Department: {}\n", employee.department));
}
// Account information if available
let account_info = employee_obj.with_facet::<AccountFacet, String>(|account| {
format!("Account: {} (Balance: ${:.2})\n",
account.get_account_number(), account.get_balance())
}).unwrap_or_else(|_| "No account information\n".to_string());
summary.push_str(&account_info);
// Permission information if available
let permission_info = employee_obj.with_facet::<PermissionFacet, String>(|permissions| {
format!("Role: {}\n", permissions.get_role())
}).unwrap_or_else(|_| "No permission information\n".to_string());
summary.push_str(&permission_info);
// Audit information if available
let audit_info = employee_obj.with_facet::<AuditFacet, String>(|audit| {
let recent_entries = audit.get_recent_entries(3);
if !recent_entries.is_empty() {
let mut info = "Recent Activity:\n".to_string();
for entry in recent_entries {
info.push_str(&format!(" - {:?}: {} ({})\n",
entry.timestamp,
entry.operation,
entry.details));
}
info
} else {
"No recent activity\n".to_string()
}
}).unwrap_or_else(|_| "No audit information\n".to_string());
summary.push_str(&audit_info);
summary
}
}
// Usage example
fn example_usage() -> Result<(), String> {
println!("=== Dynamic Facets Example ===");
// Create an employee
let employee = Employee::new("Alice Johnson", "EMP001", "Engineering");
let employee_obj = FacetedObject::new(employee);
// Attach different facets based on requirements
employee_obj.attach_facet(AccountFacet::new("ACC001"))?;
employee_obj.attach_facet(PermissionFacet::new("manager"))?;
employee_obj.attach_facet(AuditFacet::new())?;
println!("Facets attached successfully!");
// Use facets through the composite object
let summary = EmployeeOperations::get_employee_summary(&employee_obj);
println!("\nEmployee Summary:\n{}", summary);
// Attempt financial operation (deposit)
let result = EmployeeOperations::perform_financial_operation(
&employee_obj,
|account| account.deposit(1000.0)
)?;
println!("Deposit result: {}", result);
// Attempt another financial operation (withdrawal)
let result = EmployeeOperations::perform_financial_operation(
&employee_obj,
|account| account.withdraw(250.0)
)?;
println!("Withdrawal result: {}", result);
// Display final summary
let final_summary = EmployeeOperations::get_employee_summary(&employee_obj);
println!("\nFinal Employee Summary:\n{}", final_summary);
Ok(())
}
fn main() {
match example_usage() {
Ok(_) => println!("\nFacet composition example completed successfully."),
Err(e) => eprintln!("Error: {}", e),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_facet_attachment() {
let employee = Employee::new("Test User", "TEST001", "Engineering");
let employee_obj = FacetedObject::new(employee);
// Test attaching facets
assert!(employee_obj.attach_facet(AccountFacet::new("ACC001")).is_ok());
assert!(employee_obj.has_facet::<AccountFacet>());
// Test duplicate attachment fails
assert!(employee_obj.attach_facet(AccountFacet::new("ACC002")).is_err());
}
#[test]
fn test_financial_operations() {
let employee = Employee::new("Test User", "TEST001", "Engineering");
let employee_obj = FacetedObject::new(employee);
employee_obj.attach_facet(AccountFacet::new("ACC001")).unwrap();
employee_obj.attach_facet(PermissionFacet::new("manager")).unwrap();
// Test deposit
let result = employee_obj.with_facet_mut::<AccountFacet, Result<f64, String>>(|account| {
account.deposit(1000.0)
}).unwrap();
assert_eq!(result.unwrap(), 1000.0);
// Test balance check
let balance = employee_obj.with_facet::<AccountFacet, f64>(|account| {
account.get_balance()
}).unwrap();
assert_eq!(balance, 1000.0);
}
#[test]
fn test_permission_checking() {
let employee = Employee::new("Test User", "TEST001", "Engineering");
let employee_obj = FacetedObject::new(employee);
employee_obj.attach_facet(PermissionFacet::new("employee")).unwrap();
let has_financial = employee_obj.with_facet::<PermissionFacet, bool>(|permissions| {
permissions.has_permission("financial_operations")
}).unwrap();
assert_eq!(has_financial, false);
let has_read = employee_obj.with_facet::<PermissionFacet, bool>(|permissions| {
permissions.has_permission("read")
}).unwrap();
assert_eq!(has_read, true);
}
}
The Rust implementation provides several key advantages:
- Type Safety: The type system ensures that facets can only be cast to their correct types
- Memory Safety: Rust’s ownership model prevents common issues with shared mutable state
- Performance: Zero-cost abstractions mean the facet system has minimal runtime overhead
- Concurrency: Built-in thread safety through
SendandSynctraits

TypeScript Implementation: Dynamic Composition with Type Safety
TypeScript’s type system allows for sophisticated compile-time checking while maintaining JavaScript’s dynamic nature:
// Base interfaces for the facet system
// Base interfaces for the facet system
interface Facet {
readonly facetType: string;
}
interface FacetConstructor<T extends Facet> {
new(...args: any[]): T;
readonly facetType: string;
}
// Core faceted object implementation
class FacetedObject<TCore = any> {
private facets: Map<string, Facet> = new Map();
private core: TCore;
constructor(core: TCore) {
this.core = core;
}
// Attach a facet to this object
attachFacet<T extends Facet>(FacetClass: FacetConstructor<T>, ...args: any[]): T {
const facet = new FacetClass(...args);
if (this.facets.has(FacetClass.facetType)) {
throw new Error(`Facet ${FacetClass.facetType} already attached`);
}
this.facets.set(FacetClass.facetType, facet);
return facet;
}
// Get a facet by its constructor
getFacet<T extends Facet>(FacetClass: FacetConstructor<T>): T | undefined {
const facet = this.facets.get(FacetClass.facetType);
return facet as T | undefined;
}
// Check if a facet is attached
hasFacet<T extends Facet>(FacetClass: FacetConstructor<T>): boolean {
return this.facets.has(FacetClass.facetType);
}
// Remove a facet
removeFacet<T extends Facet>(FacetClass: FacetConstructor<T>): boolean {
return this.facets.delete(FacetClass.facetType);
}
// Get the core object
getCore(): TCore {
return this.core;
}
// Execute operation with facet requirement checking
withFacet<T extends Facet, R>(
FacetClass: FacetConstructor<T>,
operation: (facet: T) => R
): R {
const facet = this.getFacet(FacetClass);
if (!facet) {
throw new Error(`Required facet ${FacetClass.facetType} not found`);
}
return operation(facet);
}
// Get all attached facet types
getAttachedFacetTypes(): string[] {
return Array.from(this.facets.keys());
}
}
// Example domain objects
interface Employee {
name: string;
id: string;
department: string;
email: string;
}
class EmployeeImpl implements Employee {
constructor(
public name: string,
public id: string,
public department: string,
public email: string
) {}
}
// Account facet for financial operations
class AccountFacet implements Facet {
static readonly facetType = 'account';
readonly facetType = AccountFacet.facetType;
private balance: number = 0;
private accountNumber: string;
private transactions: Transaction[] = [];
constructor(accountNumber: string, initialBalance: number = 0) {
this.accountNumber = accountNumber;
this.balance = initialBalance;
}
deposit(amount: number): number {
if (amount <= 0) {
throw new Error('Deposit amount must be positive');
}
this.balance += amount;
this.transactions.push({
type: 'deposit',
amount,
timestamp: new Date(),
balanceAfter: this.balance
});
return this.balance;
}
withdraw(amount: number): number {
if (amount <= 0) {
throw new Error('Withdrawal amount must be positive');
}
if (amount > this.balance) {
throw new Error('Insufficient funds');
}
this.balance -= amount;
this.transactions.push({
type: 'withdrawal',
amount,
timestamp: new Date(),
balanceAfter: this.balance
});
return this.balance;
}
getBalance(): number {
return this.balance;
}
getAccountNumber(): string {
return this.accountNumber;
}
getTransactionHistory(): Transaction[] {
return [...this.transactions];
}
getRecentTransactions(count: number): Transaction[] {
return this.transactions.slice(-count);
}
}
interface Transaction {
type: 'deposit' | 'withdrawal';
amount: number;
timestamp: Date;
balanceAfter: number;
}
// Notification facet for alerting
class NotificationFacet implements Facet {
static readonly facetType = 'notification';
readonly facetType = NotificationFacet.facetType;
private subscribers: Map<string, NotificationHandler[]> = new Map();
subscribe(eventType: string, handler: NotificationHandler): void {
if (!this.subscribers.has(eventType)) {
this.subscribers.set(eventType, []);
}
this.subscribers.get(eventType)!.push(handler);
}
unsubscribe(eventType: string, handler: NotificationHandler): boolean {
const handlers = this.subscribers.get(eventType);
if (!handlers) return false;
const index = handlers.indexOf(handler);
if (index !== -1) {
handlers.splice(index, 1);
return true;
}
return false;
}
notify(eventType: string, data: any): void {
const handlers = this.subscribers.get(eventType) || [];
handlers.forEach(handler => {
try {
handler(eventType, data);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.error(`Notification handler error for ${eventType}:`, errorMessage);
}
});
}
getSubscriberCount(eventType: string): number {
return this.subscribers.get(eventType)?.length || 0;
}
}
type NotificationHandler = (eventType: string, data: any) => void;
// Cache facet for performance optimization
class CacheFacet implements Facet {
static readonly facetType = 'cache';
readonly facetType = CacheFacet.facetType;
private cache: Map<string, CacheEntry> = new Map();
private maxSize: number;
private defaultTTL: number;
constructor(maxSize: number = 100, defaultTTL: number = 300000) { // 5 minutes default
this.maxSize = maxSize;
this.defaultTTL = defaultTTL;
}
set<T>(key: string, value: T, ttl?: number): void {
// Remove oldest entries if cache is full
if (this.cache.size >= this.maxSize) {
const oldestKey = this.cache.keys().next().value;
if (oldestKey !== undefined) {
this.cache.delete(oldestKey);
}
}
this.cache.set(key, {
value,
timestamp: Date.now(),
ttl: ttl || this.defaultTTL
});
}
get<T>(key: string): T | undefined {
const entry = this.cache.get(key);
if (!entry) return undefined;
// Check if entry has expired
if (Date.now() - entry.timestamp > entry.ttl) {
this.cache.delete(key);
return undefined;
}
return entry.value as T;
}
has(key: string): boolean {
const entry = this.cache.get(key);
if (!entry) return false;
// Check if entry has expired
if (Date.now() - entry.timestamp > entry.ttl) {
this.cache.delete(key);
return false;
}
return true;
}
invalidate(key: string): boolean {
return this.cache.delete(key);
}
clear(): void {
this.cache.clear();
}
getStats(): CacheStats {
return {
size: this.cache.size,
maxSize: this.maxSize,
hitRate: 0 // Would need to track hits/misses for real implementation
};
}
}
interface CacheEntry {
value: any;
timestamp: number;
ttl: number;
}
interface CacheStats {
size: number;
maxSize: number;
hitRate: number;
}
// Permission facet with role-based access control
class PermissionFacet implements Facet {
static readonly facetType = 'permission';
readonly facetType = PermissionFacet.facetType;
private permissions: Set<string> = new Set();
private role: string;
constructor(role: string) {
this.role = role;
this.initializeRolePermissions(role);
}
private initializeRolePermissions(role: string): void {
const rolePermissions: Record<string, string[]> = {
'admin': ['read', 'write', 'delete', 'financial', 'admin'],
'manager': ['read', 'write', 'financial', 'manage_team'],
'employee': ['read', 'view_profile'],
'guest': ['read']
};
const perms = rolePermissions[role] || [];
perms.forEach(perm => this.permissions.add(perm));
}
hasPermission(permission: string): boolean {
return this.permissions.has(permission);
}
grantPermission(permission: string): void {
this.permissions.add(permission);
}
revokePermission(permission: string): void {
this.permissions.delete(permission);
}
getPermissions(): string[] {
return Array.from(this.permissions);
}
getRole(): string {
return this.role;
}
requirePermission(permission: string): void {
if (!this.hasPermission(permission)) {
throw new Error(`Access denied: missing permission '${permission}'`);
}
}
}
// Composite operations using multiple facets
class EmployeeService {
static performSecureFinancialOperation(
employeeObj: FacetedObject<Employee>,
operation: (account: AccountFacet) => number,
operationType: string
): number {
// Check permissions
const permissions = employeeObj.getFacet(PermissionFacet);
if (permissions) {
permissions.requirePermission('financial');
}
// Perform operation
const result = employeeObj.withFacet(AccountFacet, operation);
// Send notification if facet is available
const notifications = employeeObj.getFacet(NotificationFacet);
if (notifications) {
notifications.notify('financial_operation', {
employee: employeeObj.getCore().name,
operation: operationType,
timestamp: new Date()
});
}
// Invalidate related cache entries
const cache = employeeObj.getFacet(CacheFacet);
if (cache) {
cache.invalidate(`balance_${employeeObj.getCore().id}`);
cache.invalidate(`transactions_${employeeObj.getCore().id}`);
}
return result;
}
static getEmployeeSummary(employeeObj: FacetedObject<Employee>): string {
const employee = employeeObj.getCore();
const facetTypes = employeeObj.getAttachedFacetTypes();
let summary = `Employee: ${employee.name} (${employee.id})\n`;
summary += `Department: ${employee.department}\n`;
summary += `Email: ${employee.email}\n`;
summary += `Active Facets: ${facetTypes.join(', ')}\n`;
// Add account information if available
const account = employeeObj.getFacet(AccountFacet);
if (account) {
summary += `Account: ${account.getAccountNumber()} (Balance: $${account.getBalance().toFixed(2)})\n`;
const recentTransactions = account.getRecentTransactions(3);
if (recentTransactions.length > 0) {
summary += 'Recent Transactions:\n';
recentTransactions.forEach(tx => {
summary += ` ${tx.type}: $${tx.amount.toFixed(2)} on ${tx.timestamp.toLocaleString()}\n`;
});
}
}
// Add permission information if available
const permissions = employeeObj.getFacet(PermissionFacet);
if (permissions) {
summary += `Role: ${permissions.getRole()}\n`;
summary += `Permissions: ${permissions.getPermissions().join(', ')}\n`;
}
// Add cache stats if available
const cache = employeeObj.getFacet(CacheFacet);
if (cache) {
const stats = cache.getStats();
summary += `Cache: ${stats.size}/${stats.maxSize} entries\n`;
}
return summary;
}
static configureEmployeeCapabilities(
employeeObj: FacetedObject<Employee>,
config: EmployeeConfig
): void {
// Attach facets based on configuration
if (config.hasAccount) {
employeeObj.attachFacet(AccountFacet, config.accountNumber, config.initialBalance);
}
if (config.role) {
employeeObj.attachFacet(PermissionFacet, config.role);
}
if (config.enableNotifications) {
const notifications = employeeObj.attachFacet(NotificationFacet);
// Set up default notification handlers
notifications.subscribe('financial_operation', (eventType, data) => {
console.log(`Financial operation performed: ${JSON.stringify(data)}`);
});
}
if (config.enableCaching) {
employeeObj.attachFacet(CacheFacet, config.cacheSize, config.cacheTTL);
}
}
}
interface EmployeeConfig {
hasAccount?: boolean;
accountNumber?: string;
initialBalance?: number;
role?: string;
enableNotifications?: boolean;
enableCaching?: boolean;
cacheSize?: number;
cacheTTL?: number;
}
// Usage example
function demonstrateFacetComposition(): void {
console.log('=== Dynamic Facet Composition Demo ===');
// Create an employee
const employee = new EmployeeImpl('Bob Smith', 'EMP002', 'Finance', 'bob.smith@company.com');
const employeeObj = new FacetedObject(employee);
// Configure capabilities based on requirements
EmployeeService.configureEmployeeCapabilities(employeeObj, {
hasAccount: true,
accountNumber: 'ACC002',
initialBalance: 500,
role: 'manager',
enableNotifications: true,
enableCaching: true,
cacheSize: 50,
cacheTTL: 600000 // 10 minutes
});
// Display initial summary
console.log('\nInitial Employee Summary:');
console.log(EmployeeService.getEmployeeSummary(employeeObj));
// Perform financial operations
try {
const newBalance = EmployeeService.performSecureFinancialOperation(
employeeObj,
(account) => account.deposit(1000),
'deposit'
);
console.log(`Deposit successful. New balance: $${newBalance.toFixed(2)}`);
const finalBalance = EmployeeService.performSecureFinancialOperation(
employeeObj,
(account) => account.withdraw(200),
'withdrawal'
);
console.log(`Withdrawal successful. Final balance: $${finalBalance.toFixed(2)}`);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
console.error('Operation failed:', errorMessage);
}
// Display final summary
console.log('\nFinal Employee Summary:');
console.log(EmployeeService.getEmployeeSummary(employeeObj));
}
// Run the demonstration
demonstrateFacetComposition();
The TypeScript implementation provides:
- Type Safety: Compile-time type checking for facet operations
- IntelliSense Support: Rich IDE support with autocompletion and error detection
- Interface Segregation: Clean separation between different capabilities
- Dynamic Composition: Runtime attachment and detachment of behaviors

Ruby Implementation: Metaprogramming-Powered Facets
Ruby’s metaprogramming capabilities make facet implementation particularly elegant:
require 'date'
require 'set'
require 'json'
# Core facet module that all facets include
module Facet
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def facet_type
@facet_type ||= name.downcase.gsub(/facet$/, '')
end
def facet_type=(type)
@facet_type = type
end
end
def facet_type
self.class.facet_type
end
end
# Main faceted object implementation
class FacetedObject
def initialize(core_object)
@core_object = core_object
@facets = {}
@method_cache = {}
# Enable method delegation
extend_with_facet_methods
end
def attach_facet(facet_instance)
facet_type = facet_instance.facet_type
if @facets.key?(facet_type)
raise ArgumentError, "Facet '#{facet_type}' already attached"
end
@facets[facet_type] = facet_instance
# Add facet methods to this instance
add_facet_methods(facet_instance)
# Call initialization hook if facet defines it
facet_instance.on_attached(self) if facet_instance.respond_to?(:on_attached)
facet_instance
end
def detach_facet(facet_type_or_class)
facet_type = case facet_type_or_class
when String
facet_type_or_class
when Class
facet_type_or_class.facet_type
else
facet_type_or_class.facet_type
end
facet = @facets.delete(facet_type)
if facet
# Remove facet methods
remove_facet_methods(facet)
# Call cleanup hook if facet defines it
facet.on_detached(self) if facet.respond_to?(:on_detached)
end
facet
end
def get_facet(facet_type_or_class)
facet_type = case facet_type_or_class
when String
facet_type_or_class
when Class
facet_type_or_class.facet_type
else
facet_type_or_class.facet_type
end
@facets[facet_type]
end
def has_facet?(facet_type_or_class)
!get_facet(facet_type_or_class).nil?
end
def facet_types
@facets.keys
end
def core_object
@core_object
end
def with_facet(facet_type_or_class)
facet = get_facet(facet_type_or_class)
raise ArgumentError, "Facet not found: #{facet_type_or_class}" unless facet
yield(facet)
end
# Require specific facets for an operation
def requires_facets(*facet_types, &block)
missing_facets = facet_types.select { |type| !has_facet?(type) }
unless missing_facets.empty?
raise ArgumentError, "Missing required facets: #{missing_facets.join(', ')}"
end
block.call(self) if block_given?
end
private
def extend_with_facet_methods
# Add method_missing to handle facet method calls
singleton_class.class_eval do
define_method :method_missing do |method_name, *args, &block|
# Try to find the method in attached facets
@facets.values.each do |facet|
if facet.respond_to?(method_name)
return facet.send(method_name, *args, &block)
end
end
# Try the core object
if @core_object.respond_to?(method_name)
return @core_object.send(method_name, *args, &block)
end
super(method_name, *args, &block)
end
define_method :respond_to_missing? do |method_name, include_private = false|
@facets.values.any? { |facet| facet.respond_to?(method_name, include_private) } ||
@core_object.respond_to?(method_name, include_private) ||
super(method_name, include_private)
end
end
end
def add_facet_methods(facet)
facet.public_methods(false).each do |method_name|
next if method_name == :facet_type
# Create a delegating method for each public method of the facet
singleton_class.class_eval do
define_method("#{facet.facet_type}_#{method_name}") do |*args, &block|
facet.send(method_name, *args, &block)
end
end
end
end
def remove_facet_methods(facet)
facet.public_methods(false).each do |method_name|
method_to_remove = "#{facet.facet_type}_#{method_name}"
if respond_to?(method_to_remove)
singleton_class.class_eval do
remove_method(method_to_remove) if method_defined?(method_to_remove)
end
end
end
end
end
# Example domain class
class Employee
attr_accessor :name, :id, :department, :email, :hire_date
def initialize(name, id, department, email, hire_date = Date.today)
@name = name
@id = id
@department = department
@email = email
@hire_date = hire_date
end
def years_of_service
((Date.today - @hire_date) / 365.25).to_i
end
def to_h
{
name: @name,
id: @id,
department: @department,
email: @email,
hire_date: @hire_date,
years_of_service: years_of_service
}
end
end
# Account facet for financial operations
class AccountFacet
include Facet
attr_reader :account_number, :balance
def initialize(account_number, initial_balance = 0)
@account_number = account_number
@balance = initial_balance.to_f
@transactions = []
end
def deposit(amount)
raise ArgumentError, "Amount must be positive" unless amount > 0
@balance += amount
log_transaction('deposit', amount)
@balance
end
def withdraw(amount)
raise ArgumentError, "Amount must be positive" unless amount > 0
raise ArgumentError, "Insufficient funds" if amount > @balance
@balance -= amount
log_transaction('withdrawal', amount)
@balance
end
def transfer_to(target_account_number, amount)
raise ArgumentError, "Cannot transfer to same account" if target_account_number == @account_number
withdraw(amount)
log_transaction('transfer_out', amount, target_account_number)
amount
end
def receive_transfer(from_account_number, amount)
deposit(amount)
log_transaction('transfer_in', amount, from_account_number)
@balance
end
def transaction_history(limit = nil)
limit ? @transactions.last(limit) : @transactions.dup
end
def monthly_summary(year, month)
start_date = Date.new(year, month, 1)
end_date = start_date.next_month - 1
monthly_transactions = @transactions.select do |tx|
tx[:timestamp].to_date.between?(start_date, end_date)
end
{
period: "#{year}-#{month.to_s.rjust(2, '0')}",
transactions: monthly_transactions,
total_deposits: monthly_transactions.select { |tx| tx[:type] == 'deposit' }.sum { |tx| tx[:amount] },
total_withdrawals: monthly_transactions.select { |tx| tx[:type] == 'withdrawal' }.sum { |tx| tx[:amount] }
}
end
private
def log_transaction(type, amount, reference = nil)
@transactions << {
type: type,
amount: amount,
balance_after: @balance,
timestamp: Time.now,
reference: reference
}
end
end
# Performance tracking facet
class PerformanceFacet
include Facet
def initialize
@metrics = {}
@goals = {}
@reviews = []
end
def set_metric(name, value, period = Date.today)
@metrics[name] ||= []
@metrics[name] << { value: value, period: period, timestamp: Time.now }
end
def get_metric(name, period = nil)
return nil unless @metrics[name]
if period
@metrics[name].find { |m| m[:period] == period }&.fetch(:value)
else
@metrics[name].last&.fetch(:value)
end
end
def set_goal(name, target_value, deadline)
@goals[name] = { target: target_value, deadline: deadline, set_on: Date.today }
end
def goal_progress(name)
goal = @goals[name]
return nil unless goal
current_value = get_metric(name)
return nil unless current_value
progress = (current_value.to_f / goal[:target]) * 100
{
goal: goal,
current_value: current_value,
progress_percentage: progress.round(2),
days_remaining: (goal[:deadline] - Date.today).to_i
}
end
def add_review(rating, comments, reviewer, review_date = Date.today)
@reviews << {
rating: rating,
comments: comments,
reviewer: reviewer,
review_date: review_date,
timestamp: Time.now
}
end
def average_rating(last_n_reviews = nil)
reviews_to_consider = last_n_reviews ? @reviews.last(last_n_reviews) : @reviews
return 0 if reviews_to_consider.empty?
total = reviews_to_consider.sum { |review| review[:rating] }
(total.to_f / reviews_to_consider.size).round(2)
end
def performance_summary
{
metrics: @metrics.transform_values { |values| values.last },
goals: @goals.transform_values { |goal| goal_progress(@goals.key(goal)) },
recent_reviews: @reviews.last(3),
average_rating: average_rating,
total_reviews: @reviews.size
}
end
end
# Security facet for access control and audit
class SecurityFacet
include Facet
def initialize(security_level = 'basic')
@security_level = security_level
@access_log = []
@failed_attempts = []
@permissions = Set.new
@active_sessions = {}
setup_default_permissions
end
def authenticate(credentials)
# Simulate authentication
success = credentials[:password] == 'secret123'
log_access_attempt(credentials[:user_id], success)
if success
session_id = generate_session_id
@active_sessions[session_id] = {
user_id: credentials[:user_id],
start_time: Time.now,
last_activity: Time.now
}
session_id
else
nil
end
end
def validate_session(session_id)
session = @active_sessions[session_id]
return false unless session
# Check session timeout (30 minutes)
if Time.now - session[:last_activity] > 1800
@active_sessions.delete(session_id)
return false
end
session[:last_activity] = Time.now
true
end
def logout(session_id)
@active_sessions.delete(session_id)
end
def grant_permission(permission)
@permissions.add(permission)
end
def revoke_permission(permission)
@permissions.delete(permission)
end
def has_permission?(permission)
@permissions.include?(permission) || @permissions.include?('admin')
end
def require_permission(permission)
unless has_permission?(permission)
raise SecurityError, "Access denied: missing permission '#{permission}'"
end
end
def security_report
{
security_level: @security_level,
permissions: @permissions.to_a,
active_sessions: @active_sessions.size,
recent_access_attempts: @access_log.last(10),
failed_attempts_today: failed_attempts_today.size,
total_access_attempts: @access_log.size
}
end
private
def setup_default_permissions
case @security_level
when 'admin'
@permissions.merge(['read', 'write', 'delete', 'admin', 'financial'])
when 'manager'
@permissions.merge(['read', 'write', 'financial'])
when 'employee'
@permissions.merge(['read'])
end
end
def log_access_attempt(user_id, success)
attempt = {
user_id: user_id,
success: success,
timestamp: Time.now,
ip_address: '127.0.0.1' # Would be actual IP in real implementation
}
@access_log << attempt
@failed_attempts << attempt unless success
end
def failed_attempts_today
today = Date.today
@failed_attempts.select { |attempt| attempt[:timestamp].to_date == today }
end
def generate_session_id
"session_#{Time.now.to_i}_#{rand(10000)}"
end
end
# Notification facet for messaging and alerts
class NotificationFacet
include Facet
def initialize
@subscribers = Hash.new { |hash, key| hash[key] = [] }
@message_history = []
@preferences = {
email: true,
sms: false,
push: true,
frequency: 'immediate'
}
end
def subscribe(event_type, &handler)
@subscribers[event_type] << handler
end
def unsubscribe(event_type, handler)
@subscribers[event_type].delete(handler)
end
def notify(event_type, data = {})
timestamp = Time.now
message = {
event_type: event_type,
data: data,
timestamp: timestamp
}
@message_history << message
# Deliver to subscribers
@subscribers[event_type].each do |handler|
begin
handler.call(message)
rescue => e
puts "Notification handler error: #{e.message}"
end
end
# Simulate different delivery channels based on preferences
deliver_message(message) if should_deliver?(event_type)
end
def set_preference(channel, enabled)
@preferences[channel] = enabled
end
def set_frequency(frequency)
raise ArgumentError, "Invalid frequency" unless %w[immediate hourly daily].include?(frequency)
@preferences[:frequency] = frequency
end
def message_history(limit = nil)
limit ? @message_history.last(limit) : @message_history.dup
end
def unread_count
# In a real implementation, this would track read status
@message_history.count { |msg| msg[:timestamp] > Time.now - 3600 } # Last hour
end
private
def should_deliver?(event_type)
# Simple delivery logic based on preferences
case @preferences[:frequency]
when 'immediate'
true
when 'hourly'
@message_history.select { |msg| msg[:timestamp] > Time.now - 3600 }.size <= 1
when 'daily'
@message_history.select { |msg| msg[:timestamp] > Time.now - 86400 }.size <= 1
else
true
end
end
def deliver_message(message)
puts "? Email: #{message[:event_type]} - #{message[:data]}" if @preferences[:email]
puts "? Push: #{message[:event_type]} - #{message[:data]}" if @preferences[:push]
puts "? SMS: #{message[:event_type]} - #{message[:data]}" if @preferences[:sms]
end
end
# Service class for coordinated operations
class EmployeeService
def self.create_employee(name, id, department, email, capabilities = {})
employee = Employee.new(name, id, department, email)
faceted_employee = FacetedObject.new(employee)
# Attach facets based on capabilities
if capabilities[:account]
account_facet = AccountFacet.new(capabilities[:account][:number], capabilities[:account][:balance])
faceted_employee.attach_facet(account_facet)
end
if capabilities[:security]
security_facet = SecurityFacet.new(capabilities[:security][:level])
capabilities[:security][:permissions]&.each { |perm| security_facet.grant_permission(perm) }
faceted_employee.attach_facet(security_facet)
end
if capabilities[:performance_tracking]
faceted_employee.attach_facet(PerformanceFacet.new)
end
if capabilities[:notifications]
notification_facet = NotificationFacet.new
# Set up default notification handlers
notification_facet.subscribe('financial_transaction') do |message|
puts "? Financial Alert: #{message[:data][:type]} of $#{message[:data][:amount]}"
end
notification_facet.subscribe('performance_update') do |message|
puts "? Performance Update: #{message[:data][:metric]} = #{message[:data][:value]}"
end
faceted_employee.attach_facet(notification_facet)
end
faceted_employee
end
def self.perform_secure_transaction(employee_obj, transaction_type, amount)
employee_obj.requires_facets('security', 'account') do |obj|
# Authenticate and check permissions
security = obj.get_facet('security')
security.require_permission('financial')
# Perform transaction
account = obj.get_facet('account')
result = case transaction_type
when 'deposit'
account.deposit(amount)
when 'withdraw'
account.withdraw(amount)
else
raise ArgumentError, "Unknown transaction type: #{transaction_type}"
end
# Send notification if available
if obj.has_facet?('notification')
notification = obj.get_facet('notification')
notification.notify('financial_transaction', {
type: transaction_type,
amount: amount,
new_balance: result,
employee: obj.core_object.name
})
end
result
end
end
def self.update_performance(employee_obj, metric_name, value)
employee_obj.with_facet('performance') do |performance|
performance.set_metric(metric_name, value)
# Notify if notification facet is available
if employee_obj.has_facet?('notification')
notification = employee_obj.get_facet('notification')
notification.notify('performance_update', {
metric: metric_name,
value: value,
employee: employee_obj.core_object.name
})
end
end
end
def self.comprehensive_report(employee_obj)
employee = employee_obj.core_object
report = {
employee_info: employee.to_h,
attached_facets: employee_obj.facet_types,
timestamp: Time.now
}
# Add facet-specific information
if employee_obj.has_facet?('account')
account = employee_obj.get_facet('account')
report[:financial] = {
account_number: account.account_number,
balance: account.balance,
recent_transactions: account.transaction_history(5)
}
end
if employee_obj.has_facet?('performance')
performance = employee_obj.get_facet('performance')
report[:performance] = performance.performance_summary
end
if employee_obj.has_facet?('security')
security = employee_obj.get_facet('security')
report[:security] = security.security_report
end
if employee_obj.has_facet?('notification')
notification = employee_obj.get_facet('notification')
report[:notifications] = {
unread_count: notification.unread_count,
recent_messages: notification.message_history(3)
}
end
report
end
end
# Usage demonstration
def demonstrate_facet_system
puts "=== Dynamic Facet Composition Demo ==="
# Create employee with various capabilities
employee_obj = EmployeeService.create_employee(
'Sarah Connor', 'EMP003', 'Engineering', 'sarah.connor@company.com',
{
account: { number: 'ACC003', balance: 1000 },
security: { level: 'manager', permissions: ['read', 'write', 'financial'] },
performance_tracking: true,
notifications: true
}
)
puts "\n--- Initial Employee State ---"
puts "Attached facets: #{employee_obj.facet_types.join(', ')}"
# Demonstrate financial operations
puts "\n--- Financial Operations ---"
begin
# First authenticate (in a real system)
security = employee_obj.get_facet('security')
session_id = security.authenticate(user_id: 'sarah', password: 'secret123')
puts "Authentication successful: #{session_id}"
# Perform transactions
new_balance = EmployeeService.perform_secure_transaction(employee_obj, 'deposit', 500)
puts "Deposit completed. New balance: $#{new_balance}"
new_balance = EmployeeService.perform_secure_transaction(employee_obj, 'withdraw', 200)
puts "Withdrawal completed. New balance: $#{new_balance}"
rescue => e
puts "Transaction failed: #{e.message}"
end
# Demonstrate performance tracking
puts "\n--- Performance Tracking ---"
EmployeeService.update_performance(employee_obj, 'projects_completed', 5)
EmployeeService.update_performance(employee_obj, 'customer_satisfaction', 4.5)
performance = employee_obj.get_facet('performance')
performance.set_goal('projects_completed', 10, Date.today + 90)
puts "Goal progress: #{performance.goal_progress('projects_completed')}"
# Generate comprehensive report
puts "\n--- Comprehensive Employee Report ---"
report = EmployeeService.comprehensive_report(employee_obj)
puts JSON.pretty_generate(report)
# Demonstrate dynamic facet management
puts "\n--- Dynamic Facet Management ---"
puts "Before detachment: #{employee_obj.facet_types.join(', ')}"
# Detach performance facet
employee_obj.detach_facet('performance')
puts "After detaching performance: #{employee_obj.facet_types.join(', ')}"
# Try to use detached facet (should fail gracefully)
begin
EmployeeService.update_performance(employee_obj, 'test_metric', 1)
rescue => e
puts "Expected error when using detached facet: #{e.message}"
end
end
# Run the demonstration
demonstrate_facet_system
The Ruby implementation showcases:
- Metaprogramming Power: Dynamic method addition and removal using Ruby’s metaprogramming capabilities
- Elegant Syntax: Clean, readable code that expresses intent clearly
- Flexible Composition: Easy attachment and detachment of facets at runtime
- Duck Typing: Natural method delegation without complex type hierarchies
Real-World Applications
The facets pattern proves particularly valuable in several domains:
Enterprise Software Integration
Modern enterprise systems often need to integrate with multiple external services. Facets allow core business objects to gain integration capabilities dynamically:
// Core customer object
const customer = new Customer('ABC Corp', 'enterprise');
const customerObj = new FacetedObject(customer);
// Attach integration facets based on configuration
if (config.salesforce.enabled) {
customerObj.attachFacet(SalesforceFacet, config.salesforce.credentials);
}
if (config.stripe.enabled) {
customerObj.attachFacet(PaymentFacet, config.stripe.apiKey);
}
if (config.analytics.enabled) {
customerObj.attachFacet(AnalyticsFacet, config.analytics.trackingId);
}
Multi-Tenant SaaS Applications
Different tenants often require different feature sets. Facets enable feature composition based on subscription levels:
// Configure tenant capabilities based on plan
match subscription_plan {
Plan::Basic => {
tenant_obj.attach_facet(BasicAnalyticsFacet::new())?;
},
Plan::Professional => {
tenant_obj.attach_facet(AdvancedAnalyticsFacet::new())?;
tenant_obj.attach_facet(IntegrationFacet::new())?;
},
Plan::Enterprise => {
tenant_obj.attach_facet(AdvancedAnalyticsFacet::new())?;
tenant_obj.attach_facet(IntegrationFacet::new())?;
tenant_obj.attach_facet(WhiteLabelFacet::new())?;
tenant_obj.attach_facet(ApiAccessFacet::new())?;
}
}
IoT Device Management
IoT devices often have optional capabilities that depend on hardware configuration or runtime conditions:
# Device base configuration
device_obj = FacetedObject.new(IoTDevice.new(device_id, device_type))
# Attach facets based on detected capabilities
if device.has_sensor?('temperature')
device_obj.attach_facet(TemperatureFacet.new)
end
if device.has_connectivity?('wifi')
device_obj.attach_facet(WiFiFacet.new)
end
if device.battery_powered?
device_obj.attach_facet(PowerManagementFacet.new)
end

Performance Considerations
While facets provide tremendous flexibility, they come with performance trade-offs that must be carefully managed:
Method Resolution Overhead
Dynamic method resolution can introduce latency. Caching strategies help mitigate this:
class OptimizedFacetedObject<TCore> extends FacetedObject<TCore> {
private methodCache: Map<string, Facet> = new Map();
getFacetForMethod(methodName: string): Facet | undefined {
// Check cache first
if (this.methodCache.has(methodName)) {
return this.methodCache.get(methodName);
}
// Search facets for method
for (const facet of this.facets.values()) {
if (typeof (facet as any)[methodName] === 'function') {
this.methodCache.set(methodName, facet);
return facet;
}
}
return undefined;
}
}
Memory Management
Facets can create reference cycles. Proper cleanup is essential:
impl Drop for FacetedObject {
fn drop(&mut self) {
// Clean up facet references
for (_, facet) in self.facets.drain() {
// Perform any necessary cleanup
// Call facet-specific cleanup if implemented
}
}
}
Serialization Challenges
Faceted objects require special handling for persistence:
class FacetedObject
def to_serializable
{
core_object: @core_object,
facets: @facets.transform_values { |facet| serialize_facet(facet) },
facet_types: @facets.keys
}
end
def self.from_serializable(data)
obj = new(data[:core_object])
data[:facets].each do |type, facet_data|
facet_class = Object.const_get("#{type.camelize}Facet")
facet = facet_class.deserialize(facet_data)
obj.attach_facet(facet)
end
obj
end
private
def serialize_facet(facet)
if facet.respond_to?(:serialize)
facet.serialize
else
# Default serialization
facet.instance_variables.each_with_object({}) do |var, hash|
hash[var] = facet.instance_variable_get(var)
end
end
end
end
Architecture Patterns and Best Practices
Facet Discovery and Registration
Large systems benefit from automatic facet discovery:
class FacetRegistry {
private static facetClasses: Map<string, FacetConstructor<any>> = new Map();
static register<T extends Facet>(facetClass: FacetConstructor<T>): void {
this.facetClasses.set(facetClass.facetType, facetClass);
}
static createFacet<T extends Facet>(
facetType: string,
...args: any[]
): T | undefined {
const FacetClass = this.facetClasses.get(facetType);
return FacetClass ? new FacetClass(...args) : undefined;
}
static getAvailableFacets(): string[] {
return Array.from(this.facetClasses.keys());
}
}
// Automatic registration
@RegisterFacet
class EmailFacet implements Facet {
static readonly facetType = 'email';
// ...
}
Configuration-Driven Composition
Enable declarative facet composition through configuration:
# facet-config.yml
employee_types:
manager:
facets:
- type: account
config:
initial_balance: 1000
- type: permission
config:
role: manager
- type: notification
config:
channels: [email, push]
admin:
inherits: manager
facets:
- type: audit
config:
level: detailed
- type: permission
config:
role: admin
pub struct FacetComposer {
config: HashMap<String, EmployeeTypeConfig>,
}
impl FacetComposer {
pub fn compose_employee(&self, employee_type: &str, employee: Employee) -> Result<FacetedObject, String> {
let config = self.config.get(employee_type)
.ok_or_else(|| format!("Unknown employee type: {}", employee_type))?;
let mut employee_obj = FacetedObject::new(employee);
for facet_config in &config.facets {
let facet = self.create_facet(&facet_config.facet_type, &facet_config.config)?;
employee_obj.attach_facet(facet)?;
}
Ok(employee_obj)
}
}
Testing Strategies
Faceted objects require comprehensive testing approaches:
RSpec.describe FacetedObject do
let(:employee) { Employee.new('Test User', 'TEST001', 'Engineering', 'test@example.com') }
let(:employee_obj) { FacetedObject.new(employee) }
describe 'facet composition' do
it 'allows dynamic attachment of facets' do
account_facet = AccountFacet.new('ACC001', 1000)
employee_obj.attach_facet(account_facet)
expect(employee_obj.has_facet?('account')).to be true
expect(employee_obj.balance).to eq 1000
end
it 'prevents duplicate facet attachment' do
employee_obj.attach_facet(AccountFacet.new('ACC001'))
expect {
employee_obj.attach_facet(AccountFacet.new('ACC002'))
}.to raise_error(ArgumentError, /already attached/)
end
end
describe 'cross-facet operations' do
before do
employee_obj.attach_facet(AccountFacet.new('ACC001', 1000))
employee_obj.attach_facet(SecurityFacet.new('manager'))
employee_obj.attach_facet(NotificationFacet.new)
end
it 'coordinates operations across multiple facets' do
expect {
EmployeeService.perform_secure_transaction(employee_obj, 'withdraw', 100)
}.to change { employee_obj.balance }.by(-100)
.and output(/Financial Alert/).to_stdout
end
end
end
Comparison with Related Patterns
Facets vs Decorators
While both patterns add behavior dynamically, they serve different purposes:
Decorators: Wrap objects to modify or extend their interface Facets: Compose objects from multiple behavioral aspects
// Decorator pattern - wrapping behavior
class LoggingDecorator implements Employee {
constructor(private wrapped: Employee) {}
performAction(action: string): void {
console.log(`Performing: ${action}`);
this.wrapped.performAction(action);
console.log(`Completed: ${action}`);
}
}
// Facets pattern - compositional behavior
const employee = new FacetedObject(new EmployeeImpl());
employee.attachFacet(LoggingFacet);
employee.attachFacet(SecurityFacet);
// Employee now has both logging AND security capabilities
Facets vs Mixins
Mixins operate at the class level, facets at the instance level:
# Mixin - class-level composition
module Auditable
def log_action(action)
puts "Action: #{action}"
end
end
class Employee
include Auditable # All instances get audit capability
end
# Facets - instance-level composition
employee1 = FacetedObject.new(Employee.new)
employee1.attach_facet(AuditFacet.new) # Only this instance gets audit capability
employee2 = FacetedObject.new(Employee.new) # This instance doesn't have audit
Emerging Patterns
AI-Driven Facet Composition
Machine learning could optimize facet composition based on usage patterns:
class IntelligentFacetComposer {
private usageAnalyzer: UsageAnalyzer;
private mlModel: FacetRecommendationModel;
async recommendFacets(
objectType: string,
context: CompositionContext
): Promise<FacetRecommendation[]> {
const usagePatterns = await this.usageAnalyzer.analyze(objectType);
const contextFeatures = this.extractFeatures(context);
return this.mlModel.predict(usagePatterns, contextFeatures);
}
async optimizeForPerformance(
facetedObject: FacetedObject<any>
): Promise<OptimizationSuggestions> {
const usage = await this.usageAnalyzer.getObjectUsage(facetedObject);
return {
facetsToPreload: usage.frequentlyUsedFacets,
facetsToLazyLoad: usage.rarelyUsedFacets,
cacheStrategy: usage.recommendedCacheStrategy
};
}
}
Blockchain and Distributed Facets
Distributed systems could benefit from blockchain-verified facet capabilities:
pub struct DistributedFacetRegistry {
blockchain_client: BlockchainClient,
capability_verifier: CapabilityVerifier,
}
impl DistributedFacetRegistry {
pub async fn verify_facet_capability(
&self,
facet_hash: &str,
required_permissions: &[String]
) -> Result<bool, DistributedError> {
// Verify facet authenticity on blockchain
let facet_record = self.blockchain_client
.get_facet_record(facet_hash).await?;
// Verify permissions
self.capability_verifier
.verify_permissions(&facet_record, required_permissions)
}
}
Conclusion
The facets pattern represents a powerful approach to runtime behavior composition that complements the Adaptive Object Model pattern I discussed previously. While AOM focuses on schema flexibility, facets address the equally important challenge of behavioral composition. The implementations in Rust, TypeScript, and Ruby demonstrate how this pattern can be adapted to different language paradigms while maintaining its core principles. Each language brings unique strengths: Rust’s type safety and performance, TypeScript’s gradual typing and tooling support, and Ruby’s metaprogramming elegance.
Unfortunately, ObjectSpace company that created Voyager went out of business and San Francisco Design Patterns book didn’t gain traction, in part because of its ties to the now-obsolete EJB technology and the performance overhead from using runtime reflection in the extension pattern. Nevertheless, the facets/extension pattern excels in domains requiring high configurability and runtime adaptability. However, it requires careful attention to performance implications and testing strategies. The pattern works best when you have clear separation of concerns and well-defined interfaces between facets. The combination of AOM for schema evolution and facets for behavior composition provides a comprehensive approach to building truly adaptive systems. Together, these patterns enable software that can evolve gracefully with changing requirements while maintaining performance and reliability.
The sample implementations are available at the Dynamic Facets Sample Project, providing working examples in all three languages discussed. These implementations serve as a foundation for building more sophisticated facet-based systems tailored to specific domain requirements.