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.
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 with npm:
npm install ddd-base
Entities are domain concepts that have a unique identity in the problem domain.
Entity's equability is 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 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"
});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📝 ValueObject's props have not a limitation like Entity. Because, ValueObject's equability is not identifier.
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
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;
}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;
}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);📝 Limitation:
Convert can be possible one for one converting.
// Can not do convert following pattern
// JSON -> Entity
// a -> b, c properties
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.
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"
);
});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.
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); // => trueThis 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;
}
}| 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:
- azu/irodr: RSS reader client like LDR for Inoreader.
- azu/searchive: Search All My Documents{PDF}.
- proofdict/editor: Proofdict editor.
See Releases page.
Install devDependencies and Run npm test:
npm i -d && npm test
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
MIT © azu