OOP Best Practices
Follow these best practices to write clean, maintainable, and effective object-oriented code.
1. Single Responsibility Principle
A class should have only one reason to change - it should do one thing and do it well.
// ❌ BAD: Class doing too much
class User {
constructor(
public name: string,
public email: string
) {}
save(): void {
// Database logic - NOT the user's responsibility
console.log("Saving to database...");
}
sendEmail(message: string): void {
// Email logic - NOT the user's responsibility
console.log(`Sending email to ${this.email}: ${message}`);
}
generateReport(): string {
// Reporting logic - NOT the user's responsibility
return `User Report for ${this.name}`;
}
}
// ✅ GOOD: Separate responsibilities
class User {
constructor(
public name: string,
public email: string
) {}
getFullName(): string {
return this.name;
}
}
class UserRepository {
save(user: User): void {
console.log(`Saving user: ${user.name}`);
// Database logic here
}
find(email: string): User | null {
// Find logic here
return null;
}
}
class EmailService {
send(to: string, message: string): void {
console.log(`Sending email to ${to}: ${message}`);
// Email logic here
}
}
class UserReportGenerator {
generate(user: User): string {
return `User Report for ${user.name}`;
// Reporting logic here
}
}2. Favor Composition Over Inheritance
Use composition to combine behaviors rather than creating deep inheritance hierarchies.
// ❌ BAD: Deep inheritance
class Animal {}
class Bird extends Animal {}
class FlyingBird extends Bird {}
class SwimmingFlyingBird extends FlyingBird {} // Getting complex!
// ✅ GOOD: Composition
interface Flyable {
fly(): void;
}
interface Swimmable {
swim(): void;
}
class Duck implements Flyable, Swimmable {
fly(): void {
console.log("Duck flying");
}
swim(): void {
console.log("Duck swimming");
}
}
class Penguin implements Swimmable {
swim(): void {
console.log("Penguin swimming");
}
}
class Eagle implements Flyable {
fly(): void {
console.log("Eagle soaring");
}
}3. Program to Interfaces, Not Implementations
Depend on abstractions rather than concrete implementations.
// ❌ BAD: Depending on concrete class
class MySQLDatabase {
query(sql: string): any[] {
return [];
}
}
class UserService {
private db: MySQLDatabase; // Tightly coupled
constructor() {
this.db = new MySQLDatabase();
}
getUsers(): any[] {
return this.db.query("SELECT * FROM users");
}
}
// ✅ GOOD: Depending on interface
interface Database {
query(sql: string): any[];
}
class MySQLDatabase implements Database {
query(sql: string): any[] {
console.log("MySQL query:", sql);
return [];
}
}
class PostgreSQLDatabase implements Database {
query(sql: string): any[] {
console.log("PostgreSQL query:", sql);
return [];
}
}
class UserService {
constructor(private db: Database) {} // Flexible!
getUsers(): any[] {
return this.db.query("SELECT * FROM users");
}
}
// Easy to swap implementations
const service1 = new UserService(new MySQLDatabase());
const service2 = new UserService(new PostgreSQLDatabase());4. Keep Classes Small and Focused
If a class has too many methods or properties, it probably needs to be split.
// ❌ BAD: God class doing everything
class OrderManager {
createOrder() {}
updateOrder() {}
deleteOrder() {}
calculateTotal() {}
applyDiscount() {}
processPayment() {}
sendConfirmationEmail() {}
updateInventory() {}
generateInvoice() {}
calculateShipping() {}
// ... too many responsibilities!
}
// ✅ GOOD: Focused classes
class Order {
constructor(
public id: string,
public items: OrderItem[],
public customerId: string
) {}
calculateTotal(): number {
return this.items.reduce((sum, item) => sum + item.getTotal(), 0);
}
}
class OrderRepository {
save(order: Order): void {}
find(id: string): Order | null { return null; }
delete(id: string): void {}
}
class DiscountService {
apply(order: Order, code: string): number { return 0; }
}
class PaymentProcessor {
process(order: Order, method: string): boolean { return true; }
}
class OrderNotificationService {
sendConfirmation(order: Order): void {}
}
class InventoryService {
update(order: Order): void {}
}
class InvoiceGenerator {
generate(order: Order): string { return ""; }
}
class ShippingCalculator {
calculate(order: Order): number { return 0; }
}5. Use Meaningful Names
Classes, methods, and properties should have clear, descriptive names.
// ❌ BAD: Unclear names
class Mgr {
private d: Data[];
proc(): void {}
get(): Data[] { return this.d; }
}
// ✅ GOOD: Clear names
class UserManager {
private users: User[];
processUserRegistration(): void {}
getAllUsers(): User[] { return this.users; }
findUserByEmail(email: string): User | null { return null; }
}
// Class names should be nouns
class PaymentProcessor {} // Good
class ProcessPayment {} // Bad (verb)
// Method names should be verbs
getUser() // Good
createOrder() // Good
user() // Bad (noun)
order() // Bad (noun)6. Minimize Coupling
Classes should depend on as few other classes as possible.
// ❌ BAD: Tight coupling
class EmailService {
send(to: string, message: string): void {
const smtp = new SMTPClient("smtp.gmail.com", 587);
smtp.connect();
smtp.send(to, message);
smtp.disconnect();
}
}
// ✅ GOOD: Loose coupling through dependency injection
interface MailTransport {
send(to: string, message: string): void;
}
class SMTPTransport implements MailTransport {
constructor(
private host: string,
private port: number
) {}
send(to: string, message: string): void {
console.log(`SMTP: Sending to ${to}`);
}
}
class SendGridTransport implements MailTransport {
constructor(private apiKey: string) {}
send(to: string, message: string): void {
console.log(`SendGrid: Sending to ${to}`);
}
}
class EmailService {
constructor(private transport: MailTransport) {}
send(to: string, message: string): void {
this.transport.send(to, message);
}
}
// Easy to test and swap implementations
const emailService = new EmailService(
new SMTPTransport("smtp.gmail.com", 587)
);7. Follow the Law of Demeter (Principle of Least Knowledge)
Don't talk to strangers - a method should only call methods on: itself, its parameters, objects it creates, or its direct fields.
// ❌ BAD: Violates Law of Demeter (train wreck)
const userName = order.getCustomer().getAddress().getCity().getName();
// ✅ GOOD: Ask, don't dig
class Order {
getCustomerCityName(): string {
return this.customer.getCityName();
}
}
class Customer {
getCityName(): string {
return this.address.getCityName();
}
}
const userName = order.getCustomerCityName();Summary
Keep classes small and focused on a single responsibility
Favor composition over inheritance for flexibility
Program to interfaces to reduce coupling
Use meaningful, descriptive names
Minimize dependencies between classes
Encapsulate what varies
Follow the Law of Demeter
Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free