Skip to main content

Creational Design Patterns

Design pattern adalah solusi efektif untuk masalah yang berulang dalam pengembangan perangkat lunak.

Sebuah pattern bukan kode yang langsung di-copy-paste — melainkan blueprint yang perlu disesuaikan dengan konteks spesifikmu. Satu pattern bisa memecahkan beberapa masalah, tapi tidak setiap masalah membutuhkan pattern.

Sumber: Refactoring Guru


Singleton

Memastikan sebuah kelas hanya memiliki satu instansi, dan menyediakan titik akses global ke instansi tersebut.

Explorer
src
problem1-singleton
databaseConnection.before.ts
databaseConnection.ts
userRepository.before.ts
userRepository.ts
productRepository.before.ts
productRepository.ts
transactionRepository.before.ts
transactionRepository.ts
demo.ts
src/problem1-singleton/databaseConnection.ts
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

export class DatabaseConnection {
private static maxConnectionPool: number = 2;
private static currentConnectionPool: number = 0;
private static _instance: DatabaseConnection | null = null;

private constructor() {
if (this.isConnectionFull()) throw Error("Connection pool is full");
this.init();
}

// Intinya ProductRepository, TransactionRepository, UserRepository langsung ambil instance
// tanpa harus buat instance baru lagi, karena sudah ada instance yang dibuat di constructor DatabaseConnection
public static getInstance(): DatabaseConnection {
if (this._instance === null) {
this._instance = new DatabaseConnection();
}
return this._instance;
}

private async init() {
console.log("Starting connection...");
console.log("Connecting to database ...");
console.log("✓ Connection created");
this.increaseCurrentConnnectionPool();
}

private increaseCurrentConnnectionPool() {
DatabaseConnection.currentConnectionPool++;
}

private isConnectionFull(): boolean {
return (
DatabaseConnection.currentConnectionPool ===
DatabaseConnection.maxConnectionPool
);
}
}
Masalah — Setiap repository membuat koneksi baru
export class DatabaseConnection {
private static maxConnectionPool: number = 2;
private static currentConnectionPool: number = 0;

constructor() { // constructor PUBLIC — bisa dibuat berkali-kali
if (this.isConnectionFull()) throw Error("Connection pool is full");
this.init();
}
// ...
}

// ❌ Setiap repository membuat koneksi barunya sendiri
export class UserRepository {
private _db: DatabaseConnection;
constructor() {
this._db = new DatabaseConnection(); // koneksi baru
}
}

export class ProductRepository {
private _db: DatabaseConnection;
constructor() {
this._db = new DatabaseConnection(); // koneksi baru lagi
}
}
Solusi — Semua repository berbagi satu koneksi
export class DatabaseConnection {
private static _instance: DatabaseConnection | null = null;

private constructor() { // constructor PRIVATE — tidak bisa dibuat dari luar
if (this.isConnectionFull()) throw Error("Connection pool is full");
this.init();
}

public static getInstance(): DatabaseConnection {
if (this._instance === null) {
this._instance = new DatabaseConnection();
}
return this._instance; // selalu mengembalikan instansi yang sama
}
// ...
}

// ✅ Semua repository berbagi satu koneksi
export class UserRepository {
private _db: DatabaseConnection;
constructor() {
this._db = DatabaseConnection.getInstance();
}
}

export class ProductRepository {
private _db: DatabaseConnection;
constructor() {
this._db = DatabaseConnection.getInstance();
}
}

Kapan Digunakan

  • Kamu butuh tepat satu instansi yang dibagi ke seluruh aplikasi (misal: koneksi DB, config, logger)
  • Membuat objeknya membutuhkan proses yang mahal dan sebaiknya hanya dilakukan sekali

Builder

Membangun objek yang kompleks secara bertahap, memisahkan proses konstruksi dari objek akhirnya.

Explorer
src
problem2-builder
iQueryBuilder.new.ts
query.ts
QueryBuilder.ts
demo.before.ts
demo.ts
src/problem2-builder/QueryBuilder.ts
// Ini file baru
import { IQueryBuilder } from "./iQueryBuilder.new";
import { Query } from "./query";

export class QueryBuilder implements IQueryBuilder {
private _table: string;
private _selectFields: string[];
private _whereClauses: string[];

constructor(table: string, selectFields: string[] = ["*"]) {
this._table = table;
this._selectFields = selectFields;
this._whereClauses = [];
}

setTable(table: string): IQueryBuilder {
this._table = table;
return this;
}

setSelectFields(fields: string[]): IQueryBuilder {
this._selectFields = fields;
return this;
}

setWhereClauses(clauses: string[]): IQueryBuilder {
this._whereClauses = clauses;
return this;
}

get(): Query {
return new Query(this._table, this._selectFields, this._whereClauses);
}
}
Masalah — Constructor yang membingungkan
import { Query } from "./query";

// Susah dibaca — argumen apa saja ini?
const query = new Query("users", ["id", "username", "email"], []);

query.print();

// ❌ Object bisa dimodifikasi langsung dari luar
query.table = "";
query.print();
Solusi — Builder yang bisa di-chain
import { QueryBuilder } from "./QueryBuilder";

// Mudah dibaca — setiap langkah jelas tujuannya
const query = new QueryBuilder("users", ["id", "username", "email"])
.setWhereClauses(["age > 18"])
.get();

query.print();

// ✅ Object tidak bisa dimodifikasi sembarangan
// query.table = ""; // tidak perlu lagi

Kapan Digunakan

  • Membangun objek dengan banyak konfigurasi opsional
  • Kamu ingin proses konstruksi yang mudah dibaca, langkah demi langkah
  • Proses konstruksi yang sama harus bisa menghasilkan representasi yang berbeda

Prototype

Membuat objek baru dengan mengkloning objek yang sudah ada, daripada membangun dari awal.

Explorer
src
problem3-prototype
iClonable.new.ts
invoice.before.ts
invoice.ts
demo.before.ts
demo.ts
src/problem3-prototype/invoice.ts
import { IClonable } from "./iClonable.new";

export class Invoice implements IClonable {
private _companyName: string;
private _logo: string;
private _footer: string;
private _date: Date;
private _clientName: string;
private _items: string[];
private _totalAmount: number;

constructor(
companyName: string, logo: string, footer: string,
date: Date, clientName: string, items: string[], totalAmount: number,
) {
console.log("Initializing new invoice...");
console.log("Loading invoice layout template...");
console.log("Loading company logo and footer...");
console.log("Setting up styles and fonts...");
console.log("✓ Invoice base setup complete.\n");

this._companyName = companyName;
this._logo = logo;
this._footer = footer;
this._date = date;
this._clientName = clientName;
this._items = items;
this._totalAmount = totalAmount;
}

clone(): Invoice {
return new Invoice(
this._companyName, this._logo, this._footer,
this._date, this._clientName, this._items, this._totalAmount,
);
}

// Getters & Setters
get clientName(): string { return this._clientName; }
get items(): string[] { return this._items; }
get totalAmount(): number { return this._totalAmount; }

set clientName(value: string) { this._clientName = value; }
set items(value: string[]) { this._items = value; }
set totalAmount(value: number) { this._totalAmount = value; }

public print() {
console.log(`===== INVOICE FOR ${this._clientName} =====`);
console.log(`Company: ${this._companyName}`);
console.log(`Date: ${this._date.toDateString()}`);
console.log(`Items: ${this._items.join(", ")}`);
console.log(`Total: $${this._totalAmount}`);
console.log(`${this._footer}`);
console.log("=".repeat(100));
}
}
Masalah — Setup mahal diulang terus
// Setiap invoice menjalankan constructor yang berat
const invoice1: Invoice = new Invoice(
"ABC Corp", "company_logo.png", "Thank you for your business!",
new Date(), "Client A", ["Laptop", "Mouse"], 1200,
);
// "Initializing new invoice..."
// "Loading invoice layout template..."
// "Loading company logo and footer..."
// "Setting up styles and fonts..."

const invoice2: Invoice = new Invoice(
"ABC Corp", "company_logo.png", "Thank you for your business!",
new Date(), "Client B", ["Monitor", "Keyboard"], 800,
);
// setup yang sama diulang lagi...

const invoice3: Invoice = new Invoice(
"ABC Corp", "company_logo.png", "Thank you for your business!",
new Date(), "Client C", ["Desk Chair"], 300,
);
// dan lagi...
Solusi — Clone lalu modifikasi
// Setup berat hanya dijalankan sekali
const invoice1: Invoice = new Invoice(
"ABC Corp", "company_logo.png", "Thank you for your business!",
new Date(), "Client A", ["Laptop", "Mouse"], 1200,
);

// Clone melewati constructor — hanya ubah yang berbeda
const invoice2: Invoice = invoice1.clone();
invoice2.clientName = "Client B";
invoice2.items = ["Monitor", "Keyboard"];
invoice2.totalAmount = 800;

const invoice3: Invoice = invoice1.clone();
invoice3.clientName = "Client C";
invoice3.items = ["Desk Chair"];
invoice3.totalAmount = 300;

Kapan Digunakan

  • Pembuatan objek membutuhkan proses yang mahal (setup berat, panggilan DB, loading file)
  • Kamu butuh banyak objek serupa yang hanya berbeda di beberapa properti
  • Kamu ingin menghindari subclassing hanya untuk mendapatkan state awal yang berbeda

Ringkasan

PatternTujuanMekanisme Utama
SingletonHanya satu instansi, dibagi secara globalConstructor private + getInstance()
BuilderMembangun objek kompleks secara bertahapMethod berantai + get()
PrototypeMengkloning objek yang ada daripada membuatnyaMethod clone()