ddd-base
v0.9.1
Published
DDD base class library for JavaScript application.
Downloads
5,433
Readme
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-base
Usage
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
XProps
typeXProps
should includeid: Identifier<string|number>
property.
class XIdentifier extends Identifier<string> {}
interface XProps {
id: XIdentifier; // <= required
}
- Pass
XProps
toEntity<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.
NonNullableBaseRepository
NullableBaseRepository
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 properties
Nesting 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 AEntityProps
This 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); // => true
props
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 test
Contributing
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