ddd-base icon indicating copy to clipboard operation
ddd-base copied to clipboard

Entity/ValueObject: receive Props Instead of Identifier

Open azu opened this issue 7 years ago • 5 comments

I've noticed that I often write a entity like following:

export class HatebuIdentifier extends Identifier<string> {}
export interface HatebuProps {
    readonly id: HatebuIdentifier;
    readonly bookmark: Bookmark;
}

export class Hatebu extends Entity<HatebuIdentifier> implements HatebuProps {
    readonly bookmark: Bookmark;

    constructor(args: HatebuProps) {
        super(args.id);
        this.bookmark = args.bookmark;
    }

    get name() {
        return this.id.toValue();
    }

    addBookmarkItems(bookmarkItems: BookmarkItem[]) {
        return new Hatebu({
            ...(this as HatebuProps),
            bookmark: this.bookmark.addBookmarkItems(bookmarkItems)
        });
    }

    updateBookmarkItems(bookmarkItems: BookmarkItem[]) {
        return new Hatebu({
            ...(this as HatebuProps),
            bookmark: this.bookmark.updateBookmarkItems(bookmarkItems)
        });
    }
}

This Hatebu entity has id and Props type. We can use Props type insteadof Id. Becaseue, Alwasys Props includes id.

Props

  • We can realize mixin meaningful #5
  • Copyable insterface just use Props type
    • It is similar with React https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/index.d.ts#L287-L288

Cons

  • id is fixed name
  • Always arguments is object

azu avatar Apr 30 '18 04:04 azu

TypeScript can not implements generics type. We can just use constructor(prop: Props) that is similar with React.

type Props = {
  id: Identifier<string>
}
class MyEntity extends Entity<Props>{}

azu avatar Apr 30 '18 05:04 azu

https://github.com/azu/hatebupwa/pull/11

I've tried to apply this pattern. There are props and cons.

Props

  • Props merging is easy
         return new AppSession({
-            ...(this as AppSessionProps),
+            ...this.props
+            hatebuId: hatebu.props.id
         });

Cons

  • Accessing to nest entity props is hard.

This is ugly. :(

-        const lastUpdatedDate = hatebu.bookmark.lastUpdated;
+        const lastUpdatedDate = hatebu.props.bookmark.props.lastUpdated;

We can avoid this cons to define getter function to entity. But, It will increase code.

azu avatar May 20 '18 07:05 azu

x.props.y.props.z is unly. But, we can avoid this by assign props to properties manually. Compare 0.4 between 0.5 is very similar code.

props is immutable value. It will be changed in constructor. own property like state is mutable value. It will be changed in anywhere.

import { Entity, Identifier, ValueObject } from "../src";
import * as assert from "assert";


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) {
        super(props);
        this.id = props.id;
        this.name = props.name;
        this.price = props.price;
    }
}

interface ShoppingCartItemCollectionProps {
    items: ShoppingCartItem[];
}

class ShoppingCartItemCollection extends ValueObject<ShoppingCartItemCollectionProps> implements ShoppingCartItemCollectionProps {
    items: ShoppingCartItem[];

    constructor(props: ShoppingCartItemCollectionProps) {
        super(props);
        this.items = props.items
    }
}

class ShoppingIdentifier extends Identifier<string> {
}

interface ShoppingCartProps {
    id: ShoppingIdentifier;
    itemsCollection: ShoppingCartItemCollection;
}

class ShoppingCart extends Entity<ShoppingCartProps> implements ShoppingCartProps {
    id: ShoppingIdentifier;
    itemsCollection: ShoppingCartItemCollection;

    constructor(props: ShoppingCartProps) {
        super(props);
        this.id = props.id;
        this.itemsCollection = props.itemsCollection;
    }
}

describe("ShoppingCart", () => {
    it("should have own property and props", () => {
        const shoppingCart = new ShoppingCart({
            id: new ShoppingCartItemIdentifier("shopping-cart"),
            itemsCollection: new ShoppingCartItemCollection({
                items: []
            })
        });
        assert.ok(Array.isArray(shoppingCart.itemsCollection.items));
        assert.strictEqual(shoppingCart.itemsCollection.items, shoppingCart.props.itemsCollection.props.items)
    });
});

azu avatar May 20 '18 07:05 azu

Add docs

azu avatar May 20 '18 08:05 azu

TypeScript support declaretion merging

interface UserData {
    name: string;
    birthday: Date;
    regdate: Date;
    social: string;
    passwordHash: string;
    isAdmin: boolean;
}
interface User extends UserData {}
class User {
    constructor(data: UserData) {
        Object.assign(this, data);
    }

    deservesCake() {
        return this.birthday.getDate() == new Date().getDate() || this.isAdmin;
    }
}

This approach has a limitation. TS2564 Strict Property Initialization Checks does not work.

azu avatar May 20 '18 09:05 azu