← Курс/TypeScript Design Patterns#196 из 257+35 XP

TypeScript Design Patterns

Builder Pattern

Builder позволяет пошагово конструировать сложные объекты. TypeScript делает его особенно полезным — каждый метод возвращает this с правильным типом:

class QueryBuilder<T> {
  private table = ''
  private conditions: string[] = []
  private selectedFields: string[] = ['*']
  private limitValue?: number

  from(table: string): this {
    this.table = table
    return this
  }

  select(...fields: string[]): this {
    this.selectedFields = fields
    return this
  }

  where(condition: string): this {
    this.conditions.push(condition)
    return this
  }

  limit(n: number): this {
    this.limitValue = n
    return this
  }

  build(): string {
    let query = `SELECT ${this.selectedFields.join(', ')} FROM ${this.table}`
    if (this.conditions.length) query += ` WHERE ${this.conditions.join(' AND ')}`
    if (this.limitValue) query += ` LIMIT ${this.limitValue}`
    return query
  }
}

const query = new QueryBuilder<User>()
  .from('users')
  .select('id', 'name', 'email')
  .where('age > 18')
  .where('active = true')
  .limit(10)
  .build()

Factory Pattern

Factory централизует создание объектов, скрывая детали реализации:

interface Logger {
  log(message: string): void
  error(message: string): void
}

class ConsoleLogger implements Logger {
  log(message: string) { console.log(`[INFO] ${message}`) }
  error(message: string) { console.error(`[ERROR] ${message}`) }
}

class FileLogger implements Logger {
  constructor(private filename: string) {}
  log(message: string) { /* запись в файл */ }
  error(message: string) { /* запись в файл */ }
}

class NullLogger implements Logger {
  log() {}
  error() {}
}

type LoggerType = 'console' | 'file' | 'null'

function createLogger(type: LoggerType, options?: { filename?: string }): Logger {
  switch (type) {
    case 'console': return new ConsoleLogger()
    case 'file':    return new FileLogger(options?.filename ?? 'app.log')
    case 'null':    return new NullLogger()
  }
}

Observer Pattern

type EventMap = Record<string, unknown>

class EventEmitter<Events extends EventMap> {
  private listeners = new Map<keyof Events, Set<Function>>()

  on<K extends keyof Events>(event: K, listener: (data: Events[K]) => void): this {
    if (!this.listeners.has(event)) this.listeners.set(event, new Set())
    this.listeners.get(event)!.add(listener)
    return this
  }

  off<K extends keyof Events>(event: K, listener: (data: Events[K]) => void): this {
    this.listeners.get(event)?.delete(listener)
    return this
  }

  emit<K extends keyof Events>(event: K, data: Events[K]): this {
    this.listeners.get(event)?.forEach(listener => listener(data))
    return this
  }
}

// Строго типизированные события:
interface StoreEvents {
  change: { key: string; value: unknown }
  reset: void
}

const emitter = new EventEmitter<StoreEvents>()
emitter.on('change', ({ key, value }) => console.log(key, value))

Repository Pattern

Repository абстрагирует доступ к данным:

interface Repository<T, ID = number> {
  findById(id: ID): Promise<T | null>
  findAll(): Promise<T[]>
  findWhere(predicate: (item: T) => boolean): Promise<T[]>
  save(item: T): Promise<T>
  delete(id: ID): Promise<void>
}

class InMemoryUserRepository implements Repository<User> {
  private users = new Map<number, User>()

  async findById(id: number): Promise<User | null> {
    return this.users.get(id) ?? null
  }

  async findAll(): Promise<User[]> {
    return [...this.users.values()]
  }

  async findWhere(predicate: (user: User) => boolean): Promise<User[]> {
    return [...this.users.values()].filter(predicate)
  }

  async save(user: User): Promise<User> {
    this.users.set(user.id, user)
    return user
  }

  async delete(id: number): Promise<void> {
    this.users.delete(id)
  }
}

Singleton Pattern

class Database {
  private static instance: Database | null = null
  private constructor(private url: string) {}

  static getInstance(url: string): Database {
    if (!Database.instance) {
      Database.instance = new Database(url)
    }
    return Database.instance
  }
}

Примеры

Реализация Builder, Factory, Observer и Repository паттернов в JS с демонстрацией практических use cases

// Четыре ключевых паттерна с TypeScript-стилем в чистом JS.

// ============================================================
// 1. BUILDER — QueryBuilder для SQL-подобных запросов
// ============================================================
class QueryBuilder {
  constructor() {
    this._table = ''
    this._fields = ['*']
    this._conditions = []
    this._orderBy = null
    this._limitValue = null
    this._joins = []
  }

  from(table) { this._table = table; return this }
  select(...fields) { this._fields = fields; return this }
  where(condition) { this._conditions.push(condition); return this }
  join(table, on) { this._joins.push(`JOIN ${table} ON ${on}`); return this }
  orderBy(field, dir = 'ASC') { this._orderBy = `${field} ${dir}`; return this }
  limit(n) { this._limitValue = n; return this }

  build() {
    if (!this._table) throw new Error('Table is required')
    let q = `SELECT ${this._fields.join(', ')} FROM ${this._table}`
    if (this._joins.length) q += ' ' + this._joins.join(' ')
    if (this._conditions.length) q += ` WHERE ${this._conditions.join(' AND ')}`
    if (this._orderBy) q += ` ORDER BY ${this._orderBy}`
    if (this._limitValue !== null) q += ` LIMIT ${this._limitValue}`
    return q
  }
}

console.log('=== Builder Pattern ===')
const query = new QueryBuilder()
  .from('users')
  .select('users.id', 'users.name', 'orders.total')
  .join('orders', 'users.id = orders.user_id')
  .where('users.active = true')
  .where('orders.total > 1000')
  .orderBy('orders.total', 'DESC')
  .limit(20)
  .build()
console.log(query)

// ============================================================
// 2. FACTORY — Logger с разными стратегиями
// ============================================================
class ConsoleLogger {
  constructor(prefix = '') { this.prefix = prefix }
  log(msg) { console.log(`  [${this.prefix || 'INFO'}] ${msg}`) }
  error(msg) { console.log(`  [${this.prefix || 'ERROR'}] ${msg}`) }
  warn(msg) { console.log(`  [${this.prefix || 'WARN'}] ${msg}`) }
}

class BufferedLogger {
  constructor() { this.buffer = [] }
  log(msg) { this.buffer.push({ level: 'info', msg, time: Date.now() }) }
  error(msg) { this.buffer.push({ level: 'error', msg, time: Date.now() }) }
  warn(msg) { this.buffer.push({ level: 'warn', msg, time: Date.now() }) }
  flush() {
    const logs = [...this.buffer]
    this.buffer = []
    return logs
  }
}

class NullLogger {
  log() {} error() {} warn() {}
}

function createLogger(type, options = {}) {
  switch (type) {
    case 'console': return new ConsoleLogger(options.prefix)
    case 'buffered': return new BufferedLogger()
    case 'null': return new NullLogger()
    default: throw new Error('Unknown logger type: ' + type)
  }
}

console.log('\n=== Factory Pattern ===')
const logger = createLogger('console', { prefix: 'APP' })
logger.log('Приложение запущено')
logger.warn('Медленный запрос')

const buffered = createLogger('buffered')
buffered.log('Event 1')
buffered.error('Something failed')
buffered.log('Event 2')
console.log('  Buffered logs:', buffered.flush())

// ============================================================
// 3. OBSERVER — типизированный EventEmitter
// ============================================================
class TypedEventEmitter {
  constructor() { this._listeners = new Map() }

  on(event, listener) {
    if (!this._listeners.has(event)) this._listeners.set(event, new Set())
    this._listeners.get(event).add(listener)
    return this
  }

  off(event, listener) {
    this._listeners.get(event)?.delete(listener)
    return this
  }

  once(event, listener) {
    const wrapper = (data) => { listener(data); this.off(event, wrapper) }
    return this.on(event, wrapper)
  }

  emit(event, data) {
    this._listeners.get(event)?.forEach(fn => fn(data))
    return this
  }
}

console.log('\n=== Observer Pattern ===')
const store = new TypedEventEmitter()

store.on('change', ({ key, value }) =>
  console.log(`  [change] ${key} = ${JSON.stringify(value)}`)
)
store.once('reset', () => console.log('  [reset] store was reset'))

store.emit('change', { key: 'user', value: { name: 'Алексей' } })
store.emit('change', { key: 'theme', value: 'dark' })
store.emit('reset')
store.emit('reset')  // не сработает — once уже отработал

// ============================================================
// 4. REPOSITORY — InMemory с типизированным поиском
// ============================================================
class InMemoryRepository {
  constructor() { this._store = new Map(); this._nextId = 1 }

  async save(item) {
    const saved = { ...item, id: item.id ?? this._nextId++ }
    this._store.set(saved.id, saved)
    return saved
  }

  async findById(id) {
    return this._store.get(id) ?? null
  }

  async findAll() {
    return [...this._store.values()]
  }

  async findWhere(predicate) {
    return [...this._store.values()].filter(predicate)
  }

  async delete(id) {
    this._store.delete(id)
  }

  async count() { return this._store.size }
}

console.log('\n=== Repository Pattern ===')
const userRepo = new InMemoryRepository()

async function demo() {
  await userRepo.save({ name: 'Алексей', role: 'admin', age: 30 })
  await userRepo.save({ name: 'Мария', role: 'user', age: 25 })
  await userRepo.save({ name: 'Иван', role: 'user', age: 17 })

  const all = await userRepo.findAll()
  console.log('  All users:', all.map(u => u.name))

  const admins = await userRepo.findWhere(u => u.role === 'admin')
  console.log('  Admins:', admins.map(u => u.name))

  const adults = await userRepo.findWhere(u => u.age >= 18)
  console.log('  Adults:', adults.map(u => u.name))

  const user = await userRepo.findById(2)
  console.log('  User #2:', user?.name)

  await userRepo.delete(2)
  console.log('  After delete count:', await userRepo.count())
}

demo()