Какие есть подходы к разделению бизнес-логики и UI?
Разделение бизнес-логики и UI (презентационного слоя) — ключевой принцип построения устойчивой архитектуры в React Native-приложениях. Это помогает улучшить читаемость, переиспользуемость, масштабируемость и тестируемость кода. Без такого разделения компонент становится монолитным, перегруженным и сложным для поддержки.
Существует несколько подходов и паттернов, позволяющих грамотно разделить UI и бизнес-логику:
1. Container/Presentational Pattern (Smart/Dumb Components)
Это один из самых распространённых способов разделения логики и отображения:
-
Presentational (глупый) компонент — отвечает только за отображение (UI). Получает данные и колбэки через props. Не знает, откуда приходят данные.
-
Container (умный) компонент — содержит бизнес-логику, работает с состоянием, side-effects (например, загрузка данных).
Пример:
// Presentational
const UserProfile = ({ user, onLogout }) => (
<View>
<Text>{user.name}</Text>
<Button onPress={onLogout} title="Logout" />
</View>
);
// Container
const UserProfileContainer = () => {
const user = useSelector(selectUser);
const dispatch = useDispatch();
const handleLogout = () => dispatch(logout());
return <UserProfile user={user} onLogout={handleLogout} />;
};
Плюсы:
-
Простая композиция
-
Повторное использование UI
-
Удобство тестирования UI и логики отдельно
2. Hooks-based Separation (Logic Hooks)
Разделение логики через пользовательские хуки (useXYZ), в которых инкапсулируется состояние и побочные эффекты, а UI остаётся чистым.
Пример:
// useUser.js
export function useUser() {
const user = useSelector(selectUser);
const dispatch = useDispatch();
const logout = () => dispatch(logout());
return { user, logout };
}
// UI-компонент
const UserProfile = () => {
const { user, logout } = useUser();
return (
<View>
<Text>{user.name}</Text>
<Button title="Logout" onPress={logout} />
</View>
);
};
Плюсы:
-
Чёткое отделение логики
-
Повторное использование хуков в разных местах
-
Простой способ разбить большие компоненты
3. MVVM (Model-View-ViewModel)
Этот паттерн особенно подходит для сложных приложений с обширной бизнес-логикой.
-
Model — источники данных: API, хранилища, базы
-
ViewModel — класс или хук, содержащий бизнес-логику
-
View — отображает интерфейс и подписывается на ViewModel
Пример:
// userViewModel.js
export const useUserViewModel = () => {
const { data, isLoading } = useGetUserQuery();
const navigation = useNavigation();
const goToSettings = () => navigation.navigate('Settings');
return {
username: data?.name ?? '',
isLoading,
goToSettings,
};
};
// UserView.tsx
const UserView = () => {
const { username, isLoading, goToSettings } = useUserViewModel();
return (
<View>
<Text>{isLoading ? 'Loading...' : username}</Text>
<Button onPress={goToSettings} title="Settings" />
</View>
);
};
Плюсы:
-
Отделение UI от бизнес-потоков
-
Возможность мока ViewModel в тестах
-
Близко к архитектуре нативных мобильных фреймворков
4. Redux или Zustand как слой логики
State management-библиотеки могут выступать как слой бизнес-логики, предоставляя компонентам только нужные данные и действия.
Redux:
-
Селекторы — для выборки данных
-
Thunks/Sagas — для асинхронной бизнес-логики
-
Dispatchers — для изменения состояния
const CounterView = () => {
const count = useSelector(state => state.counter.value);
const dispatch = useDispatch();
return (
<View>
<Text>{count}</Text>
<Button title="+" onPress={() => dispatch(increment())} />
</View>
);
};
Zustand:
const useStore = create((set) => ({
count: 0,
increment: () => set((s) => ({ count: s.count + 1 })),
}));
const Counter = () => {
const { count, increment } = useStore();
return (
<View>
<Text>{count}</Text>
<Button onPress={increment} title="Add" />
</View>
);
};
Плюсы:
-
Единый источник бизнес-логики
-
Компоненты максимально «тупые»
-
Легко расширять
5. Реализация слоя Use Case / Service
Выделение бизнес-операций в сервисные функции/классы или use-case слои. Это особенно полезно, если приложение взаимодействует с несколькими источниками данных или требует комплексных правил.
// authService.ts
export const authService = {
login: async (email, password) => {
const token = await api.login(email, password);
await AsyncStorage.setItem('token', token);
return token;
},
};
// В компоненте
useEffect(() => {
authService.login(email, password).then(token => {
dispatch(setToken(token));
});
}, \[\]);
Плюсы:
-
Централизация бизнес-правил
-
Упрощает тестирование и замену слоя API
-
Не связан с UI
6. Кастомные HOC (Higher-Order Components)
HOC позволяют оборачивать UI-компоненты логикой и предоставлять им нужные пропсы.
const withUser = (Component) => (props) => {
const user = useSelector(selectUser);
return <Component {...props} user={user} />;
};
const Profile = ({ user }) => <Text>{user.name}</Text>;
export default withUser(Profile);
Плюсы:
-
Переиспользуемость логики
-
Оборачивание UI без изменения его структуры
Минусы:
-
Плохо читается при вложенных HOC
-
Появляется так называемый "wrapper hell"
7. Атомарная архитектура компонентов
Смысл — разбить интерфейс на независимые, изолированные компоненты:
-
Atoms — кнопки, иконки, поля ввода
-
Molecules — группы атомов: карточка, форма
-
Organisms — большие UI-блоки с логикой
-
Pages — полноценные экраны, содержащие организмов
Такой подход часто используется с Atomic Design и Design Systems.
8. Разделение через слои в структуре проекта
Пример структуры:
/src
/components (UI)
Button.tsx
Card.tsx
/hooks (логика)
useAuth.ts
useCart.ts
/services
api.ts
storage.ts
/features
/user
UserView.tsx
useUser.ts
userSlice.ts
Плюсы:
-
Чистая структура
-
Чёткое разделение ответственности
-
Масштабируемость при росте проекта
Разделение бизнес-логики от UI — основа надёжного, поддерживаемого приложения. Каждый из описанных подходов может применяться в зависимости от размера проекта, предпочтений команды и требований архитектуры. Часто эффективной оказывается комбинация нескольких стратегий: например, хранилище состояния (Redux) + ViewModel хуки + UI компоненты.