← Назад к вопросам

Где применяется Command Query Separation?

3.0 Senior🔥 102 комментариев
#Архитектура и паттерны

Комментарии (2)

🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Command Query Separation (CQS) в разработке

Command Query Separation (CQS) — архитектурный принцип, который разделяет операции на две категории:

  • Command — операции, которые изменяют состояние (side effects), но не возвращают данные
  • Query — операции, которые возвращают данные без изменения состояния

Основной принцип: функция должна либо изменять состояние, либо возвращать информацию, но не делать оба одновременно.

Где применяется CQS

1. Backend API и REST

Разделение методов HTTP:

// Query: GET — получить данные, не меняя состояние
GET /api/v1/users/123
GET /api/v1/posts?limit=10

// Command: POST, PUT, DELETE — изменить состояние
POST /api/v1/users (создать)
PUT /api/v1/users/123 (обновить)
DELETE /api/v1/users/123 (удалить)

Плохо: нарушение CQS

// Одна операция делает всё: возвращает и меняет
GET /api/v1/users/123/activate

// Непредсказуемо: это query или command?

Хорошо: CQS разделение

// Query: только получение
GET /api/v1/users/123

// Command: изменение
PATCH /api/v1/users/123 { "status": "active" }

2. JavaScript/TypeScript функции и методы

Query функция — возвращает данные, не меняет состояние:

class UserService {
  // Query: чистая функция
  getUser(id) {
    return this.users.find(u => u.id === id);
  }

  getActiveUsers() {
    return this.users.filter(u => u.active);
  }

  getTotalUsers() {
    return this.users.length;
  }
}

// Использование
const user = userService.getUser(123);
const count = userService.getTotalUsers();

Command функция — изменяет состояние, не возвращает данные:

class UserService {
  // Command: меняет состояние, ничего не возвращает (или возвращает статус)
  createUser(data) {
    const user = { id: generateId(), ...data };
    this.users.push(user);
    return { success: true }; // или ничего не возвращает
  }

  updateUser(id, data) {
    const user = this.getUser(id);
    Object.assign(user, data);
    return { success: true };
  }

  deleteUser(id) {
    this.users = this.users.filter(u => u.id !== id);
    return { success: true };
  }
}

Плохо: нарушение CQS

// Функция и возвращает, и меняет состояние
fetchAndActivateUser(id) {
  const user = this.getUser(id);
  user.active = true;
  this.saveUser(user);
  return user; // Возвращает и меняет состояние
}

3. React компоненты и состояние

Queries в React:

function UserProfile({ userId }) {
  // Query: получение данных
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    // Query function
    fetch(`/api/users/${userId}`)
      .then(r => r.json())
      .then(data => setUser(data)); // Только setUser
  }, [userId]);

  return <div>{user?.name}</div>;
}

Commands в React:

function UserForm({ userId }) {
  // Command: изменение данных
  const handleSubmit = async (data) => {
    // Command: POST/PUT запрос
    const response = await fetch(`/api/users/${userId}`, {
      method: 'PUT',
      body: JSON.stringify(data)
    });
    return response.json();
  };

  return <form onSubmit={handleSubmit}>...</form>;
}

4. CQRS паттерн (Command Query Responsibility Segregation)

Расширенная версия CQS для сложных систем:

// Модель с разделением ответственности
class UserAggregate {
  // Command handlers — меняют состояние
  registerUser(command) {
    // RegisterUserCommand { name, email }
    const user = new User(command.name, command.email);
    this.users.push(user);
    // Издаем событие
    this.emit(new UserRegisteredEvent(user.id, user.name));
  }

  updateUserProfile(command) {
    // UpdateProfileCommand { userId, bio, avatar }
    const user = this.getUser(command.userId);
    user.bio = command.bio;
    user.avatar = command.avatar;
    this.emit(new UserProfileUpdatedEvent(user.id));
  }

  // Query handlers — возвращают данные без changes
  getUserById(query) {
    return this.getUser(query.userId);
  }

  getActiveUsers(query) {
    return this.users.filter(u => u.active);
  }
}

Отдельные классы для Commands и Queries:

// Command
class CreateUserCommand {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }
}

// Query
class GetUserQuery {
  constructor(userId) {
    this.userId = userId;
  }
}

// Handler для Command
class CreateUserCommandHandler {
  handle(command) {
    const user = new User(command.name, command.email);
    userRepository.save(user);
    return { success: true, userId: user.id };
  }
}

// Handler для Query
class GetUserQueryHandler {
  handle(query) {
    return userRepository.findById(query.userId);
  }
}

5. Форма с CQS принципом

Плохо: нарушение CQS

// Одна функция и отправляет, и получает результат с побочными эффектами
async function submitAndDisplayUser(formData) {
  const response = await fetch('/api/users', {
    method: 'POST',
    body: JSON.stringify(formData)
  });
  const user = await response.json();
  
  // Побочные эффекты
  localStorage.setItem('lastUser', JSON.stringify(user));
  showNotification(`Пользователь ${user.name} создан`);
  redirectTo(`/users/${user.id}`);
  
  return user; // Возвращает результат
}

Хорошо: CQS разделение

// Command: только отправить данные
async function createUser(formData) {
  const response = await fetch('/api/users', {
    method: 'POST',
    body: JSON.stringify(formData)
  });
  return response.json();
}

// Query: получить созданного пользователя
async function getUser(id) {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
}

// Обработчик в компоненте использует оба
async function handleFormSubmit(formData) {
  // Command
  const newUser = await createUser(formData);
  
  // Query (если нужна полная информация)
  const fullUser = await getUser(newUser.id);
  
  // Побочные эффекты отделены
  localStorage.setItem('lastUser', JSON.stringify(fullUser));
  showNotification(`Пользователь ${fullUser.name} создан`);
  redirectTo(`/users/${fullUser.id}`);
}

6. Redux/Zustand с CQS

// Command: изменение состояния
const userSlice = createSlice({
  name: 'users',
  initialState: { list: [] },
  reducers: {
    // Commands
    addUser: (state, action) => {
      state.list.push(action.payload);
    },
    updateUser: (state, action) => {
      const user = state.list.find(u => u.id === action.payload.id);
      if (user) Object.assign(user, action.payload);
    },
    deleteUser: (state, action) => {
      state.list = state.list.filter(u => u.id !== action.payload);
    }
  },
  selectors: {
    // Queries
    selectUsers: (state) => state.list,
    selectUserById: (state, id) => state.list.find(u => u.id === id),
    selectActiveUsers: (state) => state.list.filter(u => u.active)
  }
});

Преимущества CQS

  1. Ясность и предсказуемость — понятно, что делает функция
  2. Легче тестировать — query функции чистые, command функции легче мокировать
  3. Избежание побочных эффектов — query функции без side effects
  4. Масштабируемость — легче оптимизировать reads и writes отдельно
  5. Параллелизм — query операции можно выполнять параллельно
  6. Документирование — имена функций ясны: getUserById vs updateUser

Когда применять CQS

  • REST API — разделение GET/POST/PUT/DELETE методов
  • Сложные бизнес-операции — CQRS для микросервисов
  • Работа с состоянием — Redux, MobX, Zustand
  • Сервисы и репозитории — Backend логика
  • React компоненты — обработчики форм и запросов

Ключевые моменты

  • CQS — функция либо меняет состояние, либо возвращает данные
  • CQRS — расширенная версия для больших систем
  • Query функции — чистые, без побочных эффектов
  • Command функции — меняют состояние, минимум возвращаемых данных
  • HTTP методы — GET (query), POST/PUT/DELETE (commands)
  • Применение везде — API, функции, компоненты, store