https://github.com/almin/ddd-base
DDD base class library for JavaScript application.
https://github.com/almin/ddd-base
class ddd javascript typescript
Last synced: about 2 months ago
JSON representation
DDD base class library for JavaScript application.
- Host: GitHub
- URL: https://github.com/almin/ddd-base
- Owner: almin
- License: mit
- Created: 2017-08-26T09:17:43.000Z (almost 8 years ago)
- Default Branch: master
- Last Pushed: 2021-05-12T22:59:33.000Z (about 4 years ago)
- Last Synced: 2025-04-09T07:47:21.040Z (2 months ago)
- Topics: class, ddd, javascript, typescript
- Language: TypeScript
- Size: 2.18 MB
- Stars: 77
- Watchers: 1
- Forks: 7
- Open Issues: 8
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# ddd-base [](https://travis-ci.org/almin/ddd-base)
**Status**: Experimental
DDD base class library for JavaScript client-side application.
**Notes**:
This library does not depend on [Almin](https://github.com/almin/almin).
You can use it with other JavaScript framework.## Features
This library provide basic DDD base classes.
- [`Entity`](#entity): Entity is domain concept that have a unique identity
- [`Identifier`](#identifier): Identifier is unique identifier for an Entity
- [`ValueObject`](#valueobject): Value Object is an entity’s state, describing something about the entity
- [`Repository`](#repository): Repository is used to manage aggregate persistence
- [`Converter`](#converter): Converter convert between Entity <-> Props(Entity's props) <-> JSON(Serialized data object)## Install
Install with [npm](https://www.npmjs.com/):
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`.
1. Define `XProps` type
- `XProps` should include `id: Identifier` property.```ts
class XIdentifier extends Identifier {}
interface XProps {
id: XIdentifier; // <= required
}
```2. Pass `XProps` to `Entity`
```ts
class XEntity extends Entity {
// implement
}
```You can get the props via `entity.props`.
```ts
const xEntity = new XEntity({
id: new XIdentifier("x");
});
console.log(xEntity.props.id);
```**Example:**
```ts
// Entity A
class AIdentifier extends Identifier {}
interface AProps {
id: AIdentifier;
}
class AEntity extends Entity {}
// Entity B
class BIdentifier extends Identifier {}
interface BProps {
id: BIdentifier;
}
class BEntity extends Entity {}
// 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.
```ts
// Entity A
class AIdentifier extends Identifier {}interface AProps {
id: AIdentifier;
a: number;
b: string;
}class AEntity extends Entity {
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.
```ts
import {ValueObject} from "ddd-base";// X ValueObject
type XProps = { value: number };class XValue extends ValueObject {
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.```ts
/**
* NonNullableRepository has initial value.
* In other words, NonNullableRepository#get always return a value.
*/
export declare class NonNullableRepository, Props extends Entity["props"], Id extends Props["id"]> {
protected initialEntity: Entity;
private core;
constructor(initialEntity: Entity);
readonly map: MapLike;
readonly events: RepositoryEventEmitter;
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.```ts
/**
* NullableRepository has not initial value.
* In other word, NullableRepository#get may return undefined.
*/
export declare class NullableRepository, Props extends Entity["props"], Id extends Props["id"]> {
private core;
constructor();
readonly map: MapLike;
readonly events: RepositoryEventEmitter;
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.
```ts
// 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(EntityConstructor, mappingObject): Converter;
````mappingObject` has tuple array for each property.
```ts
const converter = createConverter(Entity, {
id: [propsToJSON function, jsonToProps function],
// [(prop value) => json value, (json value) => prop value]
});
```Example of `createConveter`.
```ts
// Entity A
class AIdentifier extends Identifier {
}interface AProps {
id: AIdentifier;
a1: number;
a2: string;
}class AEntity extends Entity {
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(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**.```ts
// Parent has A and B
class ParentIdentifier extends Identifier {
}interface ParentJSON {
id: string;
a: AEntityJSON;
b: BValueJSON;
}interface ParentProps {
id: ParentIdentifier;
a: AEntity;
b: BValue;
}class ParentEntity extends Entity {
}const ParentConverter = createConverter(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](./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 JSONYou can implement `Serializer` interface and use it.
```ts
export interface Serializer {
/**
* Convert Entity to JSON format
*/
toJSON(entity: Entity): JSON;/**
* Convert JSON to Entity
*/
fromJSON(json: JSON): Entity;
}
```Implementation:
```ts
// Entity A
class AIdentifier extends Identifier {}interface AEntityArgs {
id: AIdentifier;
a: number;
b: string;
}class AEntity extends Entity {
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 = {
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.```ts
// A limitation of generics interface
type AEntityProps = {
key: string;
}
class AEntity extends Entity {}const aEntity = new AEntity({
key: "value"
});
// can not type
aEntity.key; // type is any?
```We can resolve this issue by introducing `props` property.
```ts
// `props` make realize typing
type AEntityProps = {
key: string;
}
class AEntity extends Entity {}const aEntity = new AEntity({
key: "value"
});
// can not type
aEntity.props; // props is AEntityProps
```This approach is similar with [React](https://reactjs.org/).
- [Why did React use 'props' as an abbreviation for property/properties? - Quora](https://www.quora.com/Why-did-React-use-props-as-an-abbreviation-for-property-properties)
### 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.
```ts
class ShoppingCartItemIdentifier extends Identifier {
}interface ShoppingCartItemProps {
id: ShoppingCartItemIdentifier;
name: string;
price: number;
}class ShoppingCartItem extends Entity 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.```ts
class ShoppingCartItemIdentifier extends Identifier {
}interface ShoppingCartItemProps {
id: ShoppingCartItemIdentifier;
name: string;
price: number;
}class ShoppingCartItem extends Entity 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:
- [react-guide/props-vs-state.md at master · uberVU/react-guide](https://github.com/uberVU/react-guide/blob/master/props-vs-state.md)
- [Thinking in React - React](https://reactjs.org/docs/thinking-in-react.html)## Real UseCase
- [azu/irodr: RSS reader client like LDR for Inoreader.](https://github.com/azu/irodr "azu/irodr: RSS reader client like LDR for Inoreader.")
- [azu/searchive: Search All My Documents{PDF}.](https://github.com/azu/searchive "azu/searchive: Search All My Documents{PDF}.")
- [proofdict/editor: Proofdict editor.](https://github.com/proofdict/editor "proofdict/editor: Proofdict editor.")## Changelog
See [Releases page](https://github.com/almin/ddd-base/releases).
## 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](https://github.com/almin/ddd-base/issues).
1. Fork it!
2. Create your feature branch: `git checkout -b my-new-feature`
3. Commit your changes: `git commit -am 'Add some feature'`
4. Push to the branch: `git push origin my-new-feature`
5. Submit a pull request :D## Author
- [github/azu](https://github.com/azu)
- [twitter/azu_re](https://twitter.com/azu_re)## License
MIT © azu