← Назад к курсу

Проектирование классов в Python: полное руководство с паттернами проектирования

1. Основы объектно-ориентированного программирования (ООП) в Python

1.1 Четыре столпа ООП

Инкапсуляция - скрытие внутренней реализации и предоставление интерфейса:

class BankAccount:
    def __init__(self, owner, balance):
        self.owner = owner  # публичный атрибут
        self._balance = balance  # защищенный атрибут (convention)
        self.__secret_code = 1234  # приватный атрибут (name mangling)
    
    def deposit(self, amount):
        if amount > 0:
            self._balance += amount
            self._log_transaction(f"Deposited {amount}")
    
    def get_balance(self):
        return self._balance
    
    def _log_transaction(self, message):  # защищенный метод
        print(f"Log: {message}")
    
    def __reset_balance(self):  # приватный метод
        self._balance = 0

Наследование - создание новых классов на основе существующих:

class Animal:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        raise NotImplementedError("Subclasses must implement this method")

class Dog(Animal):
    def speak(self):
        return f"{self.name} says Woof!"

class Cat(Animal):
    def speak(self):
        return f"{self.name} says Meow!"

class FlyingDog(Dog):  # Множественное наследование
    def __init__(self, name, wing_span):
        super().__init__(name)
        self.wing_span = wing_span
    
    def fly(self):
        return f"{self.name} is flying with {self.wing_span}cm wings!"

Полиморфизм - использование объектов разных типов через общий интерфейс:

def animal_sounds(animals):
    for animal in animals:
        print(animal.speak())  # Полиморфный вызов

animals = [Dog("Rex"), Cat("Whiskers"), FlyingDog("Sky", 50)]
animal_sounds(animals)

Абстракция - создание абстрактных классов и интерфейсов:

from abc import ABC, abstractmethod
from typing import List

class DatabaseConnection(ABC):
    @abstractmethod
    def connect(self):
        pass
    
    @abstractmethod
    def disconnect(self):
        pass
    
    @abstractmethod
    def execute_query(self, query: str) -> List:
        pass

class MySQLConnection(DatabaseConnection):
    def connect(self):
        print("Connecting to MySQL...")
    
    def disconnect(self):
        print("Disconnecting from MySQL...")
    
    def execute_query(self, query: str) -> List:
        print(f"Executing MySQL query: {query}")
        return []

1.2 Магические методы (dunder methods)

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    # Строковое представление
    def __str__(self):
        return f"Vector({self.x}, {self.y})"
    
    def __repr__(self):
        return f"Vector(x={self.x}, y={self.y})"
    
    # Арифметические операции
    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)
    
    def __sub__(self, other):
        return Vector(self.x - other.x, self.y - other.y)
    
    def __mul__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)
    
    # Сравнение
    def __eq__(self, other):
        return self.x == other.x and self.y == other.y
    
    def __lt__(self, other):
        return self.magnitude() < other.magnitude()
    
    # Вызов как функцию
    def __call__(self):
        return self.magnitude()
    
    # Контейнерные методы
    def __len__(self):
        return 2
    
    def __getitem__(self, index):
        if index == 0:
            return self.x
        elif index == 1:
            return self.y
        raise IndexError("Vector index out of range")
    
    # Вспомогательный метод
    def magnitude(self):
        return (self.x**2 + self.y**2) ** 0.5

# Использование
v1 = Vector(2, 3)
v2 = Vector(4, 5)
print(v1 + v2)  # Vector(6, 8)
print(v1 * 3)   # Vector(6, 9)
print(v1())     # 3.605 - вызов как функции

2. Паттерны проектирования (Design Patterns)

2.1 Порождающие паттерны (Creational)

Singleton (Одиночка) - гарантирует один экземпляр класса:

class SingletonMeta(type):
    _instances = {}
    
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            instance = super().__call__(*args, **kwargs)
            cls._instances[cls] = instance
        return cls._instances[cls]

class DatabaseConnection(metaclass=SingletonMeta):
    def __init__(self):
        print("Initializing database connection...")
        self.connection = "Active"
    
    def query(self, sql):
        print(f"Executing: {sql}")
        return []

# Альтернатива через декоратор
def singleton(cls):
    instances = {}
    
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    
    return get_instance

@singleton
class Configuration:
    def __init__(self):
        self.settings = {"theme": "dark", "language": "en"}

Factory Method (Фабричный метод):

from abc import ABC, abstractmethod

class Document(ABC):
    @abstractmethod
    def create(self):
        pass
    
    @abstractmethod
    def save(self):
        pass

class PDFDocument(Document):
    def create(self):
        return "Creating PDF document"
    
    def save(self):
        return "Saving as PDF"

class WordDocument(Document):
    def create(self):
        return "Creating Word document"
    
    def save(self):
        return "Saving as DOCX"

class DocumentFactory(ABC):
    @abstractmethod
    def create_document(self) -> Document:
        pass
    
    def process_document(self):
        doc = self.create_document()
        print(doc.create())
        print(doc.save())

class PDFFactory(DocumentFactory):
    def create_document(self) -> Document:
        return PDFDocument()

class WordFactory(DocumentFactory):
    def create_document(self) -> Document:
        return WordDocument()

Builder (Строитель) - создание сложных объектов:

class Computer:
    def __init__(self):
        self.cpu = None
        self.ram = None
        self.storage = None
        self.gpu = None
    
    def __str__(self):
        return f"Computer: CPU={self.cpu}, RAM={self.ram}, Storage={self.storage}, GPU={self.gpu}"

class ComputerBuilder:
    def __init__(self):
        self.computer = Computer()
    
    def set_cpu(self, cpu):
        self.computer.cpu = cpu
        return self
    
    def set_ram(self, ram):
        self.computer.ram = ram
        return self
    
    def set_storage(self, storage):
        self.computer.storage = storage
        return self
    
    def set_gpu(self, gpu):
        self.computer.gpu = gpu
        return self
    
    def build(self):
        return self.computer

# Использование
builder = ComputerBuilder()
computer = (builder
           .set_cpu("Intel i7")
           .set_ram("32GB")
           .set_storage("1TB SSD")
           .set_gpu("NVIDIA RTX 3080")
           .build())
print(computer)

2.2 Структурные паттерны (Structural)

Adapter (Адаптер) - преобразует интерфейс класса:

class OldSystem:
    def old_request(self):
        return "Data from old system"

class NewSystemInterface:
    def request(self):
        pass

class Adapter(NewSystemInterface):
    def __init__(self, old_system):
        self.old_system = old_system
    
    def request(self):
        return f"Adapted: {self.old_system.old_request()}"

# Использование
old = OldSystem()
adapter = Adapter(old)
print(adapter.request())

Decorator (Декоратор) - динамическое добавление функциональности:

class Coffee:
    def cost(self):
        return 5
    
    def description(self):
        return "Simple coffee"

class CoffeeDecorator:
    def __init__(self, coffee):
        self._coffee = coffee
    
    def cost(self):
        return self._coffee.cost()
    
    def description(self):
        return self._coffee.description()

class MilkDecorator(CoffeeDecorator):
    def cost(self):
        return self._coffee.cost() + 2
    
    def description(self):
        return self._coffee.description() + ", milk"

class SugarDecorator(CoffeeDecorator):
    def cost(self):
        return self._coffee.cost() + 1
    
    def description(self):
        return self._coffee.description() + ", sugar"

# Использование
coffee = Coffee()
coffee = MilkDecorator(coffee)
coffee = SugarDecorator(coffee)
print(f"{coffee.description()} costs ${coffee.cost()}")

Facade (Фасад) - упрощенный интерфейс к сложной системе:

class CPU:
    def execute(self):
        print("CPU executing instructions")

class Memory:
    def load(self):
        print("Memory loading data")

class HardDrive:
    def read(self):
        print("Hard drive reading data")

class ComputerFacade:
    def __init__(self):
        self.cpu = CPU()
        self.memory = Memory()
        self.hard_drive = HardDrive()
    
    def start(self):
        print("Starting computer...")
        self.memory.load()
        self.hard_drive.read()
        self.cpu.execute()
        print("Computer started!")

# Использование
computer = ComputerFacade()
computer.start()

Composite (Компоновщик) - работа с древовидными структурами:

from abc import ABC, abstractmethod

class FileSystemComponent(ABC):
    @abstractmethod
    def show_info(self, indent=0):
        pass
    
    @abstractmethod
    def get_size(self):
        pass

class File(FileSystemComponent):
    def __init__(self, name, size):
        self.name = name
        self._size = size
    
    def show_info(self, indent=0):
        print(" " * indent + f"📄 {self.name} ({self._size} KB)")
    
    def get_size(self):
        return self._size

class Directory(FileSystemComponent):
    def __init__(self, name):
        self.name = name
        self.children = []
    
    def add(self, component):
        self.children.append(component)
    
    def remove(self, component):
        self.children.remove(component)
    
    def show_info(self, indent=0):
        print(" " * indent + f"📁 {self.name}")
        for child in self.children:
            child.show_info(indent + 2)
    
    def get_size(self):
        total = 0
        for child in self.children:
            total += child.get_size()
        return total

# Использование
root = Directory("Root")
docs = Directory("Documents")
docs.add(File("report.pdf", 1500))
docs.add(File("notes.txt", 100))
photos = Directory("Photos")
photos.add(File("vacation.jpg", 3000))
photos.add(File("family.jpg", 2500))

root.add(docs)
root.add(photos)
root.show_info()
print(f"Total size: {root.get_size()} KB")

2.3 Поведенческие паттерны (Behavioral)

Observer (Наблюдатель) - уведомление об изменениях:

class Subject:
    def __init__(self):
        self._observers = []
        self._state = None
    
    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)
    
    @property
    def state(self):
        return self._state
    
    @state.setter
    def state(self, value):
        self._state = value
        self.notify()

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

class ConcreteObserverA(Observer):
    def update(self, subject):
        print(f"Observer A: Reacted to state change: {subject.state}")

class ConcreteObserverB(Observer):
    def update(self, subject):
        print(f"Observer B: Reacted to state change: {subject.state}")

# Использование
subject = Subject()
observer_a = ConcreteObserverA()
observer_b = ConcreteObserverB()

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

subject.state = "New State"

Strategy (Стратегия) - выбор алгоритма во время выполнения:

from abc import ABC, abstractmethod

class PaymentStrategy(ABC):
    @abstractmethod
    def pay(self, amount):
        pass

class CreditCardPayment(PaymentStrategy):
    def __init__(self, card_number, cvv):
        self.card_number = card_number
        self.cvv = cvv
    
    def pay(self, amount):
        print(f"Paid ${amount} using Credit Card {self.card_number[-4:]}")

class PayPalPayment(PaymentStrategy):
    def __init__(self, email):
        self.email = email
    
    def pay(self, amount):
        print(f"Paid ${amount} using PayPal ({self.email})")

class ShoppingCart:
    def __init__(self):
        self.items = []
        self.payment_strategy = None
    
    def add_item(self, item, price):
        self.items.append((item, price))
    
    def calculate_total(self):
        return sum(price for _, price in self.items)
    
    def set_payment_strategy(self, strategy):
        self.payment_strategy = strategy
    
    def checkout(self):
        total = self.calculate_total()
        if self.payment_strategy:
            self.payment_strategy.pay(total)
            self.items = []
        else:
            print("Please select a payment method")

# Использование
cart = ShoppingCart()
cart.add_item("Laptop", 1000)
cart.add_item("Mouse", 50)

cart.set_payment_strategy(CreditCardPayment("1234-5678-9012-3456", "123"))
cart.checkout()

cart.add_item("Keyboard", 80)
cart.set_payment_strategy(PayPalPayment("user@example.com"))
cart.checkout()

Command (Команда) - инкапсуляция запросов как объектов:

from abc import ABC, abstractmethod

class Command(ABC):
    @abstractmethod
    def execute(self):
        pass
    
    @abstractmethod
    def undo(self):
        pass

class Light:
    def turn_on(self):
        print("Light is ON")
    
    def turn_off(self):
        print("Light is OFF")

class LightOnCommand(Command):
    def __init__(self, light):
        self.light = light
    
    def execute(self):
        self.light.turn_on()
    
    def undo(self):
        self.light.turn_off()

class LightOffCommand(Command):
    def __init__(self, light):
        self.light = light
    
    def execute(self):
        self.light.turn_off()
    
    def undo(self):
        self.light.turn_on()

class RemoteControl:
    def __init__(self):
        self.commands = []
        self.history = []
    
    def set_command(self, command):
        self.commands.append(command)
    
    def press_button(self, index):
        if 0 <= index < len(self.commands):
            command = self.commands[index]
            command.execute()
            self.history.append(command)
    
    def press_undo(self):
        if self.history:
            last_command = self.history.pop()
            last_command.undo()

# Использование
light = Light()
light_on = LightOnCommand(light)
light_off = LightOffCommand(light)

remote = RemoteControl()
remote.set_command(light_on)
remote.set_command(light_off)

remote.press_button(0)  # Включаем свет
remote.press_button(1)  # Выключаем свет
remote.press_undo()     # Отменяем последнюю команду

3. Принципы проектирования SOLID

3.1 Single Responsibility (Принцип единственной ответственности)

# ❌ Плохо: Класс делает слишком много
class UserManager:
    def __init__(self):
        self.users = []
    
    def add_user(self, user):
        self.users.append(user)
    
    def remove_user(self, user):
        self.users.remove(user)
    
    def save_to_database(self, user):
        print(f"Saving {user} to database...")
    
    def send_email(self, user, message):
        print(f"Sending email to {user}: {message}")

# ✅ Хорошо: Разделение ответственности
class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email

class UserRepository:
    def save(self, user):
        print(f"Saving {user.name} to database...")
    
    def delete(self, user):
        print(f"Deleting {user.name} from database...")

class EmailService:
    def send(self, user, message):
        print(f"Sending email to {user.email}: {message}")

class UserService:
    def __init__(self):
        self.repository = UserRepository()
        self.email_service = EmailService()
    
    def register_user(self, user):
        self.repository.save(user)
        self.email_service.send(user, "Welcome!")

3.2 Open/Closed (Принцип открытости/закрытости)

# ❌ Плохо: При добавлении новой формы нужно менять класс
class AreaCalculator:
    def calculate_area(self, shape):
        if isinstance(shape, Rectangle):
            return shape.width * shape.height
        elif isinstance(shape, Circle):
            return 3.14 * shape.radius ** 2
        # Добавлять новые elif для каждой новой формы

# ✅ Хорошо: Класс закрыт для модификации, но открыт для расширения
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    def area(self):
        return self.width * self.height

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius
    
    def area(self):
        return 3.14 * self.radius ** 2

class Triangle(Shape):  # Новая форма без изменения AreaCalculator
    def __init__(self, base, height):
        self.base = base
        self.height = height
    
    def area(self):
        return 0.5 * self.base * self.height

class AreaCalculator:
    def calculate_total_area(self, shapes):
        total = 0
        for shape in shapes:
            total += shape.area()
        return total

3.3 Liskov Substitution (Принцип подстановки Барбары Лисков)

# ❌ Плохо: Нарушение принципа
class Bird:
    def fly(self):
        return "Flying"
    
    def eat(self):
        return "Eating"

class Penguin(Bird):
    def fly(self):
        raise Exception("Penguins can't fly!")  # Нарушение принципа!

# ✅ Хорошо: Правильная иерархия
class Bird:
    def eat(self):
        return "Eating"

class FlyingBird(Bird):
    def fly(self):
        return "Flying"

class Penguin(Bird):
    def swim(self):
        return "Swimming"

class Eagle(FlyingBird):
    def hunt(self):
        return "Hunting"

3.4 Interface Segregation (Принцип разделения интерфейсов)

# ❌ Плохо: Один огромный интерфейс
class Worker(ABC):
    @abstractmethod
    def work(self):
        pass
    
    @abstractmethod
    def eat(self):
        pass
    
    @abstractmethod
    def sleep(self):
        pass

# ✅ Хорошо: Разделенные интерфейсы
class Workable(ABC):
    @abstractmethod
    def work(self):
        pass

class Eatable(ABC):
    @abstractmethod
    def eat(self):
        pass

class Sleepable(ABC):
    @abstractmethod
    def sleep(self):
        pass

class Human(Workable, Eatable, Sleepable):
    def work(self):
        return "Human working"
    
    def eat(self):
        return "Human eating"
    
    def sleep(self):
        return "Human sleeping"

class Robot(Workable):  # Роботу не нужно есть или спать
    def work(self):
        return "Robot working"

3.5 Dependency Inversion (Принцип инверсии зависимостей)

# ❌ Плохо: Высокоуровневый модуль зависит от низкоуровневого
class MySQLDatabase:
    def connect(self):
        return "MySQL connection"
    
    def query(self, sql):
        return f"MySQL: {sql}"

class ReportGenerator:
    def __init__(self):
        self.db = MySQLDatabase()  # Прямая зависимость
    
    def generate(self):
        connection = self.db.connect()
        # ... генерация отчета

# ✅ Хорошо: Оба модуля зависят от абстракции
from abc import ABC, abstractmethod

class Database(ABC):
    @abstractmethod
    def connect(self):
        pass
    
    @abstractmethod
    def query(self, sql):
        pass

class MySQLDatabase(Database):
    def connect(self):
        return "MySQL connection"
    
    def query(self, sql):
        return f"MySQL: {sql}"

class PostgreSQLDatabase(Database):
    def connect(self):
        return "PostgreSQL connection"
    
    def query(self, sql):
        return f"PostgreSQL: {sql}"

class ReportGenerator:
    def __init__(self, database: Database):  # Зависимость от абстракции
        self.db = database
    
    def generate(self):
        connection = self.db.connect()
        # ... генерация отчета

# Использование
mysql_db = MySQLDatabase()
postgres_db = PostgreSQLDatabase()

report1 = ReportGenerator(mysql_db)
report2 = ReportGenerator(postgres_db)

4. Продвинутые техники проектирования

4.1 Дескрипторы (Descriptors)

class ValidatedAttribute:
    def __init__(self, validator):
        self.validator = validator
        self.data = {}
    
    def __get__(self, instance, owner):
        if instance is None:
            return self
        return self.data.get(id(instance))
    
    def __set__(self, instance, value):
        if self.validator(value):
            self.data[id(instance)] = value
        else:
            raise ValueError(f"Invalid value: {value}")

def is_positive(value):
    return value > 0

def is_non_empty_string(value):
    return isinstance(value, str) and len(value.strip()) > 0

class Product:
    price = ValidatedAttribute(is_positive)
    name = ValidatedAttribute(is_non_empty_string)
    
    def __init__(self, name, price):
        self.name = name
        self.price = price

# Использование
try:
    p = Product("Laptop", 1000)  # OK
    print(p.name, p.price)
    
    p2 = Product("", 1000)  # ValueError
except ValueError as e:
    print(e)

4.2 Метаклассы (Metaclasses)

class SingletonMeta(type):
    _instances = {}
    
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]

class AutoRegisterMeta(type):
    def __init__(cls, name, bases, attrs):
        super().__init__(name, bases, attrs)
        if not hasattr(cls, 'registry'):
            cls.registry = {}
        else:
            cls.registry[name] = cls

class ValidatedMeta(type):
    def __new__(mcs, name, bases, attrs):
        # Добавляем валидацию для всех атрибутов, начинающихся с 'validate_'
        for attr_name, attr_value in attrs.items():
            if attr_name.startswith('validate_'):
                field_name = attr_name[9:]  # Убираем 'validate_'
                if field_name in attrs:
                    # Создаем свойство с валидацией
                    attrs[field_name] = property(
                        lambda self, fn=field_name: getattr(self, f"_{fn}"),
                        lambda self, value, v=attr_value, fn=field_name: setattr(
                            self, f"_{fn}", v(value) if callable(v) else value
                        )
                    )
        return super().__new__(mcs, name, bases, attrs)

# Пример использования метакласса
class Person(metaclass=ValidatedMeta):
    validate_age = lambda x: x if x >= 0 else 0
    validate_name = lambda x: x.strip()
    
    def __init__(self, name, age):
        self.name = name
        self.age = age

p = Person("  John  ", -5)
print(p.name, p.age)  # "John" 0

4.3 Миксины (Mixins)

class JSONSerializableMixin:
    def to_json(self):
        import json
        return json.dumps(self.__dict__)
    
    @classmethod
    def from_json(cls, json_str):
        import json
        data = json.loads(json_str)
        return cls(**data)

class XMLSerializableMixin:
    def to_xml(self):
        import xml.etree.ElementTree as ET
        root = ET.Element(self.__class__.__name__)
        for key, value in self.__dict__.items():
            child = ET.SubElement(root, key)
            child.text = str(value)
        return ET.tostring(root, encoding='unicode')
    
    @classmethod
    def from_xml(cls, xml_str):
        import xml.etree.ElementTree as ET
        root = ET.fromstring(xml_str)
        data = {child.tag: child.text for child in root}
        return cls(**data)

class LoggableMixin:
    def log(self, message):
        print(f"[{self.__class__.__name__}] {message}")

class Product(JSONSerializableMixin, XMLSerializableMixin, LoggableMixin):
    def __init__(self, name, price, quantity):
        self.name = name
        self.price = price
        self.quantity = quantity
    
    def total_value(self):
        return self.price * self.quantity

# Использование
product = Product("Laptop", 1000, 5)
product.log("Product created")

json_data = product.to_json()
print(f"JSON: {json_data}")

xml_data = product.to_xml()
print(f"XML: {xml_data}")

4.4 Dependency Injection Container

class DIContainer:
    def __init__(self):
        self.services = {}
        self.instances = {}
    
    def register(self, name, service_class, dependencies=None):
        self.services[name] = {
            'class': service_class,
            'dependencies': dependencies or []
        }
    
    def get(self, name):
        if name in self.instances:
            return self.instances[name]
        
        if name not in self.services:
            raise ValueError(f"Service {name} not registered")
        
        service_info = self.services[name]
        dependencies = [
            self.get(dep_name) for dep_name in service_info['dependencies']
        ]
        
        instance = service_info['class'](*dependencies)
        self.instances[name] = instance
        return instance

# Пример использования
class Database:
    def connect(self):
        return "Database connected"

class Logger:
    def log(self, message):
        print(f"LOG: {message}")

class UserService:
    def __init__(self, database, logger):
        self.db = database
        self.logger = logger
    
    def get_users(self):
        self.logger.log("Getting users")
        return ["Alice", "Bob", "Charlie"]

# Настройка контейнера
container = DIContainer()
container.register('database', Database)
container.register('logger', Logger)
container.register('user_service', UserService, ['database', 'logger'])

# Использование
user_service = container.get('user_service')
print(user_service.get_users())

5. Best Practices и рекомендации

5.1 Композиция вместо наследования

# ❌ Плохо: Глубокие иерархии наследования
class Vehicle:
    def move(self):
        pass

class Car(Vehicle):
    def drive(self):
        pass

class ElectricCar(Car):
    def charge(self):
        pass

class Tesla(ElectricCar):
    def autopilot(self):
        pass

# ✅ Хорошо: Композиция
class Engine:
    def start(self):
        return "Engine started"

class Battery:
    def charge(self):
        return "Battery charging"

class Autopilot:
    def engage(self):
        return "Autopilot engaged"

class Car:
    def __init__(self):
        self.engine = Engine()
        self.battery = Battery()
        self.autopilot = Autopilot()
    
    def drive(self):
        return f"{self.engine.start()} and driving"
    
    def charge(self):
        return self.battery.charge()
    
    def self_drive(self):
        return self.autopilot.engage()

5.2 Использование dataclasses

from dataclasses import dataclass, field
from typing import List, Optional
from datetime import datetime

@dataclass
class Product:
    name: str
    price: float
    quantity: int = 0
    created_at: datetime = field(default_factory=datetime.now)
    tags: List[str] = field(default_factory=list)
    
    @property
    def total_value(self):
        return self.price * self.quantity
    
    def add_tag(self, tag: str):
        if tag not in self.tags:
            self.tags.append(tag)

@dataclass
class Order:
    products: List[Product]
    customer: str
    discount: Optional[float] = None
    
    def total_price(self):
        total = sum(p.price * p.quantity for p in self.products)
        if self.discount:
            total *= (1 - self.discount)
        return total

# Использование
product1 = Product("Laptop", 1000, 2)
product2 = Product("Mouse", 50, 5)

order = Order([product1, product2], "John Doe", 0.1)
print(order.total_price())

5.3 Type hints и mypy

from typing import List, Dict, Optional, Union, Callable, TypeVar, Generic
from abc import ABC, abstractmethod

T = TypeVar('T')

class Repository(Generic[T]):
    def __init__(self) -> None:
        self._items: Dict[str, T] = {}
    
    def add(self, id: str, item: T) -> None:
        self._items[id] = item
    
    def get(self, id: str) -> Optional[T]:
        return self._items.get(id)
    
    def get_all(self) -> List[T]:
        return list(self._items.values())

class User:
    def __init__(self, name: str, email: str) -> None:
        self.name = name
        self.email = email
    
    def __str__(self) -> str:
        return f"User(name={self.name}, email={self.email})"

def process_users(users: List[User], 
                  callback: Callable[[User], bool]) -> List[User]:
    return [user for user in users if callback(user)]

# Использование с type hints
user_repo = Repository[User]()
user_repo.add("1", User("Alice", "alice@example.com"))
user_repo.add("2", User("Bob", "bob@example.com"))

users = user_repo.get_all()
filtered = process_users(users, lambda u: "a" in u.name.lower())
print(filtered)

6. Антипаттерны и распространенные ошибки

6.1 Распространенные антипаттерны

# 1. Богоподобный класс (God Object)
class GodObject:
    # Слишком много ответственностей в одном классе
    def process_data(self): pass
    def save_to_db(self): pass
    def send_email(self): pass
    def generate_report(self): pass
    def validate_input(self): pass

# 2. Цепочка вызовов (Train Wreck)
# ❌ Плохо
result = object.get_data().process().filter().sort().format()

# ✅ Лучше
data = object.get_data()
processed = data.process()
filtered = processed.filter()
sorted_data = filtered.sort()
result = sorted_data.format()

# 3. Магические числа и строки
# ❌ Плохо
class Payment:
    def process(self, amount):
        if amount > 10000:  # Магическое число
            return "Needs approval"
        return "Approved"

# ✅ Лучше
class Payment:
    APPROVAL_LIMIT = 10000
    
    def process(self, amount):
        if amount > self.APPROVAL_LIMIT:
            return "Needs approval"
        return "Approved"

6.2 Тестирование классов

import unittest
from unittest.mock import Mock, patch

class ShoppingCart:
    def __init__(self, payment_processor):
        self.items = []
        self.payment_processor = payment_processor
    
    def add_item(self, item, price):
        self.items.append((item, price))
    
    def checkout(self):
        total = sum(price for _, price in self.items)
        return self.payment_processor.charge(total)

class TestShoppingCart(unittest.TestCase):
    def setUp(self):
        self.mock_processor = Mock()
        self.cart = ShoppingCart(self.mock_processor)
    
    def test_add_item(self):
        self.cart.add_item("Laptop", 1000)
        self.assertEqual(len(self.cart.items), 1)
        self.assertEqual(self.cart.items[0], ("Laptop", 1000))
    
    def test_checkout(self):
        self.cart.add_item("Laptop", 1000)
        self.cart.add_item("Mouse", 50)
        
        self.mock_processor.charge.return_value = True
        
        result = self.cart.checkout()
        
        self.assertTrue(result)
        self.mock_processor.charge.assert_called_once_with(1050)
    
    def test_empty_cart_checkout(self):
        result = self.cart.checkout()
        self.mock_processor.charge.assert_called_once_with(0)

if __name__ == '__main__':
    unittest.main()

Заключение

Проектирование классов в Python требует понимания:

  1. Основ ООП - инкапсуляция, наследование, полиморфизм, абстракция
  2. Принципов SOLID - фундамент качественного дизайна
  3. Паттернов проектирования - проверенные решения типичных проблем
  4. Продвинутых возможностей Python - дескрипторы, метаклассы, миксины
  5. Best practices - композиция вместо наследования, type hints, тестирование

Ключевые рекомендации:

  • Начинайте с простых классов и рефакторите по мере роста сложности
  • Используйте композицию вместо глубоких иерархий наследования
  • Пишите тесты для критической бизнес-логики
  • Документируйте публичный API классов и методов
  • Следуйте принципам SOLID и применяйте паттерны там, где они действительно нужны

Помните, что хороший дизайн классов делает код более поддерживаемым, расширяемым и тестируемым. Начинайте с простых решений и усложняйте архитектуру только когда это действительно необходимо.