December 20, 2025 (8mo ago) — last updated July 30, 2026 (1mo ago)

Патерни проєктування в ООП: практичний посібник

Вивчіть креаційні, структурні та поведінкові патерни з практичними прикладами TypeScript, щоб писати підтримуваний та розширюваний код.

← Back to blog
Cover Image for Патерни проєктування в ООП: практичний посібник

Design patterns are adaptable blueprints that solve common design problems in OOP. Learn core creational, structural, and behavioral patterns with practical TypeScript examples to write more maintainable, testable, and extensible code.

Design Patterns in OOP: Practical Guide

Master design patterns in object‑oriented programming with clear, practical TypeScript examples. Learn creational, structural, and behavioral patterns so you can write maintainable, testable, and extensible code.

Introduction

Design patterns are proven templates for solving recurring design problems in object‑oriented systems. They’re not copy‑and‑paste solutions but adaptable blueprints that help you structure classes and objects to make code easier to maintain, extend, and test. A practical grasp of creational, structural, and behavioral patterns helps you communicate design intent, refactor legacy code safely, and choose the right approach for a given problem.

What Are Design Patterns in Object‑Oriented Programming?

Trying to build complex software without a common vocabulary or proven approaches often leads to fragile code. Design patterns act like a cookbook of tested recipes that teams can reuse and adapt to produce consistent, maintainable results.

Ескіз кухарського ковпака, який кидає золоті літерні форми в розкриту книгу рецептів.

A Shared Developer Language

Patterns provide a compact, shared vocabulary. Say “Factory” or “Singleton” and experienced developers immediately understand intent and trade‑offs, which speeds collaboration and architecture decisions.

“Design Patterns: Elements of Reusable Object‑Oriented Software” catalogued 23 foundational patterns that remain essential reading for OOP practitioners2. Earlier academic work also documents measurable productivity and reuse benefits from pattern abstraction and reuse1.

“A design pattern is not a finished design that can be transformed directly into code. It is a description or template for how to solve a problem that can be used in many different situations.”

Getting comfortable with core OOP principles like polymorphism and inheritance makes it easier to apply patterns effectively. For a practical refresher, see our guide comparing polymorphism vs inheritance: https://cleancodeguy.com/blog/polymorphism-vs-inheritance.

The Three Core Categories of Design Patterns

The Gang of Four grouped patterns into three categories: creational, structural, and behavioral. Knowing these categories helps you quickly pick the right approach.

Діаграма, що ілюструє креаційні, структурні та поведінкові патерни проєктування з простими коробковими ілюстраціями.

Overview of OOP Design Pattern Categories

Pattern CategoryCore PurposeCommon Examples
CreationalAbstracting and managing object creationFactory, Builder, Singleton, Prototype
StructuralComposing classes and objects into larger, flexible structuresAdapter, Decorator, Facade, Composite
BehavioralDefining how objects interact and communicateObserver, Strategy, Command, Iterator

Creational Patterns: Construction Specialists

Creational patterns control how objects are created so client code isn’t tightly coupled to concrete classes. They hide creation logic, increase flexibility, and let you manage instantiation (for example, enforcing a single instance with Singleton).

Structural Patterns: Architectural Glue

Structural patterns help assemble objects into larger systems. They simplify relationships between components so parts can change without breaking the whole. Adapter lets incompatible interfaces cooperate, which is valuable when integrating third‑party libraries.

Structural patterns simplify system design by identifying simple ways to realize relationships between entities.

Behavioral Patterns: Communication Directors

Behavioral patterns govern object interaction and responsibility. They create clear communication channels—Observer for event notifications and Strategy for swapping algorithms without changing clients. Managing these communication paths reduces dependencies and improves maintainability.

Creating Objects with Creational Patterns

Creational patterns introduce an abstraction layer around object creation, so you can swap implementations without touching client code. Below are two practical TypeScript examples: Singleton and Factory Method.

Ілюстрація, що контрастує індивідуальні об'єктоподібні «яйця» з фабричною лінією, що виробляє кольорові предмети.

Singleton: Ensuring One True Instance

Singleton ensures a class has only one instance and provides a global access point. It’s useful for shared resources like a database connection or logger, but it can introduce global state that complicates testing and dependency management3.

Example: a TypeScript Singleton for a database connection.

class DatabaseConnection {
  private static instance: DatabaseConnection;

  private constructor() {
    // A private constructor prevents external 'new' calls
    console.log("Connecting to the database...");
  }

  public static getInstance(): DatabaseConnection {
    if (!DatabaseConnection.instance) {
      DatabaseConnection.instance = new DatabaseConnection();
    }
    return DatabaseConnection.instance;
  }

  public query(sql: string): void {
    console.log(`Executing query: ${sql}`);
  }
}

// Usage
const db1 = DatabaseConnection.getInstance();
const db2 = DatabaseConnection.getInstance();

db1.query("SELECT * FROM users");
console.log(db1 === db2); // true

Singleton trade‑off: use it for truly global resources; prefer dependency injection for better testability and modularity3.

Factory Method: Letting Subclasses Decide What to Create

Factory Method defines an interface for creating an object while subclasses decide which concrete product to instantiate. It decouples client code from concrete classes and simplifies extension.

Example: rendering OS‑specific buttons in TypeScript.

interface Button {
  render(): void;
  onClick(f: () => void): void;
}

class WindowsButton implements Button {
  render() { console.log("Rendering a button in Windows style."); }
  onClick(f: () => void) { console.log("Windows button click event."); f(); }
}

class MacButton implements Button {
  render() { console.log("Rendering a button in macOS style."); }
  onClick(f: () => void) { console.log("Mac button click event."); f(); }
}

abstract class Dialog {
  abstract createButton(): Button;

  render() {
    const okButton = this.createButton();
    okButton.render();
  }
}

class WindowsDialog extends Dialog {
  createButton(): Button { return new WindowsButton(); }
}

class MacDialog extends Dialog {
  createButton(): Button { return new MacButton(); }
}

// Client
const os: string = "windows";
let dialog: Dialog;
if (os === "windows") dialog = new WindowsDialog(); else dialog = new MacDialog();
dialog.render();

Factory Method keeps creators unaware of concrete products, so adding a LinuxDialog is straightforward.

Building Flexible Systems with Structural Patterns

Structural patterns help you compose objects into robust, adaptable systems. Two practical choices are Adapter and Decorator.

Накреслені вручну ілюстрації електричного адаптера та стопки кілець, що представляють патерни проєктування ООП.

Adapter: Bridging Incompatible Interfaces

Adapter wraps an incompatible interface so it conforms to what your system expects. This avoids invasive changes to legacy code.

Example: adapting a ModernLogger to an existing ILogger interface.

class ModernLogger {
  public logInfo(message: string): void {
    console.log(`[INFO]: ${message}`);
  }
}

interface ILogger { log(message: string): void; }

class LoggerAdapter implements ILogger {
  private modernLogger: ModernLogger;
  constructor() { this.modernLogger = new ModernLogger(); }
  public log(message: string): void { this.modernLogger.logInfo(message); }
}

const logger: ILogger = new LoggerAdapter();
logger.log("User logged in successfully.");

Decorator: Adding Functionality Dynamically

Decorator adds responsibilities to objects at runtime by wrapping them. It’s more flexible than subclassing and follows the Single Responsibility Principle.

Example: composing a subscription with optional add‑ons.

interface Subscription { getDescription(): string; getCost(): number; }

class BasicSubscription implements Subscription {
  getDescription(): string { return "Basic Plan"; }
  getCost(): number { return 10; }
}

abstract class SubscriptionDecorator implements Subscription {
  protected subscription: Subscription;
  constructor(subscription: Subscription) { this.subscription = subscription; }
  abstract getDescription(): string;
  abstract getCost(): number;
}

class PremiumSupportDecorator extends SubscriptionDecorator {
  getDescription(): string { return `${this.subscription.getDescription()}, Premium Support`; }
  getCost(): number { return this.subscription.getCost() + 5; }
}

class CloudStorageDecorator extends SubscriptionDecorator {
  getDescription(): string { return `${this.subscription.getDescription()}, 1TB Cloud Storage`; }
  getCost(): number { return this.subscription.getCost() + 7; }
}

let mySubscription: Subscription = new BasicSubscription();
mySubscription = new PremiumSupportDecorator(mySubscription);
mySubscription = new CloudStorageDecorator(mySubscription);
console.log(mySubscription.getDescription());
console.log(mySubscription.getCost());

This compositional approach keeps features modular and easy to test.

Managing Interactions with Behavioral Patterns

Behavioral patterns organize object communication so systems remain flexible and maintainable. Two core examples are Observer and Strategy.

Observer: Notifying Interested Parties

Observer sets up a one‑to‑many relationship so when a Subject changes state, Observers are notified automatically. It’s ideal for event‑driven systems.

Example: a simple notification service.

interface Subject { attach(observer: Observer): void; detach(observer: Observer): void; notify(): void; }
interface Observer { update(subject: Subject): void; }

class NotificationService implements Subject {
  public state: string = '';
  private observers: Observer[] = [];
  attach(observer: Observer): void { this.observers.push(observer); }
  detach(observer: Observer): void { const i = this.observers.indexOf(observer); if (i !== -1) this.observers.splice(i, 1); }
  notify(): void { for (const o of this.observers) o.update(this); }
  public createNewPost(title: string): void { this.state = `New Post: ${title}`; console.log(`\nNotificationService: A new post was created.`); this.notify(); }
}

class EmailNotifier implements Observer { public update(subject: Subject): void { if (subject instanceof NotificationService) console.log(`EmailNotifier: Sending email about "${subject.state}"`); } }
class PushNotifier implements Observer { public update(subject: Subject): void { if (subject instanceof NotificationService) console.log(`PushNotifier: Sending push notification for "${subject.state}"`); } }

const notificationService = new NotificationService();
const emailer = new EmailNotifier();
const pusher = new PushNotifier();
notificationService.attach(emailer);
notificationService.attach(pusher);
notificationService.createNewPost("Understanding Observer Pattern");
notificationService.detach(pusher);
notificationService.createNewPost("Why Strategy is Awesome");

Observer produces a loosely coupled design: Subjects don’t need to know the concrete Observers they notify.

Strategy: Encapsulating Algorithms

Strategy lets you swap algorithms at runtime, avoiding large conditional blocks. It aligns with the Open/Closed Principle by allowing new strategies without modifying existing code4.

Example: payment strategies for a shopping cart.

interface PaymentStrategy { pay(amount: number): void; }
class CreditCardStrategy implements PaymentStrategy { pay(amount: number): void { console.log(`Paying $${amount} with Credit Card.`); } }
class PayPalStrategy implements PaymentStrategy { pay(amount: number): void { console.log(`Paying $${amount} via PayPal.`); } }

class ShoppingCart {
  private paymentStrategy: PaymentStrategy;
  constructor(strategy: PaymentStrategy) { this.paymentStrategy = strategy; }
  public setPaymentStrategy(strategy: PaymentStrategy) { this.paymentStrategy = strategy; }
  public checkout(amount: number): void { this.paymentStrategy.pay(amount); }
}

const cart = new ShoppingCart(new CreditCardStrategy());
cart.checkout(150);
cart.setPaymentStrategy(new PayPalStrategy());
cart.checkout(150);

Strategy reduces conditional complexity and makes systems easier to extend and test.

Common Design Pitfalls and Refactoring Strategies

Choosing a pattern is only half the battle. Avoid anti‑patterns like the God Object, which hoards responsibilities and violates the Single Responsibility Principle. Watch for code smells—long methods, excessive dependencies, or monster classes—and address them with disciplined refactoring.

Refactoring improves internal structure without changing external behavior; it’s how you tame legacy systems safely.

Refactoring to Strategy

A large switch or long if/else chain is a classic smell. Extract varying behavior into a Strategy interface with concrete classes, and replace conditionals with a single call to the selected strategy. This improves extensibility, testability, and alignment with SOLID principles4.

Frequently Asked Questions

Q: Which patterns should I learn first?

Start with 5–7 essential patterns: Singleton, Factory, Adapter, Decorator, Observer, and Strategy. Focus on the problem each pattern solves rather than memorizing structure.

Q: When should I avoid using a pattern?

Avoid patterns that add unnecessary complexity. Don’t introduce a pattern “just in case.” Follow YAGNI: add a pattern when a clear, present problem justifies it.

Q: Can design patterns work with functional programming?

Yes. Many patterns map naturally to functional techniques—Strategy can be function parameters and Decorator can be higher‑order functions. Apply the underlying principle rather than rigidly copying OOP forms.


At Clean Code Guy, we help teams build software that lasts by embedding foundational principles into their workflow. Discover how our code audits and AI‑ready refactoring can get your team shipping with confidence at https://cleancodeguy.com.

1.
W. G. Griswold et al., “A Formal Foundation for Design Pattern Abstraction and Reuse,” Proceedings of ECOOP ’93, University of California, San Diego. https://cseweb.ucsd.edu/~wgg/CSE210/ecoop93-patterns.pdf
2.
Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, Design Patterns: Elements of Reusable Object‑Oriented Software (Addison‑Wesley, 1994). https://en.wikipedia.org/wiki/Design_Patterns
3.
Martin Fowler, “Singleton,” Bliki. Discusses Singleton trade‑offs and testing implications. https://martinfowler.com/bliki/Singleton.html
4.
Robert C. Martin (Uncle Bob), “The SOLID Principles,” which explain Open/Closed and related design goals. https://8thlight.com/blog/uncle-bob/2012/08/13/the-s-o-l-i-d-principles.html
← Back to blog
🙋🏻‍♂️

ШІ пише код.
Ви робите його довговічним.

В епоху прискорення ШІ чистий код — це не просто хороша практика — це різниця між системами, які масштабуються, та кодовими базами, які руйнуються під власною вагою.

Патерни проєктування в ООП: практичний посібник | Clean Code Guy