**Advanced OOP Concepts and Design Patterns

This lesson builds upon your foundational understanding of Object-Oriented Programming (OOP) in Python. You'll learn advanced OOP concepts like abstract classes, interfaces, and the SOLID principles, along with popular design patterns like Singleton, Factory, and Observer. These powerful tools will help you write more robust, maintainable, and scalable Python code.

Learning Objectives

  • Understand and implement abstract classes and interfaces (using abstract base classes) in Python.
  • Apply the SOLID principles to design better OOP systems.
  • Recognize and implement the Singleton, Factory, and Observer design patterns.
  • Identify situations where these design patterns are beneficial for code organization and maintainability.

Text-to-Speech

Listen to the lesson content

Lesson Content

Abstract Classes and Interfaces (ABC)

Abstract classes define a blueprint for other classes, enforcing a certain structure. In Python, we use the abc module to create abstract base classes (ABCs) which serve as interfaces. An abstract method is declared, but it's implementation is left to the concrete subclasses.

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass # Subclasses MUST implement this

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14159 * self.radius * self.radius

class Square(Shape):
    def __init__(self, side):
        self.side = side

    def area(self):
        return self.side * self.side

# shape = Shape() # This will raise an error because Shape is abstract
circle = Circle(5)
square = Square(4)
print(f"Circle area: {circle.area()}")
print(f"Square area: {square.area()}")

Here, Shape acts as an interface. Both Circle and Square must implement the area() method. If a concrete class doesn't implement it, it will cause an error.

SOLID Principles

SOLID is a mnemonic acronym for five design principles intended to make software designs more understandable, flexible, and maintainable:

  • Single Responsibility Principle (SRP): A class should have only one reason to change. Each class should have a single, well-defined responsibility.

  • Open/Closed Principle (OCP): Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. You should be able to add new functionality without changing existing code.

  • Liskov Substitution Principle (LSP): Subtypes must be substitutable for their base types without altering the correctness of the program. This means subclasses should not break the functionality of the parent class.

  • Interface Segregation Principle (ISP): Clients should not be forced to depend on methods they do not use. Break down large interfaces into smaller, more specific ones.

  • Dependency Inversion Principle (DIP): High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.

Example (SRP): Instead of a class responsible for both data access and business logic, separate them into different classes/modules.

Design Patterns: Singleton

The Singleton pattern ensures that a class has only one instance and provides a global point of access to it. This is useful when you need to control access to a shared resource, like a database connection or a configuration object.

class Singleton:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super(Singleton, cls).__new__(cls)
        return cls._instance

    def __init__(self):
        if not hasattr(self, 'initialized'):
            self.value = "I'm the Singleton!"
            self.initialized = True

s1 = Singleton()
s2 = Singleton()
print(s1 is s2) # True, same instance
print(s1.value)
s2.value = "I've been changed!"
print(s1.value) # Prints the changed value

Design Patterns: Factory

The Factory pattern provides an interface for creating objects, but lets subclasses decide which class to instantiate. This centralizes the object creation logic.

class Animal:
    def speak(self):
        pass

class Dog(Animal):
    def speak(self):
        return "Woof!"

class Cat(Animal):
    def speak(self):
        return "Meow!"

class AnimalFactory:
    def create_animal(self, animal_type):
        if animal_type == "dog":
            return Dog()
        elif animal_type == "cat":
            return Cat()
        else:
            return None

factory = AnimalFactory()
dog = factory.create_animal("dog")
cat = factory.create_animal("cat")
if dog:
    print(dog.speak())
if cat:
    print(cat.speak())

Design Patterns: Observer

The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. Useful for event-driven systems.

class Subject:
    def __init__(self):
        self._observers = []

    def attach(self, observer):
        self._observers.append(observer)

    def detach(self, observer):
        self._observers.remove(observer)

    def notify(self):
        for observer in self._observers:
            observer.update(self)

class ConcreteSubject(Subject):
    def __init__(self):
        super().__init__()
        self._state = None

    @property
    def state(self):
        return self._state

    @state.setter
    def state(self, state):
        self._state = state
        self.notify()

class Observer:
    def update(self, subject):
        pass

class ConcreteObserverA(Observer):
    def update(self, subject):
        print("ConcreteObserverA:  State updated to", subject.state)

class ConcreteObserverB(Observer):
    def update(self, subject):
        print("ConcreteObserverB: State updated to", subject.state)

# Example Usage
subject = ConcreteSubject()
observer_a = ConcreteObserverA()
observer_b = ConcreteObserverB()

subject.attach(observer_a)
subject.attach(observer_b)

subject.state = "New State 1"
subject.state = "New State 2"
Progress
0%