Melded Object Composition (MOC) is Doh's system for controlling exactly how properties and methods combine when creating object instances from potentially multiple inherited Patterns. Think of it like defining custom recipes for merging ingredients: when you inherit multiple Patterns (ingredients), MOC specifies how their properties (e.g., arrays, objects, functions) should be intelligently blended together in the final object.
This makes object composition highly flexible and predictable, enabling complex components to be built reliably from smaller, reusable parts.
This guide covers:
MOC Definitions Meld: Remember that
moc
blocks from all inherited patterns are melded together using the standard Resolution Order. You only need to definemoc
entries in a derived pattern if you are introducing new properties/methods that require specific melding or are defining new lifecycle phases. Core phases likeobject_phase
andbuilder_phase
are typically defined in the baseobject
pattern and don't need redeclaration.
Important: Default behavior is Override-first. Unless a property declares a MOC melder (e.g.,
Array
,Object
,Method
,{}
), property values are replaced by later applications rather than melded. Method melding is opt-in. Use theOverride
melder to explicitly force replacement.
At its core, MOC is defined through the moc
property within a Pattern's idea (or an idea passed to New()
). This property holds an object where keys are property names and values specify the MOC behaviors (the "recipe") for that property:
Pattern('ExamplePattern', {
moc: {
arrayProp: 'Array', // Array melding with type validation
deepProp: {}, // Deep melding with nested control
methodProp: 'Method', // Method melding with context binding
overrideProp: 'Override', // Explicitly override this property
stringProp: 'IsString', // Type validation only
notNullProp: 'NotNull', // Null/undefined exclusion
multiProp: ['Array', 'NotNull'] // Multiple MOC behaviors
},
// Properties controlled by the above MOC definitions
arrayProp: [1, 2, 3],
deepProp: { nested: { value: true } },
methodProp: function() { /* ... */ },
overrideProp: function() { /* This will be replaced */},
stringProp: 'example',
notNullProp: 'must have a value',
multiProp: [4, 5, 6]
});
MOC operations are organized into logical "tracks" or categories, allowing different aspects of property behavior to be controlled independently. This means you can combine a merging strategy (like Array
) with validation (like NotNull
) without conflict.
The main tracks are:
melder
: Controls how property values combine during inheritance (e.g., Array
, Object
, Concat
, {}
).typeof
: Manages type validation (e.g., IsString
, IsArrayOrFalse
). Only one typeof
rule applies (the first one encountered in the inheritance chain).not*
: Handles type exclusion (e.g., NotNull
, NotUndefined
, NotString
). Multiple not
rules are cumulative.async_method
: Specifically controls asynchronous behavior for method melding types (used implicitly by Async_*
melders).Each track operates mostly independently when resolving the final behavior for a property.
MOC integrates with Doh's type system to provide validation and type-checking capabilities.
Type validation ensures properties conform to expected types:
Pattern('TypeValidationExample', {
moc: {
types: [
// Basic Types
'IsAny', // Any value (never fails)
'IsAnything', // Alias for IsAny
'IsDefined', // Not undefined and not null
'IsSet', // Alias for IsDefined (changing soon to be for Set's, instead)
'HasValue', // Not undefined, null, or "" (if string)
'IsLiteral', // Static values (not objects/arrays)
'IsBoolean', // true or false
'IsString', // String type
'IsNumber', // Number (excluding NaN)
'IsInt', // Integer numbers only
'IsArray', // Array type
'IsObjectObject', // Plain object (not array/literal)
'IsObject', // Alias for IsObjectObject
'IsIterable', // Has Symbol.iterator
'IsFunction', // Function type
'IsPromise', // Promise instance
'IsDohObject', // Doh object instance
// Or False Variants
'IsStringOrFalse', // String or false
'IsNumberOrFalse', // Number or false
'IsIntOrFalse', // Integer or false
'IsLiteralOrFalse', // Static value or false
'IsArrayOrFalse', // Array or false
'IsObjectOrFalse', // Object or false
'IsFunctionOrFalse', // Function or false
'IsPromiseOrFalse', // Promise or false
'IsDohObjectOrFalse', // Doh object or false
'IsPromiseOrFunctionOrFalse', // Promise, function, or false
// Composite Types
'IsStringOrArray', // String or array
'IsStringOrNumber', // String or number
'IsArrayOrObjectObject', // Array or plain object
'IsStringOrArrayOrObjectObject', // String, array, or plain object
// Composite Or False Variants
'IsStringOrArrayOrFalse', // String, array, or false
'IsArrayOrObjectObjectOrFalse', // Array, plain object, or false
'IsStringOrArrayOrObjectObjectOrFalse', // String, array, plain object, or false
// Special Validation
'IsKeySafe', // Safe to use as object key
// Different from HasValue in that it requires a String value
'IsStringAndHasValue', // String that is not empty
// Different from IsNumber in that it can be a string of digits,
// or decimal number with a decimal point, or a signed number
// excludes NaN, Infinity, and non-numeric strings
'IsOnlyNumbers', // Any number type, or String containing only numeric characters
// Object State
'IsObjectObjectAndEmpty', // Empty plain object
'IsObjectObjectAndNotEmpty', // Non-empty plain object
'IsObjectObjectAndNotEmptyOrFalse', // Non-empty plain object or false
'IsArrayAndEmpty', // Empty array
'IsArrayAndNotEmpty' // Non-empty array
]
}
});
Most type validation operators ignore null
and undefined
values by default, allowing properties to be optional. Use NotNull
or NotUndefined
to require a value.
Type exclusion operators prevent properties from having certain types:
Pattern('TypeExclusionExample', {
moc: {
value: [
'NotUndefined', // Cannot be undefined
'NotNull', // Cannot be null
'NotString', // Cannot be string
'NotNumber', // Cannot be number
'NotBoolean', // Cannot be boolean
'NotArray', // Cannot be array
'NotObject', // Cannot be object
'NotFunction', // Cannot be function
'NotPromise', // Cannot be Promise
'NotDohObject' // Cannot be Doh object
]
}
});
Unlike validation, multiple exclusions can be combined to create sophisticated type constraints.
MOC provides various melding strategies for combining properties during inheritance.
Override
)The Override
melder explicitly prevents any melding behavior and enforces standard property replacement. This reflects Doh's default semantics: when a property has no MOC melder, it is replaced rather than melded. Override
is provided to disambiguate intent and to force replacement.
This is useful for making your code's intent clear, especially with methods. It ensures that a method is replaced in the traditional object-oriented sense, rather than being combined with the base method.
Pattern('BaseWithMelding', {
moc: {
doSomething: 'Method' // This method is designed to be melded
},
doSomething: function() {
console.log('Base action');
}
});
Pattern('ChildWithOverride', 'BaseWithMelding', {
moc: {
doSomething: 'Override' // Explicitly state we are replacing, not melding
},
doSomething: function() {
console.log('Child action completely replaces base action');
}
});
const obj = New('ChildWithOverride');
obj.doSomething();
// Console output:
// "Child action completely replaces base action"
Array
)Combines arrays with duplicate removal:
Pattern('ArrayMeldingExample', {
moc: {
items: 'Array' // Automatically includes IsArray validation
},
items: [1, 2]
});
const obj = New('ArrayMeldingExample', {
items: [2, 3]
});
console.log(obj.items); // [1, 2, 3]
Concat
)Combines strings or arrays without removing duplicates:
Pattern('ConcatExample', {
moc: {
text: 'Concat',
list: 'Concat'
},
text: 'Hello ',
list: [1, 2]
});
const obj = New('ConcatExample', {
text: 'World',
list: [2, 3]
});
console.log(obj.text); // "Hello World"
console.log(obj.list); // [1, 2, 2, 3]
Object
)Performs shallow merging of objects using Object.assign
:
Pattern('ObjectMeldingExample', {
moc: {
config: 'Object'
},
config: {
a: 1,
b: { x: 1 }
}
});
const obj = New('ObjectMeldingExample', {
config: {
b: { y: 2 },
c: 3
}
});
console.log(obj.config); // { a: 1, b: { y: 2 }, c: 3 }
{}
)Recursively merges objects with nested MOC control:
Pattern('DeepMeldingExample', {
moc: {
settings: { // Empty object = deep melding
theme: 'IsString',
counts: 'Array',
nested: { // Nested MOC definitions
enabled: 'IsBoolean'
}
}
},
settings: {
theme: 'light',
counts: [1],
nested: {
enabled: true
}
}
});
const obj = New('DeepMeldingExample', {
settings: {
counts: [2],
nested: {
enabled: false
}
}
});
console.log(obj.settings);
// {
// theme: 'light',
// counts: [1, 2],
// nested: { enabled: false }
// }
All method melding types (Method
, Chain
, Blender
, etc.) are opt-in and are designed to combine functions from the inheritance chain into a single executable method. By default (no melder), methods are replaced. If you need classic override semantics in the presence of inherited method melders, use the 'Override'
melder to force replacement and avoid melding.
MOC provides sophisticated control over how methods are combined during inheritance.
All method melding types share these core behaviors:
this
binding in all contexts (callbacks, events, timers)pre_methodName
and post_methodName
Method
)The standard method melding type. Executes methods in Pattern Resolution Order (base to derived) and intelligently merges truthy return values onto this
object.
Pattern('MethodExample', {
moc: {
handler: 'Method'
},
pre_handler: function() {
console.log('Pre-processing');
},
handler: function(arg1, arg2) {
console.log('Base handling', arg1, arg2);
return { newProp: 'value' }; // Merged onto this
},
post_handler: function() {
console.log('Post-processing');
}
});
Pattern('ExtendedMethod', 'MethodExample', {
handler: function(arg1, arg2) {
console.log('Extended handling', arg1, arg2);
return { anotherProp: 'value2' }; // Also merged onto this
}
});
Key behaviors:
this
false
to short-circuit with false
true
to short-circuit with this
Phase
)Designates a method as a lifecycle phase. Methods of this type are executed in a specific order determined by __.phase_order()
within the object.machine()
function during the synchronous New()
execution. They automatically support pre_
and post_
hooks (e.g., pre_myPhase
, post_myPhase
) which are executed immediately before and after the main phase method during the machine
execution, without needing explicit MOC declarations for the hooks themselves.
IMPORTANT: Phases and their pre_
/post_
hooks can be asynchronous (async
) only when using AsyncNew()
. When using regular New()
, phases must remain synchronous as they are part of the core synchronous object construction process. Using async
phase handlers with New()
will generate warnings.
If you need asynchronous initialization:
AsyncNew()
with async phase handlersMethod
or Async_Method
and call it after the synchronous phases completepre_
/post_
hooks (which can be async for Method
/Async_Method
), providing structured flow control for your asynchronous logic// Example using AsyncNew() with async phases (Recommended approach)
Pattern('AsyncCustomPhaseExample', {
moc: {
custom_setup_phase: 'Phase' // Still declared as 'Phase'
},
async pre_custom_setup_phase() {
console.log('Pre custom setup (async)');
this.preData = await fetch('/api/pre-data').then(r => r.json());
},
async custom_setup_phase() {
console.log('Running custom_setup_phase (async)');
this.setupData = await fetch('/api/setup').then(r => r.json());
return { customInitialized: true, setupId: this.setupData.id };
},
async post_custom_setup_phase() {
console.log('Post custom setup (async)');
await this.finalizeSetup();
return { setupComplete: true };
},
async finalizeSetup() {
// Additional async work
await new Promise(resolve => setTimeout(resolve, 100));
this.finalData = { timestamp: Date.now() };
}
});
// CORRECT: Use AsyncNew for patterns with async phases
const asyncObj = await AsyncNew('AsyncCustomPhaseExample');
// Alternative approach: Synchronous phases with separate async method
Pattern('MixedPhaseExample', {
moc: {
setup_phase: 'Phase', // Synchronous phase
async_init: 'Async_Method' // Separate async method
},
setup_phase() {
console.log('Synchronous setup');
this.ready = false;
return { basicSetup: true };
},
async async_init() {
console.log('Async initialization');
this.data = await fetch('/api/data').then(r => r.json());
this.ready = true;
return { asyncComplete: true };
}
});
// Can use regular New(), then call async method separately
const mixedObj = New('MixedPhaseExample');
await mixedObj.async_init(); // Call async method after construction
Key behaviors:
machine
AsyncNew()
, synchronous with regular New()
this
pre_
and post_
hooks, which can also be async with AsyncNew()
This_Blender
)Functionally identical to Method
. This name exists for semantic clarity, emphasizing that return values blend onto this
. Method
is the original name and remains fully supported. Choose whichever name makes the intent clearer in your context.
Pattern('ThisBlenderExample', {
moc: {
configure: 'This_Blender'
},
configure: function(options) {
console.log('Configuring with', options);
return { configured: true }; // Merged onto this
}
});
Key behaviors:
This_Blender
this
Phase
is the only MOC type that cannot be made asynchronous. Its pre_
and post_
hooks must also be synchronous.Chain
)Executes methods in Pattern Resolution Order, passing the return value of one method as the first argument to the next, enabling sequential processing or data transformation pipelines.
Pattern('ChainExample', {
moc: {
process: 'Chain'
},
process: function(data) {
console.log('Processing', data);
return data + 1; // Passed as first arg to next method
}
});
Pattern('ExtendedChain', 'ChainExample', {
process: function(data) {
console.log('Extended processing', data);
return data * 2; // Final return value
}
});
const obj = New('ExtendedChain');
console.log(obj.process(5)); // Outputs: 12 ((5 + 1) * 2)
Key behaviors:
Blender
)Executes methods in Pattern Resolution Order, merging return values onto a target object passed as the first argument. Useful for enhancing or modifying an external object.
Pattern('BlenderExample', {
moc: {
enhance: 'Blender'
},
enhance: function(target, extra) {
console.log('Enhancing with', extra);
return { base: true }; // Merged onto target
}
});
Pattern('ExtendedBlender', 'BlenderExample', {
enhance: function(target, extra) {
return { extended: true }; // Also merged onto target
}
});
const obj = New('ExtendedBlender');
const result = obj.enhance({}, 'extra');
console.log(result); // { base: true, extended: true }
Key behaviors:
Funnel
)Like Blender
, but executes methods in reverse Pattern Resolution Order (derived to base). Merges return values onto the target object (first argument). Useful for applying final modifications or configurations.
Note: Like all method melding types except Phase
, Funnel
methods can be async
if declared using the Async_Funnel
type.
Pattern('FunnelExample', {
moc: {
finalize: 'Funnel'
},
finalize: function(target) {
console.log('Base finalizing');
return { base: true }; // Applied last
}
});
Pattern('ExtendedFunnel', 'FunnelExample', {
finalize: function(target) {
console.log('Extended finalizing');
return { extended: true }; // Applied first
}
});
const obj = New('ExtendedFunnel', {
finalize: function(target) {
console.log('Idea finalizing');
return { idea: true }; // Applied first
}
});
const result = obj.finalize({});
console.log(result);
// { idea: true, extended: true, base: true }
Key behaviors:
This_Funnel
)Like Method
/This_Blender
, but executes in reverse Pattern Resolution Order (derived to base). Merges return values onto this
. Often used for cleanup or teardown logic.
Pattern('ThisFunnelExample', {
moc: {
cleanup: 'This_Funnel'
},
cleanup: function() {
console.log('Base cleanup');
return { baseClean: true }; // Applied last onto this
}
});
Pattern('ExtendedThisFunnel', 'ThisFunnelExample', {
cleanup: function() {
console.log('Extended cleanup');
return { extendedClean: true }; // Applied first onto this
}
});
Key behaviors:
this
Chain_Funnel
)Like Chain
, but executes in reverse Pattern Resolution Order (derived to base). Passes return values sequentially as the first argument to the next method. Useful for bottom-up transformations.
Pattern('ChainFunnelExample', {
moc: {
transform: 'Chain_Funnel'
},
transform: function(value) {
console.log('Base transform', value);
return value + 1; // Applied last
}
});
Pattern('ExtendedChainFunnel', 'ChainFunnelExample', {
transform: function(value) {
console.log('Extended transform', value);
return value * 2; // Applied first
}
});
const obj = New('ExtendedChainFunnel');
console.log(obj.transform(5)); // Outputs: 11 ((5 * 2) + 1)
Key behaviors:
Each method type (except Phase) has an async variant prefixed with Async_
:
Pattern('AsyncExample', {
moc: {
method: 'Async_Method',
chain: 'Async_Chain',
blender: 'Async_Blender',
funnel: 'Async_Funnel',
this_funnel: 'Async_This_Funnel',
chain_funnel: 'Async_Chain_Funnel',
// init_phase: 'Phase' // NO Async_Phase! Phases must be sync.
},
async method() {
await someAsyncOperation();
return { done: true };
}
});
Key behaviors:
Phase
methods can be async when using AsyncNew()
(unlike other method types which have dedicated async variants)The {}
melder enables MOC definitions for nested properties without adding MOC metadata to the actual objects:
Pattern('NestedMOCExample', {
moc: {
config: { // Deep melding with nested MOC
theme: 'IsString',
counts: 'Array',
nested: { // Further nesting
enabled: 'IsBoolean',
values: 'Array'
}
}
}
});
This is particularly useful when the config object needs to be passed to APIs that don't expect additional properties.
Properties can have multiple MOC options from different tracks:
Pattern('MultipleOptionsExample', {
moc: {
// Combines Array melding, type validation, and exclusions
items: ['Array', 'NotNull', 'NotUndefined']
}
});
Melders automatically include appropriate type validation:
Pattern('AutoTypeCheckExample', {
moc: {
list: 'Array', // Includes IsArray
obj: 'Object', // Includes IsObjectObject
fn: 'Method', // Includes IsFunction
text: 'Concat' // Includes IsString or IsArray
}
});
MOC is tightly integrated with several other core Doh systems:
Pattern System: MOC definitions in patterns control how properties are inherited and combined when creating new objects.
Inheritance System: MOC works with Doh's inheritance system to determine how properties are resolved and merged across the inheritance chain.
Object Lifecycle: Method MOC types, especially Phase, play a crucial role in the object lifecycle, controlling how initialization methods are executed. Remember that Phase
methods and their pre_
/post_
hooks must be synchronous.
Sub-Object-Builder: MOC definitions help the Sub-Object-Builder system determine how nested objects should be constructed and composed.