dev-mastery / comments-api

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Classes vs. factory functions?

richardantao opened this issue · comments

Hi,

Firstly, thanks for the tutorial; I’ve spent the weekend refactoring my node js monolith and it is so much cleaner, and unit testing has never been so easy!

To my question: what is the difference between using classes vs factory functions in this architecture? Is there any difference or is it just semantics? Performance implications? I’m using TypeScript, do you think classes are a better fit in this case?

In JavaScript (especially at the time when I wrote this tutorial) the only way to get truly private members and avoid the traps of this was to use factory functions. Because factory functions let you use a closure to define a private field which you can then expose as read-only via a method.

With TypeScript Classes today you can use private fields to get truly private members, you can use getters and have the IDE enforce them, and Classes let you write custom type guards using instanceof instead of resorting to discriminated unions. So, in TypeScript today Classes are better in my opinion; although you still have to be careful of this traps.

@arcdev1 any chance of a small example for the usage of classes?
Thanks!

@ydennisy if I were to implement the Comment Entity in Typescript as a Class, it would look something like this. There's a lot of boilerplate but I use the extensions: reafctorix and TabNine Pro which give me really good auto-complete and code generation capabilities so I don't have to type much by hand.

// TODO: Implement SanitizedText and Md5Hash for real, these are stubs
const SanitizedText = buildSanitizedText({sanitize: (s: string) => s});
const Md5Hash = buildMd5Hash({ hash: (s: string) => s });

export class Comment {
  #author: Author;
  #createdOn: Date;
  #hash?: Md5Hash;
  #id: Id;
  #modifiedOn: Date;
  #postId: Id;
  #published: boolean;
  #replyToId?: Id;
  #source: Source;
  #text: SanitizedText;

  constructor({
    author,
    createdOn,
    id,
    modifiedOn,
    postId,
    published,
    replyToId,
    source,
    text,
  }: {
    author: Author;
    createdOn: Date;
    id: Id;
    modifiedOn: Date;
    postId: Id;
    published: boolean;
    replyToId: Id;
    source: Source;
    text: SanitizedText;
  }) {
    this.#author = author;
    this.#createdOn = createdOn;
    this.#id = id;
    this.#modifiedOn = modifiedOn;
    this.#postId = postId;
    this.#published = published;
    this.#replyToId = replyToId;
    this.#source = source;
    this.#text = text;
  }

  public publish() {
    this.#published = true;
  }

  public unpublish() {
    this.#published = false;
  }

  public markDeleted() {
    this.#text = new SanitizedText(".xX This comment has been deleted Xx.");
    this.#author = new Author({ name: "deleted" });
  }

  public get author() {
    return this.#author.value;
  }
  public get createdOn() {
    return this.#createdOn;
  }
  public get hash() {
    this.#hash = this.#hash ?? new Md5Hash(this.text + this.author + this.replyToId);
    return this.#hash.value;
  }
  public get id() {
    return this.#id.value;
  }
  public get modifiedOn() {
    return this.#modifiedOn;
  }
  public get postId() {
    return this.#postId.value;
  }
  public get published() {
    return this.#published;
  }
  public get replyToId() {
    return this.#replyToId;
  }
  public get source() {
    return this.#source;
  }
  public get text() {
    return this.#text.value;
  }
}

export interface ValueObject {
  value: Readonly<string>;
}

export class Author implements ValueObject {
  #value;
  constructor({ name }: { name: string }) {
    if (!name?.length) {
      throw new TypeError("Author cannot be null or empty.");
    }
    this.#value = name;
  }

  public get value() {
    return this.#value;
  }
}

interface IdMaker {
  makeId(): string;
}
interface Id extends ValueObject {}

export function buildId(idMaker: IdMaker) {
  return class IdImpl implements Id {
    #value;

    private static makeId() {
      let value = idMaker.makeId();
      return new IdImpl(value);
    }

    constructor(value: string) {
      // code to validate value
      this.#value = value;
    }

    public get value() {
      return this.#value;
    }
  };
}

export class Source {
  // class details
}

interface Md5Hasher {
  hash(vallue: string): string;
}
export interface Md5Hash extends ValueObject {}
export function buildMd5Hash(hasher: Md5Hasher) {
  return class Md5HashImpl implements Md5Hash {
    #value: string;

    constructor(value: string) {
      // validation code
      this.#value = value;
    }

    public get value() {
      return this.#value;
    }
  };
}

interface Sanitizer {
  sanitize(value: string): string;
}
export interface SanitizedText extends ValueObject {}
export function buildSanitizedText(sanitizer: Sanitizer) {
  return class SanitizedTextImpl implements SanitizedText {
    #value: string;

    constructor(text: string) {
      // validation code
      this.#value = text;
    }

    public get value() {
      return this.#value;
    }
  };
}