Comments 25
По-моему, нужно идти в другую сторону — к упрощению архитектуры проекта, а не придумывать 100500 волшебных способов решения проблем, которых вообще не должно возникать на frontend-е.
Упрощение архитектуры в данном случае вижу в максимальном упрощении и ускорении получения информации об организации. Скорее всего, это реализуется силами backend-а, кешированием и прочим.
После манипуляций с провайдерами, компоненту вообще стало пофиг откуда организации берутся и как обновляются. Компонент теперь зависит только от Организаций, а не способа их получения. (Я предпочитаю использовать State + Resolver, чтобы на роут юзера перебрасывало только после загрузки данных. Но это дело даже не вкуса, а UX.)
Другое дело, что boilerplate не очень приятный получается вокруг провайдеров, но это отдельная проблема.
Я участвовал во нескольких больших проектах на ng и видел слишком много примеров, когда код компонента крайне усложняется тем, что нужно по каким-то параметрам получить данные из нескольких источников. Три уровня вложенности .subscribe() — лишь меньшее из виденных ужасов.
У нас провайдеры хорошо упрощают код в кейсах, когда компонент зависит именно от данных. Для других случаев мы используем как обычные сервисы, так и разные вроде Observabled-based или Subject-based варианты, как в статье, ссылка на которой в комментарии ниже
Ну или еще проще, обычный подчиненный сервис, который инжектит локальный роут и апи-сервис и предоставляет метод для получения сущности, зависимой от роута.
Это чище читается, чем FactoryProvider.
Но могу сказать что у этого подхода есть и минус. При чтении кода не получится быстро определить, какие сущности зависят от роута, приходится перебирать все сервисы и смотреть где что инжектится.
Возможно самый читабельный вариант это просто
selectOrganizatons(route: ActivatedRoute): Observable<Organization[]>
Приведенный в статье кейс очень простой, но это плата за лаконичность статьи. Сложный кейс мог бы дать один конкретный хороший пример, но замылыть суть самой идеи. Я выбрал первый вариант
С человеком, статью которого вы привели в пример, мы как раз работаем в одной команде и активно используем оба подхода: для некоторых кейсов лучше ложиться одно решение, для некоторых — другое.
Возможно, не до конца вышло донести мысль, что это лишь один из альтернативных подходов и точно не заменяющий все остальные :(
Решали следующую логику: Есть таблица данных(или иное представление), с возможностью перехода в форму элемента. Переход может осуществляться как полный(средствами роутера), в модальном окне, сайд баром — определяется настройками пользователем. В одной точке в зависимости от настроек происходит подмена сервиса, который выполняет переходы. Тем самым избавляем необходимость проталкивать логику выбора в другие компоненты. Подмена поведения логики с помощью DI одна из сильных качеств)
Обычно формы живут в рамках одного компонента, поэтому с ними таких вопросов не возникает.
Бывают случаи сложнее, когда форма большая и хочется ее разнести. Помню был случай, когда сложный компонент вроде выпадающего календаря нуждался в данных контрола, но с дефолтным значением. Тоже решали частными провайдерами и вот такой штукой в useFactory: https://twitter.com/marsibarsi/status/1269201441756979200
Сколько не читал статей о DI, всё равно для меня не понятно как это всё работает. Фабрики какие-то, инжекторы. Я в observable быстрее въехал и активно использую. Может когда-то пойму…
Могу порекомендовать тут скринкаст Степана Суворова на ютюбе, там базовые штуки: www.youtube.com/watch?v=bJvBRJUytgg&list=PLDyvV36pndZF-vwsVB48ivZyNJ4ETBKNY&index=14
Более фундаментально про саму концепцию IoC и конкретно DI есть интересная серия видео у Ильи Климова:
www.youtube.com/watch?v=ETyltCwtQHs&list=PLvTBThJr861xKTf1x6P49MwN6yoN4v69k
На почитать могу собственный материал порекомендовать: первая глава в angular.institute. Только там на английском, надеюсь не будет проблемой
Далее в компоненте в массив providers просто добавляется ORGANIZATION_PROVIDERS. Без spread-оператора.
Это работает, т.к. providers видимо поддерживает работу с массивами.
Проблема вылезает, когда нужно сделать тест на такой компонент и переопределить провайдер: overrideComponent похоже не поддерживает массивы в providers. Со spread'ом работает.
Вопрос, собственно, следующий — не правильнее ли тогда везде использовать массив провайдеров со spread? Как вы решали подобные проблемы?
Вопросы
Автору
1) Обычно выносят в файлы тот код который планируют переиспользовать — я правильно понимаю что этот код(провайдер) будет использоваться только в одном компоненте?
2) С коллегами пообсуждали статью и возник еще вопрос. А не будет ли проблем с отловом ошибок если прописали не тот интерфейс в фабрике или месте инжектирования
3) не будет ли сложно поддерживать/использовать такой код?
Комментаторам
1) инжектируя работу роут как параметр разве мы не увеличиваем связанность разных классов — веть сервисы нужны для работы с данными/бизнесовой логикой (то есть даем ему id и получаем список организаций для этого id, сервис не должен знать что такое роутер)
Отвергаешь предлагай
собственно предложить нечего =(
1) разве что сделать какой то базовый компонент для всех компонентов-контейнеров в котором будет инжектиться роутер и стоять геттер на параметры роута (такой метод затрудняет понимание кода, и не стоит того)
Попробую ответить на все вопросы одним сообщением, а то у меня один ответ залезает на другой:
Мы недавно с коллегой делали сайд проект, который полностью построили на концепции частных провайдерах. Если здесь в примерах мы код просто выносили в соседний файл «для удобства», то там как раз мы специально заделали папочку «providers» в основе проекта и добавляли туда различные провайдеры, которые позже сплетались в цепочки друг из друга. Над проектом работаем по 10-20 часов в неделю в течение нескольких месяцев, пока полет отличный — все сущности ничего не знают о существовании друг друга, весь полет данных идет через различные преобразования в провайдерах.
По поводу типизации: да, ошибки теоретически могут быть и TS никак не спасет, так уж устроен DI
А не будет ли проблем с отловом ошибок если прописали не тот интерфейс в фабрике или месте инжектирования
Будет, и еще какая, но это скорее проблема @Inject()
, который не может проверить типы предоставленного значения и того что нужно.
const TT = new InjectionToken<number>("ddd");
@Component({
providers: [
{
provide: TT,
useValue: {}
}
]
})
export class AppComponent {
constructor(@Inject(TT) t: string) {
console.log(t);
}
}
Как видим должен быть number
, предоставили {}
, а сам компонент ждет string
— и ни одной ошибки. Поэтому @Inject()
скорее антипатерн и вместо него почти всегда лучше использовать абстракный класс.
1) инжектируя работу роут как параметр ...
Для случая описанного в статье так-то есть https://angular.io/api/router/Resolve, который отложит рендеринг компонента до момента резолва всех резолверов (= пользователь не будет смотреть в пустую страницу) тем самым позволит выбросить *ngIf
+ можно вывести ошибку и опционально перенаправить пользователя на другую страницу если резолвинг провалился (= сервер вернул 404 например)
Правда с типами тут тоже не все идеально ибо data: Observable<{[name: string]: any;}>
.
Используем DI в Angular по максимуму — концепция частных провайдеров