\n\n\n\n\n\n```\n\n**Пример 3: State management в Zustand**\n\n```javascript\nimport { create } from 'zustand';\n\n// CQS в Zustand\nconst useStore = create((set, get) => ({\n // State\n items: [],\n filter: 'all',\n \n // Commands - изменяют состояние\n addItem: (item) => set((state) => ({\n items: [...state.items, item]\n })),\n \n removeItem: (id) => set((state) => ({\n items: state.items.filter(i => i.id !== id)\n })),\n \n setFilter: (filter) => set({ filter }),\n \n // Queries - только читают\n getItems: () => get().items,\n \n getFilteredItems: () => {\n const state = get();\n if (state.filter === 'completed') {\n return state.items.filter(i => i.completed);\n }\n return state.items;\n },\n \n getItemCount: () => get().items.length,\n \n hasItems: () => get().items.length > 0\n}));\n\n// Использование\nconst store = useStore();\n\n// Command\nstore.addItem({ id: 1, text: 'Learn CQS', completed: false });\nstore.setFilter('completed');\n\n// Query\nconst count = store.getItemCount();\nconst filtered = store.getFilteredItems();\n```\n\n### Преимущества CQS\n\n1. **Ясность** - сразу видно что делает функция\n2. **Тестируемость** - queries и commands тестируются отдельно\n3. **Безопасность** - queries безопасны вызывать много раз\n4. **Отладка** - легче найти где изменилось состояние\n5. **Масштабируемость** - лучше работает с большими приложениями\n6. **Переиспользование** - queries можно вызывать откуда угодно без опасений\n\n### Когда НЕ использовать CQS\n\n- Простые функции где разделение затруднит код\n- Getter свойства в классах\n- Функции которые логически неделимы\n\nNO absolute rule - баланс между ясностью и простотой.\n\nCommand Query Separation - это мощный принцип архитектуры, который делает код более предсказуемым, тестируемым и поддерживаемым.","dateCreated":"2026-04-02T22:13:25.553908","upvoteCount":0,"author":{"@type":"Person","name":"claude-haiku-4.5"}}}}
← Назад к вопросам

Что такое Command Query Separation?

1.8 Middle🔥 191 комментариев
#JavaScript Core

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

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

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

Command Query Separation (CQS): принцип разделения операций

Command Query Separation - это архитектурный принцип, который разделяет операции на две категории: команды (которые изменяют состояние) и запросы (которые только читают данные), и запрещает смешивать их.

Основная идея

Command - операция которая:

  • Изменяет состояние системы
  • Имеет побочные эффекты (side effects)
  • НЕ возвращает значение (или возвращает результат выполнения)

Query - операция которая:

  • ТОЛЬКО читает данные
  • НЕ изменяет состояние
  • Безопасна для многократного вызова (idempotent)
  • Всегда возвращает одинаковый результат
// НЕПРАВИЛЬНО - смешиваем Command и Query
function addItemAndGetTotal(item) {
  this.items.push(item);  // <- Command (изменение)
  return this.items.reduce((sum, i) => sum + i.price, 0); // <- Query (чтение)
}

// ПРАВИЛЬНО - разделяем
function addItem(item) {  // <- Command
  this.items.push(item);
}

function getTotal() {     // <- Query
  return this.items.reduce((sum, i) => sum + i.price, 0);
}

Проблемы когда CQS не соблюдается

1. Сложно предсказать поведение

// ПЛОХО - неясно что произойдёт
const result = fetchUserAndUpdateCache(userId);
// Загрузил ли юзера? Обновил кэш? Возвращает что?

// ХОРОШО - ясный контракт
const user = fetchUser(userId);        // Query - только читает
updateUserCache(userId, user);         // Command - изменяет состояние

2. Трудно тестировать

// ПЛОХО - функция делает много
function processOrder(orderId) {
  const order = database.find(orderId);        // Query
  order.status = 'processing';                 // Command
  database.save(order);                        // Command
  sendEmail(order.customerEmail);              // Command
  updateInventory(order.items);                // Command
  return order.id;                             // Возвращает ID
}

// Тестировать сложно - нужно мокировать БД, email, inventory

// ХОРОШО - разделены
function getOrder(orderId) {                   // Query
  return database.find(orderId);
}

function markOrderAsProcessing(order) {        // Command
  order.status = 'processing';
  database.save(order);
}

function notifyOrderProcessing(order) {        // Command
  sendEmail(order.customerEmail, 'processing');
  updateInventory(order.items);
}

// Тестируются отдельно и просто

3. Сложно использовать в многопоточной среде

// ПЛОХО - race condition
function incrementAndGet() {
  this.count++;          // Command
  return this.count;     // Query
}

// Если два потока вызовут одновременно:
// Thread 1: count = 0, increment -> 1, return 1
// Thread 2: count = 0, increment -> 1, return 1
// Оба получили 1, хотя ожидали 1 и 2

// ХОРОШО - разделены
function increment() {     // Command
  this.count++;
}

function getCount() {      // Query
  return this.count;
}

CQS в Frontend приложениях

1. React компоненты - правильное разделение

// ПЛОХО - компонент читает и пишет одновременно
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  
  const loadAndUpdate = async () => {
    const userData = await fetch(`/api/users/${userId}`);
    setUser(userData);  // Команда - обновляет состояние
    return userData;    // Query - возвращает данные
  };
  
  // Непонятно когда вызывать
}

// ХОРОШО - разделяем
function UserProfile({ userId }) {
  const user = useQuery(['user', userId], () => 
    fetch(`/api/users/${userId}`).then(r => r.json())
  );
  // useQuery обрабатывает всю логику
}

// Или вручную:
function loadUser(userId) {           // Query - загружает данные
  return fetch(`/api/users/${userId}`).then(r => r.json());
}

function updateUserInUI(user) {        // Command - обновляет UI
  setUser(user);
}

2. Redux - встроенный CQS

Redux уже следует CQS:

// Query - selectors (читают состояние)
const selectUser = (state, userId) => state.users[userId];
const selectUserCount = (state) => Object.keys(state.users).length;

// Command - actions (изменяют состояние)
const ADD_USER = 'ADD_USER';
const UPDATE_USER = 'UPDATE_USER';
const DELETE_USER = 'DELETE_USER';

// Reducer - чистая функция, применяет команды
function userReducer(state = {}, action) {
  switch(action.type) {
    case ADD_USER:
      return { ...state, [action.payload.id]: action.payload };
    case UPDATE_USER:
      return { ...state, [action.payload.id]: action.payload };
    default:
      return state;
  }
}

// Использование
const user = selectUser(getState(), userId);  // Query
dispatch({ type: ADD_USER, payload: newUser }); // Command

3. API дизайн

// ПЛОХО - смешиваем
app.post('/api/items/:id/increment', (req, res) => {
  const item = items[id];
  item.count++;  // Command
  res.json(item);  // Query - возвращает данные
});

// ХОРОШО - разделяем
app.post('/api/items/:id/increment', (req, res) => {
  // Command - изменяем состояние
  items[id].count++;
  res.send({ success: true });
});

app.get('/api/items/:id', (req, res) => {
  // Query - только читаем
  res.json(items[id]);
});

// Использование на клиенте
await fetch(`/api/items/${id}/increment`, { method: 'POST' }); // Command
const item = await fetch(`/api/items/${id}`).then(r => r.json()); // Query

Практические примеры

Пример 1: Управление корзиной товаров

// НЕПРАВИЛЬНО
class ShoppingCart {
  addItemAndGetTotal(item) {  // Смешиваем!
    this.items.push(item);
    return this.calculateTotal();
  }
}

// ПРАВИЛЬНО
class ShoppingCart {
  // Commands - изменяют состояние
  addItem(item) {
    this.items.push(item);
  }
  
  removeItem(itemId) {
    this.items = this.items.filter(i => i.id !== itemId);
  }
  
  updateQuantity(itemId, quantity) {
    const item = this.items.find(i => i.id === itemId);
    if (item) item.quantity = quantity;
  }
  
  // Queries - только читают
  getItems() {
    return [...this.items]; // Возвращаем копию
  }
  
  getTotal() {
    return this.items.reduce((sum, i) => sum + i.price * i.quantity, 0);
  }
  
  getItemCount() {
    return this.items.length;
  }
  
  isEmpty() {
    return this.items.length === 0;
  }
}

// Использование
const cart = new ShoppingCart();

// Commands
cart.addItem({ id: 1, name: 'Book', price: 10, quantity: 1 });
cart.addItem({ id: 2, name: 'Pen', price: 2, quantity: 3 });
cart.updateQuantity(1, 2);

// Queries
console.log(cart.getTotal());     // 26
console.log(cart.getItemCount()); // 2
console.log(cart.getItems());      // [...]

Пример 2: Компонент фильтрации

<!-- НЕПРАВИЛЬНО -->
<template>
  <div>
    <button @click="applyFilterAndLoad()">Apply</button>
    <ProductList :products="products" />
  </div>
</template>

<script>
export default {
  data() {
    return { products: [], filters: {} };
  },
  methods: {
    async applyFilterAndLoad() {  // Смешиваем!
      // Command - применяем фильтр
      this.filters = { ...this.newFilters };
      
      // Command - загружаем данные
      const response = await fetch(`/api/products?${new URLSearchParams(this.filters)}`);
      
      // Command - обновляем состояние
      this.products = await response.json();
      
      // Что возвращаем?
      return this.products;
    }
  }
};
</script>

<!-- ПРАВИЛЬНО -->
<template>
  <div>
    <FilterPanel 
      :filters="filters"
      @change="setFilters"
    />
    
    <button @click="loadProducts">Apply</button>
    
    <ProductList :products="products" />
  </div>
</template>

<script>
export default {
  data() {
    return { products: [], filters: {} };
  },
  
  methods: {
    // Command - только изменяет фильтры
    setFilters(newFilters) {
      this.filters = { ...newFilters };
    },
    
    // Command - загружает данные и обновляет состояние
    async loadProducts() {
      const response = await fetch(
        `/api/products?${new URLSearchParams(this.filters)}`
      );
      this.products = await response.json();
    },
    
    // Query - только возвращает данные (обычно не нужно)
    getFilteredProductCount() {
      return this.products.length;
    }
  }
};
</script>

Пример 3: State management в Zustand

import { create } from 'zustand';

// CQS в Zustand
const useStore = create((set, get) => ({
  // State
  items: [],
  filter: 'all',
  
  // Commands - изменяют состояние
  addItem: (item) => set((state) => ({
    items: [...state.items, item]
  })),
  
  removeItem: (id) => set((state) => ({
    items: state.items.filter(i => i.id !== id)
  })),
  
  setFilter: (filter) => set({ filter }),
  
  // Queries - только читают
  getItems: () => get().items,
  
  getFilteredItems: () => {
    const state = get();
    if (state.filter === 'completed') {
      return state.items.filter(i => i.completed);
    }
    return state.items;
  },
  
  getItemCount: () => get().items.length,
  
  hasItems: () => get().items.length > 0
}));

// Использование
const store = useStore();

// Command
store.addItem({ id: 1, text: 'Learn CQS', completed: false });
store.setFilter('completed');

// Query
const count = store.getItemCount();
const filtered = store.getFilteredItems();

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

  1. Ясность - сразу видно что делает функция
  2. Тестируемость - queries и commands тестируются отдельно
  3. Безопасность - queries безопасны вызывать много раз
  4. Отладка - легче найти где изменилось состояние
  5. Масштабируемость - лучше работает с большими приложениями
  6. Переиспользование - queries можно вызывать откуда угодно без опасений

Когда НЕ использовать CQS

  • Простые функции где разделение затруднит код
  • Getter свойства в классах
  • Функции которые логически неделимы

NO absolute rule - баланс между ясностью и простотой.

Command Query Separation - это мощный принцип архитектуры, который делает код более предсказуемым, тестируемым и поддерживаемым.

Что такое Command Query Separation? | PrepBro