Introduction
Object-Oriented Programming (OOP) is not just about using classes and objects—it's about designing your software in a modular, extensible, and maintainable way. Good OOP design helps reduce code duplication, increases flexibility, and makes it easier to debug and test your software.
1. Use Encapsulation to Protect Data
Encapsulation means restricting direct access to some of an object's components, which is achieved by making variables private and exposing only necessary methods.
class BankAccount:
private balance
method initialize(amount):
balance = amount
method deposit(amount):
if amount > 0:
balance = balance + amount
method getBalance():
return balance
Output:
Encapsulated balance value is only accessible through methods.
Why is encapsulation important?
It helps prevent accidental changes to critical data and enforces clear boundaries within your system.
Question:
What could go wrong if we allowed direct access to balance
?
Answer: External code might modify it arbitrarily, like setting it to a negative value, which breaks the logic of a real-world bank account.
2. Apply the Single Responsibility Principle (SRP)
Every class should have only one responsibility. Mixing multiple concerns into a single class makes code harder to maintain.
class Invoice:
method calculateTotal():
...
class InvoicePrinter:
method print(invoice):
...
class InvoiceSaver:
method saveToDatabase(invoice):
...
Output:
Each class handles only one task: calculation, printing, or saving.
3. Favor Composition over Inheritance
Rather than relying solely on inheritance, try to design your classes by combining simpler behaviors through composition.
class Engine:
method start():
...
class Car:
has engine
method initialize():
engine = new Engine()
method startCar():
engine.start()
Output:
Car uses Engine by composition rather than extending Engine class.
Question:
Why can composition be more flexible than inheritance?
Answer: It allows you to dynamically combine behaviors at runtime and avoids tight coupling of class hierarchies.
4. Open/Closed Principle
Classes should be open for extension but closed for modification. You should be able to add new functionality without changing existing code.
abstract class Shape:
method draw()
class Circle inherits Shape:
method draw():
...
class Square inherits Shape:
method draw():
...
method drawAll(shapes):
for each shape in shapes:
shape.draw()
Output:
New shapes can be added without modifying drawAll logic.
5. Interface Segregation Principle
Don't force classes to implement methods they don’t use. Instead, split large interfaces into smaller, specific ones.
interface Printable:
method print()
interface Scannable:
method scan()
class Printer implements Printable:
method print():
...
class Scanner implements Scannable:
method scan():
...
Output:
Each device implements only the operations it needs.
6. Dependency Inversion Principle
Depend on abstractions, not on concrete implementations. This makes code more testable and decoupled.
interface NotificationService:
method send(message)
class EmailService implements NotificationService:
method send(message):
...
class App:
has notificationService
method initialize(service):
notificationService = service
method notifyUser(msg):
notificationService.send(msg)
Output:
App is independent of how messages are sent.
7. Don't Repeat Yourself (DRY)
Duplicate code is a source of bugs. If you find yourself copying and pasting code, extract it into a reusable method or class.
method calculateTax(amount):
return amount * 0.10
method processOrder(orderAmount):
tax = calculateTax(orderAmount)
...
Output:
Tax calculation logic is reused across multiple parts of the codebase.
Final Thoughts
Following these OOP design best practices will help you build scalable, testable, and easy-to-maintain software. As you grow in your programming journey, try to review your designs through these principles regularly.
Summary Checklist
- Encapsulate everything that changes.
- Keep each class focused on one responsibility.
- Use composition instead of deep inheritance.
- Code against abstractions, not implementations.
- Keep interfaces lean and purpose-specific.
Practice Question
Imagine you have a class that reads data from files, parses the content, and prints it. How would you refactor it using the Single Responsibility Principle?
Answer: Create three classes: FileReader
for reading, Parser
for parsing, and Printer
for outputting the data.