О нас Руководства Проекты Контакты
Админка
пожалуйста подождите

Business objects (BOs) — это фундаментальный паттерн разработки enterprise-приложений, который инкапсулирует данные и бизнес-логику в переиспользуемой и поддерживаемой структуре. Решение о том, когда и как внедрять business objects, может как обеспечить, так и подорвать долгосрочную жизнеспособность проекта. В этом руководстве рассматривается проектирование эффективной архитектуры business object с точки зрения senior-разработчика.

Зачем нужны Business Objects

Business objects обеспечивают:

  1. Абстракцию: отделение бизнес-логики от инфраструктуры
  2. Переиспользуемость: одна и та же логика для нескольких интерфейсов
  3. Валидацию: централизованные правила целостности данных
  4. Тестируемость: изолированные единицы для тестирования
  5. Документацию: самодокументируемую доменную модель

Когда Business Objects имеют смысл

Хорошо подходит: сложные enterprise-системы

Рассмотрим систему управления документами для крупных организаций:

  • Несколько типов сущностей: документы, пользователи, права доступа, workflow
  • Сложные связи: документы относятся к категориям, имеют версии, требуют согласований
  • Бизнес-правила: валидация, переходы состояний, контроль доступа
  • Несколько интерфейсов: Web UI, мобильное приложение, API, пакетная обработка

Здесь business objects обеспечивают:

┌─────────────────────────────────────────────────────────┐
│ User Interface │
├─────────────────────────────────────────────────────────┤
│ Business Objects │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Document │ │ Workflow │ │ User │ │
│ │ - validate│ │ - approve│ │ - auth │ │
│ │ - version │ │ - reject │ │ - perms │ │
│ └──────────┘ └──────────┘ └──────────┘ │
├─────────────────────────────────────────────────────────┤
│ Data Access Layer (ORM) │
├─────────────────────────────────────────────────────────┤
│ Database │
└─────────────────────────────────────────────────────────┘

Плохо подходит: простая обработка данных

Для быстрого сервиса обработки SMS:

  • Простая модель данных: 10 полей, минимальные связи
  • Простые операции: принять, провалидировать, сохранить
  • Один интерфейс: API endpoint
  • Нет сложных бизнес-правил

Использование полноценного BO-фреймворка (например, Symfony с Propel) требует создания 40+ файлов для простых CRUD-операций. Накладные расходы превышают выгоды.

Архитектурные решения

Фреймворк или кастомная реализация

Используйте фреймворк, когда:

  • команда уже его знает
  • у проекта долгий срок жизни (5+ лет)
  • поддержкой будут заниматься несколько разработчиков
  • стандартные паттерны соответствуют вашим потребностям

Делайте кастомную реализацию, когда:

  • уникальные требования не укладываются во фреймворки
  • производительность критична
  • у команды глубокая доменная экспертиза
  • приложение простое и сфокусированное

Соображения по code generation

Многие BO-фреймворки используют code generators (Propel, Entity Framework и т. д.):

Плюсы:

  • быстрое стартовое развитие
  • единообразные паттерны
  • меньше boilerplate-кода

Минусы:

  • изменения схемы становятся болезненными
  • сгенерированный код часто требует кастомизации
  • обновления генератора могут ломать расширения
  • подход «копировать-вставить» снижает уровень осмысления

Лучшая практика: предпочитайте наследование и композицию вместо модификации сгенерированного кода:

// Сгенерированный базовый класс (не изменять)
abstract class BaseDocument {
protected $id;
protected $title;
protected $content;
// Сгенерированные геттеры/сеттеры
}
// Ваши кастомизации
class Document extends BaseDocument {
public function validate(): array {
$errors = [];
if (empty($this->title)) {
$errors[] = 'Title is required';
}
if (strlen($this->content) < 100) {
$errors[] = 'Content must be at least 100 characters';
}
return $errors;
}
public function publish(): void {
if (!$this->validate()) {
$this->status = 'published';
$this->publishedAt = new DateTime();
}
}
}

Проектирование Business Objects

Принцип единственной ответственности

У каждого BO должна быть одна причина для изменения:

// Плохо: Document делает слишком много
class Document {
public function save() { /*database*/ }
public function sendEmail() { /*notification*/ }
public function generatePDF() { /*export*/ }
public function validate() { /*rules*/ }
}
// Хорошо: ответственности разделены
class Document {
public function validate(): bool { /*только валидация*/ }
public function getData(): array { /*только доступ к данным*/ }
}
class DocumentRepository {
public function save(Document $doc): void { /*persistence*/ }
}
class DocumentNotifier {
public function notify(Document $doc): void { /*email*/ }
}
class DocumentExporter {
public function toPDF(Document $doc): string { /*export*/ }
}

Богатая vs. анемичная доменная модель

Анемичная модель (только контейнеры данных):

class Order {
public int $id;
public string $status;
public float $total;
public array $items;
}
// Логика в service
class OrderService {
public function calculateTotal(Order $order): float {
return array_sum(array_map(
fn($item) => $item->price * $item->quantity,
$order->items
));
}
public function canCancel(Order $order): bool {
return $order->status === 'pending';
}
}

Богатая модель (инкапсулированное поведение):

class Order {
private int $id;
private string $status;
private array $items = [];
public function addItem(Product $product, int $quantity): void {
if ($this->status !== 'draft') {
throw new DomainException('Cannot modify submitted order');
}
$this->items[] = new OrderItem($product, $quantity);
}
public function getTotal(): float {
return array_sum(array_map(
fn($item) => $item->getSubtotal(),
$this->items
));
}
public function submit(): void {
if (empty($this->items)) {
throw new DomainException('Cannot submit empty order');
}
$this->status = 'pending';
}
public function cancel(): void {
if ($this->status !== 'pending') {
throw new DomainException('Only pending orders can be cancelled');
}
$this->status = 'cancelled';
}
}

Выбирайте богатую модель, когда:

  • сложные переходы состояний
  • необходимо поддерживать инварианты
  • бизнес-правила специфичны для домена

Выбирайте анемичную модель, когда:

  • простые CRUD-операции
  • бизнес-логика процедурная
  • команда предпочитает transaction scripts

Value Objects

Неизменяемые объекты, представляющие концепции без идентичности:

class Money {
private readonly float $amount;
private readonly string $currency;
public function __construct(float $amount, string $currency) {
if ($amount < 0) {
throw new InvalidArgumentException('Amount cannot be negative');
}
$this->amount = $amount;
$this->currency = $currency;
}
public function add(Money $other): Money {
if ($this->currency !== $other->currency) {
throw new DomainException('Cannot add different currencies');
}
return new Money($this->amount + $other->amount, $this->currency);
}
public function multiply(float $factor): Money {
return new Money($this->amount * $factor, $this->currency);
}
public function equals(Money $other): bool {
return $this->amount === $other->amount
&& $this->currency === $other->currency;
}
}
class Address {
public function __construct(
public readonly string $street,
public readonly string $city,
public readonly string $postalCode,
public readonly string $country
) {}
public function format(): string {
return "{$this->street}\n{$this->city}, {$this->postalCode}\n{$this->country}";
}
}

Aggregate Roots

Группируйте связанные объекты и задавайте границы:

class Order { // Корень aggregate
private array $items = [];
public function addItem(Product $product, int $qty): void {
// Весь доступ к OrderItems осуществляется через Order
$this->items[] = new OrderItem($product, $qty);
}
public function removeItem(int $itemIndex): void {
unset($this->items[$itemIndex]);
}
// OrderItem недоступен напрямую
}
// Repository работает только с корнем aggregate
interface OrderRepository {
public function save(Order $order): void; // Сохраняет Order + Items
public function findById(int $id): ?Order; // Загружает Order + Items
}

Работа со сквозными аспектами

Валидация

interface Validatable {
public function validate(): ValidationResult;
}
class ValidationResult {
private array $errors = [];
public function addError(string $field, string $message): void {
$this->errors[$field][] = $message;
}
public function isValid(): bool {
return empty($this->errors);
}
public function getErrors(): array {
return $this->errors;
}
}
class Document implements Validatable {
public function validate(): ValidationResult {
$result = new ValidationResult();
if (empty($this->title)) {
$result->addError('title', 'Title is required');
}
if (strlen($this->title) > 255) {
$result->addError('title', 'Title must be under 255 characters');
}
return $result;
}
}

Domain Events

interface DomainEvent {
public function occurredAt(): DateTimeImmutable;
}
class OrderSubmitted implements DomainEvent {
public function __construct(
public readonly int $orderId,
public readonly float $total,
private DateTimeImmutable $occurredAt = new DateTimeImmutable()
) {}
public function occurredAt(): DateTimeImmutable {
return $this->occurredAt;
}
}
class Order {
private array $domainEvents = [];
public function submit(): void {
$this->status = 'pending';
$this->domainEvents[] = new OrderSubmitted($this->id, $this->getTotal());
}
public function pullDomainEvents(): array {
$events = $this->domainEvents;
$this->domainEvents = [];
return $events;
}
}

Соображения по производительности

Lazy Loading

class Order {
private ?array $items = null;
private ItemRepository $itemRepo;
public function getItems(): array {
if ($this->items === null) {
$this->items = $this->itemRepo->findByOrderId($this->id);
}
return $this->items;
}
}

Bulk Operations

// Плохо: N+1 запросов
foreach ($orders as $order) {
$order->save(); // Один запрос на каждый order
}
// Хорошо: batch-операции
$orderRepository->saveAll($orders); // Один запрос

Ключевые выводы

  1. Соотносите сложность с потребностями: не переусложняйте простые приложения
  2. Инкапсулируйте поведение: богатые доменные модели для сложной бизнес-логики
  3. Используйте value objects: неизменяемые объекты для концепций без идентичности
  4. Определяйте границы aggregate: контролируйте согласованность и транзакции
  5. Разделяйте ответственность: repositories, services и domain objects
  6. Учитывайте производительность: lazy loading, batching, caching

Архитектура business object требует баланса между абстракцией и практичностью — выбирайте паттерны, которые решают ваши реальные проблемы, а не теоретические.

 
 
 
Языки
Темы
Copyright © 1999 — 2026
Зетка Интерактив