Проектирование классов в 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 требует понимания:
- Основ ООП - инкапсуляция, наследование, полиморфизм, абстракция
- Принципов SOLID - фундамент качественного дизайна
- Паттернов проектирования - проверенные решения типичных проблем
- Продвинутых возможностей Python - дескрипторы, метаклассы, миксины
- Best practices - композиция вместо наследования, type hints, тестирование
Ключевые рекомендации:
- Начинайте с простых классов и рефакторите по мере роста сложности
- Используйте композицию вместо глубоких иерархий наследования
- Пишите тесты для критической бизнес-логики
- Документируйте публичный API классов и методов
- Следуйте принципам SOLID и применяйте паттерны там, где они действительно нужны
Помните, что хороший дизайн классов делает код более поддерживаемым, расширяемым и тестируемым. Начинайте с простых решений и усложняйте архитектуру только когда это действительно необходимо.