Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
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
- Ясность - сразу видно что делает функция
- Тестируемость - queries и commands тестируются отдельно
- Безопасность - queries безопасны вызывать много раз
- Отладка - легче найти где изменилось состояние
- Масштабируемость - лучше работает с большими приложениями
- Переиспользование - queries можно вызывать откуда угодно без опасений
Когда НЕ использовать CQS
- Простые функции где разделение затруднит код
- Getter свойства в классах
- Функции которые логически неделимы
NO absolute rule - баланс между ясностью и простотой.
Command Query Separation - это мощный принцип архитектуры, который делает код более предсказуемым, тестируемым и поддерживаемым.