ddd-base v0.9.1
ddd-base 
Status: Experimental
DDD base class library for JavaScript client-side application.
Notes:
This library does not depend on Almin. You can use it with other JavaScript framework.
Features
This library provide basic DDD base classes.
Entity: Entity is domain concept that have a unique identityIdentifier: Identifier is unique identifier for an EntityValueObject: Value Object is an entity’s state, describing something about the entityRepository: Repository is used to manage aggregate persistenceConverter: Converter convert between Entity <-> Props(Entity's props) <-> JSON(Serialized data object)
Install
Install with npm:
npm install ddd-baseUsage
Entity
Entities are domain concepts that have a unique identity in the problem domain.
Entity's equability is Identifier.
Identifier
Identifier is a unique object for each entity.
Entity#equals check that the Entity's identifier is equaled to other entity's identifier.
Entity Props
Entity Props have to id props that is instance of Identifier.
- Define
XPropstypeXPropsshould includeid: Identifier<string|number>property.
class XIdentifier extends Identifier<string> {}
interface XProps {
id: XIdentifier; // <= required
}- Pass
XPropstoEntity<XProps>
class XEntity extends Entity<XProps> {
// implement
}You can get the props via entity.props.
const xEntity = new XEntity({
id: new XIdentifier("x");
});
console.log(xEntity.props.id);Example:
// Entity A
class AIdentifier extends Identifier<string> {}
interface AProps {
id: AIdentifier;
}
class AEntity extends Entity<AProps> {}
// Entity B
class BIdentifier extends Identifier<string> {}
interface BProps {
id: BIdentifier;
}
class BEntity extends Entity<BProps> {}
// A is not B
const a = new AEntity({
id: new AIdentifier("1"))
});
const b = new BEntity({
id: new BIdentifier("1")
});
assert.ok(!a.equals(b), "A is not B");Props can includes other property.
// Entity A
class AIdentifier extends Identifier<string> {}
interface AProps {
id: AIdentifier;
a: number;
b: string;
}
class AEntity extends Entity<AProps> {
constructor(props: AProps) {
super(props);
}
}
const entity = new AEntity({
id: new AIdentifier("a"),
a: 42,
b: "string"
});ValueObject
Value object is an entity’s state, describing something about the entity or the things it owns.
ValueObject's equability is values.
import {ValueObject} from "ddd-base";
// X ValueObject
type XProps = { value: number };
class XValue extends ValueObject<XProps> {
constructor(props: XProps) {
super(props);
}
}
// x1's value equal to x2's value
const x1 = new XValue({ value: 42 });
const x2 = new XValue({ value: 42 });
console.log(x1.props.value); // => 42
console.log(x2.props.value); // => 42
console.log(x1.equals(x2));// => true
// x3's value not equal both
const x3 = new XValue({ value: 1 });
console.log(x1.equals(x3));// => false
console.log(x2.equals(x3));// => false:memo: ValueObject's props have not a limitation like Entity. Because, ValueObject's equability is not identifier.
Repository
A repository is used to manage aggregate persistence and retrieval while ensuring that there is a separation between the domain model and the data model.
Repository collect entity.
Currently, Repository implementation is in-memory database like Map.
This library provide following types of repository.
NonNullableBaseRepositoryNullableBaseRepository
NonNullableBaseRepository
NonNullableRepository has initial value.
In other words, NonNullableRepository#get always return a value.
/**
* NonNullableRepository has initial value.
* In other words, NonNullableRepository#get always return a value.
*/
export declare class NonNullableRepository<Entity extends EntityLike<any>, Props extends Entity["props"], Id extends Props["id"]> {
protected initialEntity: Entity;
private core;
constructor(initialEntity: Entity);
readonly map: MapLike<string, Entity>;
readonly events: RepositoryEventEmitter<Entity>;
get(): Entity;
getAll(): Entity[];
findById(entityId?: Id): Entity | undefined;
save(entity: Entity): void;
delete(entity: Entity): void;
clear(): void;
}NullableBaseRepository
NullableRepository has not initial value.
In other word, NullableRepository#get may return undefined.
/**
* NullableRepository has not initial value.
* In other word, NullableRepository#get may return undefined.
*/
export declare class NullableRepository<Entity extends EntityLike<any>, Props extends Entity["props"], Id extends Props["id"]> {
private core;
constructor();
readonly map: MapLike<string, Entity>;
readonly events: RepositoryEventEmitter<Entity>;
get(): Entity | undefined;
getAll(): Entity[];
findById(entityId?: Id): Entity | undefined;
save(entity: Entity): void;
delete(entity: Entity): void;
clear(): void;
}Converter
JSON <-> Props <-> Entity
Converter is that convert JSON <-> Props <-> Entity.
createConverter create Converter instance from Props and JSON types and converting definition.
// Pass Props type and JSON types as generics
// 1st argument is that a Constructor of entity that is required for creating entity from JSON
// 2nd argument is that a mapping object
// mapping object has tuple array for each property.
// tuple is [Props to JSON, JSON to Props]
createConverter<PropsType, JSONType>(EntityConstructor, mappingObject): Converter<PropsType, JSONType>;mappingObject has tuple array for each property.
const converter = createConverter<EntityProps, EntityJSON>(Entity, {
id: [propsToJSON function, jsonToProps function],
// [(prop value) => json value, (json value) => prop value]
});Example of createConveter.
// Entity A
class AIdentifier extends Identifier<string> {
}
interface AProps {
id: AIdentifier;
a1: number;
a2: string;
}
class AEntity extends Entity<AProps> {
constructor(args: AProps) {
super(args);
}
}
interface AEntityJSON {
id: string;
a1: number;
a2: string;
}
// Create converter
// Tuple has two convert function that Props -> JSON and JSON -> Props
const AConverter = createConverter<AProps, AEntityJSON>(AEntity, {
id: [prop => prop.toValue(), json => new AIdentifier(json)],
a1: [prop => prop, json => json],
a2: [prop => prop, json => json]
});
const entity = new AEntity({
id: new AIdentifier("a"),
a: 42,
b: "b prop"
});
// Entity to JSON
const json = AConverter.toJSON(entity);
assert.deepStrictEqual(json, {
id: "a",
a: 42,
b: "b prop"
});
// JSON to Entity
const entity_conveterted = converter.fromJSON(json);
assert.deepStrictEqual(entity, entity_conveterted);:memo: Limitation:
Convert can be possible one for one converting.
// Can not do convert following pattern
// JSON -> Entity
// a -> b, c propertiesNesting Converter
You can set Converter instead of mapping functions.
This pattern called Nesting Converter.
// Parent has A and B
class ParentIdentifier extends Identifier<string> {
}
interface ParentJSON {
id: string;
a: AEntityJSON;
b: BValueJSON;
}
interface ParentProps {
id: ParentIdentifier;
a: AEntity;
b: BValue;
}
class ParentEntity extends Entity<ParentProps> {
}
const ParentConverter = createConverter<ParentProps, ParentJSON>(ParentEntity, {
id: [prop => prop.toValue(), json => new ParentIdentifier(json)],
a: AConverter, // Set Conveter instead of mapping functions
b: BConverter
});For more details, see test/Converter-test.ts.
Deprecated Serializer
JSON <-> Entity
DDD-base just define the interface of Serializer that does following converting.
- Convert from JSON to Entity
- Convert from Entity to JSON
You can implement Serializer interface and use it.
export interface Serializer<Entity, JSON> {
/**
* Convert Entity to JSON format
*/
toJSON(entity: Entity): JSON;
/**
* Convert JSON to Entity
*/
fromJSON(json: JSON): Entity;
}Implementation:
// Entity A
class AIdentifier extends Identifier<string> {}
interface AEntityArgs {
id: AIdentifier;
a: number;
b: string;
}
class AEntity extends Entity<AIdentifier> {
private a: number;
private b: string;
constructor(args: AEntityArgs) {
super(args.id);
this.a = args.a;
this.b = args.b;
}
toJSON(): AEntityJSON {
return {
id: this.id.toValue(),
a: this.a,
b: this.b
};
}
}
// JSON
interface AEntityJSON {
id: string;
a: number;
b: string;
}
// Serializer
const ASerializer: Serializer<AEntity, AEntityJSON> = {
fromJSON(json) {
return new AEntity({
id: new AIdentifier(json.id),
a: json.a,
b: json.b
});
},
toJSON(entity) {
return entity.toJSON();
}
};
it("toJSON: Entity -> JSON", () => {
const entity = new AEntity({
id: new AIdentifier("a"),
a: 42,
b: "b prop"
});
const json = ASerializer.toJSON(entity);
assert.deepStrictEqual(json, {
id: "a",
a: 42,
b: "b prop"
});
});
it("fromJSON: JSON -> Entity", () => {
const entity = ASerializer.fromJSON({
id: "a",
a: 42,
b: "b prop"
});
assert.ok(entity instanceof AEntity, "entity should be instanceof AEntity");
assert.deepStrictEqual(
ASerializer.toJSON(entity),
{
id: "a",
a: 42,
b: "b prop"
},
"JSON <-> Entity"
);
});:memo: Design Note
Why entity and value object has props?
It come from TypeScript limitation. TypeScript can not define type of class's properties.
// A limitation of generics interface
type AEntityProps = {
key: string;
}
class AEntity extends Entity<AEntityProps> {}
const aEntity = new AEntity({
key: "value"
});
// can not type
aEntity.key; // type is any?We can resolve this issue by introducing props property.
// `props` make realize typing
type AEntityProps = {
key: string;
}
class AEntity extends Entity<AEntityProps> {}
const aEntity = new AEntity({
key: "value"
});
// can not type
aEntity.props; // props is AEntityPropsThis approach is similar with React.
Nesting props is ugly
If you want to access nested propery via props, you have written a.props.b.props.c.
It is ugly syntax.
Instead of this, you can assign props values to entity's properties directly.
class ShoppingCartItemIdentifier extends Identifier<string> {
}
interface ShoppingCartItemProps {
id: ShoppingCartItemIdentifier;
name: string;
price: number;
}
class ShoppingCartItem extends Entity<ShoppingCartItemProps> implements ShoppingCartItemProps {
id: ShoppingCartItemIdentifier;
name: string;
price: number;
constructor(props: ShoppingCartItemProps) {
// pass to props
super(props);
// assign own property
this.id = props.id;
this.name = props.name;
this.price = props.price;
}
}
const item = new ShoppingCartItem({
id: new ShoppingCartItemIdentifier("item 1");
name: "bag";
price: 1000
});
console.log(item.props.price === item.price); // => trueprops is readonly by default
This is related with "Nesting props is ugly"
props is readonly and Object.freeze(props) by default.
props:
It is clear that props are a Entity's configureation.
They are received from above and immutable as far as the Entity receiving them is concerned.
state:
ddd-base does not define state type.
But, state is own properties of Entity.
It is mutable value and it can be modified by default.
For example, this.id, this.name, and this.price are state of ShoppingCartItem.
You can modify this state.
class ShoppingCartItemIdentifier extends Identifier<string> {
}
interface ShoppingCartItemProps {
id: ShoppingCartItemIdentifier;
name: string;
price: number;
}
class ShoppingCartItem extends Entity<ShoppingCartItemProps> implements ShoppingCartItemProps {
id: ShoppingCartItemIdentifier;
name: string;
price: number;
constructor(props: ShoppingCartItemProps) {
// pass to props
super(props);
// assign own property
this.id = props.id;
this.name = props.name;
this.price = props.price;
}
}Changing props and state
| props | state | |
|---|---|---|
| Can get initial value from parent Entity? | Yes | Yes |
| Can be changed by parent Entity? | Yes | No |
| Can set default values inside Entity? | Yes | Yes |
| Can change inside Entity? | No | Yes |
| Can set initial value for child Entity? | Yes | Yes |
Related concept:
Real UseCase
- azu/irodr: RSS reader client like LDR for Inoreader.
- azu/searchive: Search All My Documents{PDF}.
- proofdict/editor: Proofdict editor.
Changelog
See Releases page.
Running tests
Install devDependencies and Run npm test:
npm i -d && npm testContributing
Pull requests and stars are always welcome.
For bugs and feature requests, please create an issue.
- Fork it!
- Create your feature branch:
git checkout -b my-new-feature - Commit your changes:
git commit -am 'Add some feature' - Push to the branch:
git push origin my-new-feature - Submit a pull request :D
Author
License
MIT © azu