Mastering Object-Oriented Programming: Best Practices and Design Patterns
Exploring the Core Principles of OOP Through Python Examples
Introduction
Object-oriented programming (OOP) is a paradigm that uses “objects” — data structures that consist of data fields and methods together with their interactions — to design applications and computer programs. Mastering OOP involves not just understanding the theory behind it but also adopting the best practices and design patterns that make your code efficient, reusable, and easy to maintain. This article outlines the best practices and design patterns for mastering object-oriented programming.
Best Practices
Some best practice examples would include: encapsulation, inheritance, polymorphism, proper class design, and composition over inheritance. Here are more details about each one of them:
- Encapsulation: This practice involves hiding the implementation details of an object. This can be done in Python using private members with double underscore prefixes.
class BankAccount:
def __init__(self):
self.__account_number = "123456789"
self.__balance = 0.0
def deposit(self, amount):
self.__balance += amount
def withdraw(self, amount):
if amount > self.__balance:
print("Insufficient balance!")
return
self.__balance -= amount
def get_balance(self):
return self.__balance
In this example, the bank account number and balance are private to the BankAccount
class and can't be accessed directly from outside the class.
- Inheritance: Inheritance is used to create a new class that has all the properties and behaviours of another class, with the potential to add or override them.
class Vehicle:
def __init__(self, color, wheels):
self.color = color
self.wheels = wheels
class Car(Vehicle):
def __init__(self, color, wheels, brand):
super().__init__(color, wheels)
self.brand = brand
Here, Car
is a subclass of Vehicle
, inheriting the color
and wheels
properties and adding a brand
property.
- Polymorphism: This principle allows methods to act differently based on the object type they are acting on.
class Dog:
def make_sound(self):
return "Woof!"
class Cat:
def make_sound(self):
return "Meow!"
def animal_sound(animal):
print(animal.make_sound())
dog = Dog()
cat = Cat()
animal_sound(dog) # prints "Woof!"
animal_sound(cat) # prints "Meow!"
In this example, the animal_sound
function uses the make_sound
method, which behaves differently depending on whether it's called on a Dog
or a Cat
object.
- Proper Class Design: Each class should have a single responsibility. The following is an example of a class that only handles operations related to a bank account:
class BankAccount:
def __init__(self):
self.__balance = 0.0
def deposit(self, amount):
self.__balance += amount
def withdraw(self, amount):
if amount > self.__balance:
print("Insufficient balance!")
return
self.__balance -= amount
def get_balance(self):
return self.__balance
In this example, BankAccount
is only responsible for handling operations related to the bank account.
- Composition over Inheritance: While inheritance can be useful, it can lead to overly complex hierarchies. Composition, on the other hand, promotes flexibility.
class Engine:
def start(self):
print("Engine starts")
class Car:
def __init__(self):
self.engine = Engine()
def start(self):
self.engine.start()
print("Car starts")
In this example, instead of inheriting from Engine
, the Car
class is composed with an Engine
object, demonstrating the principle of composition over inheritance.
Design Patterns
Design patterns provide general solutions or flexible ways to solve common design problems. These are some of the most important ones in OOP:
- Factory Pattern: The factory pattern involves creating an object factory to abstract the object creation process.
class Dog:
def __init__(self, name):
self._name = name
def speak(self):
return "Woof!"
class Cat:
def __init__(self, name):
self._name = name
def speak(self):
return "Meow!"
def get_pet(pet="dog"):
pets = dict(dog=Dog("Hope"), cat=Cat("Peace"))
return pets[pet]
d = get_pet("dog")
print(d.speak())
c = get_pet("cat")
print(c.speak())
In this example, the get_pet
function is a factory function that creates and returns an instance of Dog
or Cat
depending on the input.
- Singleton Pattern: The Singleton pattern ensures that a class has only one instance and provides a global point of access to it.
class Singleton:
_instance = None
@staticmethod
def getInstance():
if Singleton._instance == None:
Singleton()
return Singleton._instance
def __init__(self):
if Singleton._instance != None:
raise Exception("This class is a singleton!")
else:
Singleton._instance = self
s = Singleton()
print(s)
s = Singleton.getInstance()
print(s)
s = Singleton.getInstance()
print(s)
In this example, Singleton
is a singleton class that always returns the same instance.
- Strategy Pattern: The Strategy pattern enables an algorithm’s behaviour to be selected at runtime.
class StrategyExample:
def __init__(self, func=None):
if func:
self.execute = func
def execute(self):
print("Original execution")
def executpue_replacement1():
print("Strategy 1")
def execute_replacement2():
print("Strategy 2")
strategy_example = StrategyExample()
strategy_example.execute()
strategy_example1 = StrategyExample(execute_replacement1)
strategy_example1.execute()
strategy_example2 = StrategyExample(execute_replacement2)
strategy_example2.execute()
In this example, the StrategyExample
class's execute
method can be replaced with different functions at runtime.
- Observer Pattern: The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified.
class Subject:
def __init__(self):
self._observers = []
def attach(self, observer):
if observer not in self._observers:
self._observers.append(observer)
def detach(self, observer):
try:
self._observers.remove(observer)
except ValueError:
pass
def notify(self):
for observer in self._observers:
observer.update(self)
class Observer:
def update(self, subject):
pass
class AmericanStockMarket(Observer):
def update(self, subject):
print("American stock market received: {0}".format(subject))
class EuropeanStockMarket(Observer):
def update(self, subject):
print("European stock market received: {0}".format(subject))
if __name__ == '__main__':
ibm = Subject()
asm = AmericanStockMarket()
ibm.attach(asm)
esm = EuropeanStockMarket()
ibm.attach(esm)
ibm.notify()
In this example, the Subject
class notifies all registered observers when its notify
method is called.
- Decorator Pattern: The Decorator pattern allows behaviour to be added to an individual object, either statically or dynamically, without affecting the behaviour of other objects from the same class.
class Component:
def operation(self):
pass
class ConcreteComponent(Component):
def operation(self):
return "ConcreteComponent"
class Decorator(Component):
def __init__(self, component):
self._component = component
@property
def component(self):
return self._component
def operation(self):
return self._component.operation()
class ConcreteDecoratorA(Decorator):
def operation(self):
return f"ConcreteDecoratorA({self.component.operation()})"
class ConcreteDecoratorB(Decorator):
def operation(self):
return f"ConcreteDecoratorB({self.component.operation()})"
In this example, ConcreteDecoratorA
and ConcreteDecoratorB
are decorators that add responsibilities to ConcreteComponent
without changing its interface. Decorators are a flexible alternative to subclassing for extending functionality.
In conclusion, design patterns are proven solutions to common software design problems. They are not classes or libraries that can be plugged into an application and automatically solve problems. Instead, they are templates that provide guidance on how to solve various design problems in object-oriented software.
Conclusion
Mastering object-oriented programming requires understanding the fundamental principles and best practices, as well as how and when to use common design patterns. Through careful application of encapsulation, inheritance, polymorphism, and proper class design, along with the judicious use of design patterns, you can write code that is efficient, reusable, and easy to maintain. Remember, the ultimate goal of mastering OOP is to write code that not only works, but is also clean and manageable. This makes your software easier to read, understand, and modify, leading to more robust and maintainable applications.
Thanks for reading this article!
In our collective journey into the future, your thoughts and contributions are invaluable. For more engaging talks about blockchain, AI, technology, and biohacking, let’s stay connected across these platforms:
- Read more of my articles on Medium 📚
- Follow me on Twitter 🐦 for thoughts and discussions on AI, biohacking, and blockchain.
- Check out my projects and contribute on GitHub 👩💻.
- And join me on YouTube 🎥 for more in-depth conversations.
Let’s continue shaping the future together, one creation at a time. 🚀
Aurora Poppyseed