JavaScript Design Patterns | Singleton· Factory
이 글의 핵심
JavaScript design patterns: Singleton, Factory, Observer patterns. Learn principles, code implementation, and practical applications with real-world examples.
Introduction
What are Design Patterns?
Design Patterns are proven solution templates with names for recurring design problems. They help teams communicate efficiently with phrases like “let’s use a factory.” Why Learn Patterns:
- ✅ Proven Solutions: Already validated approaches
- ✅ Code Quality: Easier maintenance
- ✅ Communication: Common language among developers
- ✅ Reusability: Applicable to various situations
1. Singleton Pattern
Concept
Creates only one instance and makes it globally accessible.
Implementation
The following is a detailed implementation code using JavaScript. Define classes to encapsulate data and functionality, and perform branching with conditionals. Understand the role of each part as you examine the code.
class Database {
constructor() {
if (Database.instance) {
return Database.instance;
}
this.connection = null;
Database.instance = this;
}
connect() {
if (!this.connection) {
this.connection = "DB Connected";
console.log(this.connection);
}
return this.connection;
}
}
// Usage
const db1 = new Database();
const db2 = new Database();
console.log(db1 === db2); // true (same instance)
db1.connect(); // DB Connected
db2.connect(); // (already connected)
Modern Approach (ES6 Module)
The following is a detailed implementation code using JavaScript. Define classes to encapsulate data and functionality, and perform branching with conditionals. Understand the role of each part as you examine the code.
// database.js
class Database {
constructor() {
this.connection = null;
}
connect() {
if (!this.connection) {
this.connection = "DB Connected";
}
return this.connection;
}
}
export default new Database();
// main.js
import db from './database.js';
db.connect();
2. Factory Pattern
Concept
Encapsulates object creation logic for flexible object instantiation.
Implementation
The following is a detailed implementation code using JavaScript. Define classes to encapsulate data and functionality, handle errors for stability, and perform branching with conditionals. Understand the role of each part as you examine the code.
class User {
constructor(name, role) {
this.name = name;
this.role = role;
}
getPermissions() {
return [];
}
}
class Admin extends User {
getPermissions() {
return ['read', 'write', 'delete'];
}
}
class Guest extends User {
getPermissions() {
return ['read'];
}
}
class Member extends User {
getPermissions() {
return ['read', 'write'];
}
}
// Factory
class UserFactory {
static createUser(name, role) {
switch (role) {
case 'admin':
return new Admin(name, role);
case 'member':
return new Member(name, role);
case 'guest':
return new Guest(name, role);
default:
throw new Error(`Unknown role: ${role}`);
}
}
}
// Usage
const admin = UserFactory.createUser("John", "admin");
console.log(admin.getPermissions()); // ['read', 'write', 'delete']
const guest = UserFactory.createUser("Guest", "guest");
console.log(guest.getPermissions()); // ['read']
3. Module Pattern
Concept
Uses encapsulation to distinguish between private variables and public methods.
Implementation (IIFE)
The following is a detailed implementation code using JavaScript. Understand the role of each part as you examine the code.
const Counter = (function() {
// Private variable
let count = 0;
// Private function
function log() {
console.log(`Current count: ${count}`);
}
// Public API
return {
increment() {
count++;
log();
},
decrement() {
count--;
log();
},
getCount() {
return count;
}
};
})();
// Usage
Counter.increment(); // Current count: 1
Counter.increment(); // Current count: 2
console.log(Counter.getCount()); // 2
console.log(Counter.count); // undefined (private)
Modern Approach (ES6 Class)
The following is a detailed implementation code using JavaScript. Define classes to encapsulate data and functionality. Understand the role of each part as you examine the code.
class Counter {
#count = 0; // Private field
increment() {
this.#count++;
this.#log();
}
decrement() {
this.#count--;
this.#log();
}
getCount() {
return this.#count;
}
#log() {
console.log(`Current count: ${this.#count}`);
}
}
const counter = new Counter();
counter.increment(); // Current count: 1
console.log(counter.getCount()); // 1
// console.log(counter.#count); // SyntaxError (private)
4. Observer Pattern
Concept
Notifies subscribers of object state changes.
Implementation
The following is a detailed implementation code using JavaScript. Define classes to encapsulate data and functionality, and process data with loops. Understand the role of each part as you examine the code.
class Subject {
constructor() {
this.observers = [];
}
subscribe(observer) {
this.observers.push(observer);
}
unsubscribe(observer) {
this.observers = this.observers.filter(obs => obs !== observer);
}
notify(data) {
this.observers.forEach(observer => observer.update(data));
}
}
class Observer {
constructor(name) {
this.name = name;
}
update(data) {
console.log(`${this.name} received notification:`, data);
}
}
// Usage
const subject = new Subject();
const observer1 = new Observer("Observer1");
const observer2 = new Observer("Observer2");
subject.subscribe(observer1);
subject.subscribe(observer2);
subject.notify("New data!");
// Observer1 received notification: New data!
// Observer2 received notification: New data!
subject.unsubscribe(observer1);
subject.notify("Another data");
// Observer2 received notification: Another data
Real-world Example: Event System
The following is a detailed implementation code using JavaScript. Define classes to encapsulate data and functionality, implement logic through functions, process data with loops, and perform branching with conditionals. Understand the role of each part as you examine the code.
class EventEmitter {
constructor() {
this.events = {};
}
on(event, listener) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(listener);
}
off(event, listener) {
if (!this.events[event]) return;
this.events[event] = this.events[event].filter(l => l !== listener);
}
emit(event, data) {
if (!this.events[event]) return;
this.events[event].forEach(listener => listener(data));
}
once(event, listener) {
const wrapper = (data) => {
listener(data);
this.off(event, wrapper);
};
this.on(event, wrapper);
}
}
// Usage
const emitter = new EventEmitter();
function onUserLogin(user) {
console.log(`${user.name} logged in`);
}
emitter.on('login', onUserLogin);
emitter.on('login', (user) => {
console.log(`Welcome, ${user.name}!`);
});
emitter.emit('login', { name: 'John' });
// John logged in
// Welcome, John!
// once: Execute only once
emitter.once('logout', (user) => {
console.log(`${user.name} logged out`);
});
emitter.emit('logout', { name: 'John' }); // John logged out
emitter.emit('logout', { name: 'John' }); // (not executed)
5. Proxy Pattern
Concept
Controls access to objects or provides additional functionality.
Implementation (ES6 Proxy)
The following is a detailed implementation code using JavaScript. Handle errors for stability and perform branching with conditionals. Understand the role of each part as you examine the code.
const user = {
name: "John",
age: 25,
email: "[email protected]"
};
const handler = {
get(target, prop) {
console.log(`Reading ${prop}`);
return target[prop];
},
set(target, prop, value) {
console.log(`Setting ${prop} to ${value}`);
// Validation
if (prop === 'age' && typeof value !== 'number') {
throw new TypeError("Age must be a number");
}
target[prop] = value;
return true;
}
};
const proxyUser = new Proxy(user, handler);
console.log(proxyUser.name); // Reading name -> John
proxyUser.age = 26; // Setting age to 26
// proxyUser.age = "26"; // TypeError
Real-world Example: Caching Proxy
The following is a detailed implementation code using JavaScript. Implement logic through functions and perform branching with conditionals. Understand the role of each part as you examine the code.
function createCachedFunction(fn) {
const cache = new Map();
return new Proxy(fn, {
apply(target, thisArg, args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log("Returning from cache");
return cache.get(key);
}
console.log("Computing...");
const result = target.apply(thisArg, args);
cache.set(key, result);
return result;
}
});
}
// Usage
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
const cachedFib = createCachedFunction(fibonacci);
console.log(cachedFib(10)); // Computing....-> 55
console.log(cachedFib(10)); // Returning from cache -> 55
6. Strategy Pattern
Concept
Encapsulates algorithms to allow runtime selection.
Implementation
The following is a detailed implementation code using JavaScript. Define classes to encapsulate data and functionality. Understand the role of each part as you examine the code.
// Strategies
class CreditCardStrategy {
pay(amount) {
console.log(`Paying ${amount} with Credit Card`);
}
}
class PayPalStrategy {
pay(amount) {
console.log(`Paying ${amount} with PayPal`);
}
}
class CryptoStrategy {
pay(amount) {
console.log(`Paying ${amount} with Cryptocurrency`);
}
}
// Context
class PaymentContext {
constructor(strategy) {
this.strategy = strategy;
}
setStrategy(strategy) {
this.strategy = strategy;
}
executePayment(amount) {
this.strategy.pay(amount);
}
}
// Usage
const payment = new PaymentContext(new CreditCardStrategy());
payment.executePayment(10000); // Paying 10000 with Credit Card
payment.setStrategy(new PayPalStrategy());
payment.executePayment(20000); // Paying 20000 with PayPal
7. Real-world Example: State Management
The following is a detailed implementation code using JavaScript. Define classes to encapsulate data and functionality, and process data with loops. Understand the role of each part as you examine the code.
class Store {
constructor(initialState = {}) {
this.state = initialState;
this.listeners = [];
}
getState() {
return this.state;
}
setState(newState) {
this.state = { ...this.state, ...newState };
this.notify();
}
subscribe(listener) {
this.listeners.push(listener);
return () => {
this.listeners = this.listeners.filter(l => l !== listener);
};
}
notify() {
this.listeners.forEach(listener => listener(this.state));
}
}
// Usage
const store = new Store({ count: 0, user: null });
const unsubscribe = store.subscribe((state) => {
console.log("State changed:", state);
});
store.setState({ count: 1 });
// State changed: { count: 1, user: null }
store.setState({ user: { name: "John" } });
// State changed: { count: 1, user: { name: "John" } }
unsubscribe(); // Unsubscribe
Summary
Key Takeaways
- Singleton: Single instance
- Factory: Encapsulate object creation
- Module: Separate private/public
- Observer: Notify state changes
- Proxy: Control access
- Strategy: Swap algorithms
Next Steps
- TypeScript Getting Started
- React Design Patterns
- Node.js Design Patterns
Related Articles
- JavaScript Getting Started | Complete Introduction to Web Development
- JavaScript Variables and Data Types | let, const, var Complete Guide
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. JavaScript design patterns: Singleton, Factory, Observer patterns. Learn principles, code implementation, and practical … 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. JavaScript 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- JavaScript 클래스 | ES6 Class 문법 완벽 정리
- JavaScript 모듈 | ES6 Modules, CommonJS 완벽 정리
- C++ 디자인 패턴 종합 가이드 | Singleton·Factory
- C++ 디자인 패턴 | Singleton·Factory·Builder·Prototype 생성 패턴 가이드
- C++ Observer Pattern 완벽 가이드 | 이벤트 기반 아키텍처와 신호/슬롯
- Python 데코레이터 | @decorator 완벽 정리
이 글에서 다루는 키워드 (관련 검색어)
JavaScript, Design Patterns, Singleton, Factory, Observer, Proxy 등으로 검색하시면 이 글이 도움이 됩니다.