Пример приложения, собранного по трёхслойной архитектуре.
В коде есть несколько компромиссов, о которых я рассказывал в докладе на Frontend Crew.
Shared Kernel — это тот код и те данные, от которых могут зависеть любые модули, но зависимость от которых не повышает зацепление, поэтому сюда можно помещать не любой код. Подробнее об ограничениях и применении хорошо описано в статье “DDD, Hexagonal, Onion, Clean, CQRS, … How I put it all together”.
В этом приложении shared kernel включает в себя аннотации типов, которые могут быть доступны где и кому угодно. Такие типы собраны в shared-kernel.d.ts
.
В функции createOrder
используется «библиотечная» функция currentDatetime
для указания даты создания заказа. Это не совсем корректно, потому что домен не должен ни от чего зависеть.
По-хорошему, реализация типа Order
должна быть классом, аргументами конструктора которого были бы все необходимые данные, включая дату. А создание этого класса находилось бы в прикладном слое в orderProducts
:
async function orderProducts(user: User, { products }: Cart) {
const datetime = currentDatetime();
const order = new Order(user, cart, datetime);
// ...
}
Сама функция создания заказа orderProduct
не зависит от фреймворка и может быть использована и протестирована в отрыве от Реакта. Хук-обёртка используется лишь для предоставления юз-кейса компонентам и внедрения сервисов в сам юз-кейс.
В каноническом исполнении функция юз-кейса была бы вынесена за пределы хука, а сервисы были бы переданы юз-кейсу через последний аргумент или с помощью DI:
type Dependencies = {
notifier?: NotificationService;
payment?: PaymentService;
orderStorage?: OrderStorageService;
};
async function orderProducts(
user: User,
cart: Cart,
dependencies: Dependencies = defaultDependencies
) {
const { notifier, payment, orderStorage } = dependencies;
// ...
}
Хук в этом случае превратился бы в адаптер:
function useOrderProducts() {
const notifier = useNotifier();
const payment = usePayment();
const orderStorage = useOrdersStorage();
return (user: User, cart: Cart) =>
orderProducts(user, cart, {
notifier,
payment,
orderStorage,
});
}
В докладе я обращаю внимание этот момент и поясняю, как будет сделать правильно с точки зрения чистоты подхода. В исходниках же я посчитал, что это необязательно, так как отвлекало бы от сути. Также это — один из «срезанных углов», о которых я упоминаю в начале доклада.
В прикладном слое мы «внедряем» сервисы руками:
export function useAuthenticate() {
const storage: UserStorageService = useUserStorage();
const auth: AuthenticationService = useAuth();
// ...
}
По-хорошему, это должно быть автоматизировано и сделано через внедрение зависимостей. В случае с Реактом и хуками мы, в принципе, можем использовать их, как «контейнер», который возвращает реализацию указанного интерфейса.
В конкретно этом приложении настраивать DI особо смысла не было, потому что это бы отвлекало от сути архитектуры.