Как лучше всего оформить повторяющийся код?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Как лучше всего оформить повторяющийся код?
Дублирование кода (копипаста) — один из главных врагов качественного кода. Существует много стратегий для борьбы с дублированием, и выбор зависит от контекста.
Принцип DRY (Don't Repeat Yourself)
Если код повторяется более 2 раз, его нужно выносить в отдельную функцию или компонент.
Стратегия 1: Выделение в функцию
Самый простой и часто используемый подход.
❌ Плохо — повторение
# Обработка разных типов данных
user_data = {"name": "Alice", "age": 30}
if not user_data.get("name"):
raise ValueError("Name is required")
if len(user_data.get("name", "")) < 2:
raise ValueError("Name must be at least 2 chars")
product_data = {"title": "Laptop"}
if not product_data.get("title"):
raise ValueError("Title is required")
if len(product_data.get("title", "")) < 2:
raise ValueError("Title must be at least 2 chars")
✅ Хорошо — функция для валидации
def validate_string(value: str, field_name: str, min_length: int = 2) -> None:
"""Валидирует строковое поле."""
if not value:
raise ValueError(f"{field_name} is required")
if len(value) < min_length:
raise ValueError(f"{field_name} must be at least {min_length} chars")
# Использование
user_data = {"name": "Alice"}
validate_string(user_data.get("name"), "Name")
product_data = {"title": "Laptop"}
validate_string(product_data.get("title"), "Title")
Стратегия 2: Использование decorators
Для повторяющейся логики вокруг функций (логирование, кеширование, обработка ошибок).
❌ Плохо — повторение обработки ошибок
def get_user(user_id: int):
try:
response = requests.get(f"https://api.example.com/users/{user_id}")
return response.json()
except requests.RequestException as e:
logger.error(f"Failed to get user: {e}")
return None
def get_product(product_id: int):
try:
response = requests.get(f"https://api.example.com/products/{product_id}")
return response.json()
except requests.RequestException as e:
logger.error(f"Failed to get product: {e}")
return None
def get_order(order_id: int):
try:
response = requests.get(f"https://api.example.com/orders/{order_id}")
return response.json()
except requests.RequestException as e:
logger.error(f"Failed to get order: {e}")
return None
✅ Хорошо — decorator для обработки ошибок
import functools
import logging
logger = logging.getLogger(__name__)
def handle_api_errors(func):
"""Обрабатывает ошибки API запросов."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except requests.RequestException as e:
logger.error(f"API error in {func.__name__}: {e}")
return None
return wrapper
@handle_api_errors
def get_user(user_id: int):
response = requests.get(f"https://api.example.com/users/{user_id}")
return response.json()
@handle_api_errors
def get_product(product_id: int):
response = requests.get(f"https://api.example.com/products/{product_id}")
return response.json()
@handle_api_errors
def get_order(order_id: int):
response = requests.get(f"https://api.example.com/orders/{order_id}")
return response.json()
Стратегия 3: Наследование (Inheritance)
Для классов с общей логикой.
❌ Плохо — дублирование в классах
class UserRepository:
def __init__(self, db):
self.db = db
def find_all(self):
try:
return self.db.query("SELECT * FROM users")
except Exception as e:
logger.error(f"Database error: {e}")
return []
class ProductRepository:
def __init__(self, db):
self.db = db
def find_all(self):
try:
return self.db.query("SELECT * FROM products")
except Exception as e:
logger.error(f"Database error: {e}")
return []
✅ Хорошо — наследование из базового класса
class BaseRepository:
def __init__(self, db, table_name: str):
self.db = db
self.table_name = table_name
def find_all(self):
try:
return self.db.query(f"SELECT * FROM {self.table_name}")
except Exception as e:
logger.error(f"Database error: {e}")
return []
class UserRepository(BaseRepository):
def __init__(self, db):
super().__init__(db, "users")
class ProductRepository(BaseRepository):
def __init__(self, db):
super().__init__(db, "products")
Стратегия 4: Использование Composition (Композиция)
Более гибкий подход чем наследование.
class DatabaseHandler:
def query(self, sql: str):
try:
return self.db.execute(sql)
except Exception as e:
logger.error(f"Database error: {e}")
return []
class Repository:
def __init__(self, db_handler: DatabaseHandler, table: str):
self.db_handler = db_handler
self.table = table
def find_all(self):
return self.db_handler.query(f"SELECT * FROM {self.table}")
# Использование
db_handler = DatabaseHandler()
users_repo = Repository(db_handler, "users")
products_repo = Repository(db_handler, "products")
Стратегия 5: Context Manager для повторяющейся логики
Для setup/cleanup операций.
❌ Плохо — повторение try-finally
def process_file1(filename):
f = open(filename, 'r')
try:
data = f.read()
# обработка
finally:
f.close()
def process_file2(filename):
f = open(filename, 'r')
try:
data = f.read()
# обработка
finally:
f.close()
✅ Хорошо — context manager
def process_file1(filename):
with open(filename, 'r') as f:
data = f.read()
# обработка
def process_file2(filename):
with open(filename, 'r') as f:
data = f.read()
# обработка
Стратегия 6: Template Method (паттерн проектирования)
Для алгоритмов с одинаковой структурой, но разными шагами.
from abc import ABC, abstractmethod
class DataProcessor(ABC):
"""Шаблон обработки данных."""
def process(self, data):
"""Template Method — определяет структуру."""
validated = self.validate(data)
transformed = self.transform(validated)
saved = self.save(transformed)
return saved
@abstractmethod
def validate(self, data):
pass
@abstractmethod
def transform(self, data):
pass
@abstractmethod
def save(self, data):
pass
class UserProcessor(DataProcessor):
def validate(self, data):
assert data.get("name"), "Name required"
return data
def transform(self, data):
return {**data, "name": data["name"].upper()}
def save(self, data):
db.save("users", data)
return data
class ProductProcessor(DataProcessor):
def validate(self, data):
assert data.get("title"), "Title required"
return data
def transform(self, data):
return {**data, "title": data["title"].upper()}
def save(self, data):
db.save("products", data)
return data
Стратегия 7: Утилиты (Utils)
Для небольших повторяющихся операций.
# utils/validators.py
def validate_email(email: str) -> bool:
return "@" in email and "." in email
def validate_phone(phone: str) -> bool:
return len(phone) >= 10 and phone.isdigit()
def normalize_phone(phone: str) -> str:
return phone.replace("-", "").replace(" ", "")
# Использование
from utils.validators import validate_email, validate_phone, normalize_phone
user_email = "alice@example.com"
if validate_email(user_email):
save_user(user_email)
Когда использовать каждый подход
| Ситуация | Подход | Пример |
|---|---|---|
| Просто повторение кода | Функция | Валидация, форматирование |
| Логика вокруг функции | Decorator | Логирование, кеширование, обработка ошибок |
| Классы с общей логикой | Наследование/Композиция | Repository, Service |
| Открытие/закрытие ресурсов | Context Manager | Файлы, БД подключения |
| Алгоритм с вариантами | Template Method | Процессоры, парсеры |
| Утилиты | Модуль utils | Форматирование, валидация |
Чеклист борьбы с дублированием
✅ Если код повторяется 2+ раза — выносите в функцию ✅ Параметризуйте различия — не копируйте и не меняйте ✅ Используйте правильный паттерн — функция, класс, decorator ✅ Тестируйте — повторно используемый код нужно протестировать ✅ Не переусложняйте — KISS (Keep It Simple, Stupid)
Добавлены абстракции должны упростить код, а не усложнить.