Не понял всей этой темы с отрицанием необходимости тестировать АПИ, кроме разумеется того как вы это делаете. Так делать, конечно, не стоит.
Если у вас есть юнит-тест, который учитывает схему данных полученных по сети, то вы тестируете всю цепочку от получения данных до отображения на экране. Меняется апи и вам нужно поменять схему зода, если надо модельки, но при этом в самом тесте меняется только мок и только то, что должно поменяться на экране.
Если у вас есть сторибук и не один юнит тест, то должна быть отдельная функциональность для генерации данных для моков. Если такая функциональность есть, то вносить изменения нужно в одно место, а не во все юнит тесты. И тогда ваши юнит тесты будут тестировать и схемы zod, и другие изменения данных, которые не влияют на предыдущую функциональность.
И, разумеется, если у вас настроен msw для тестов, то добавлять `axios.get.mockResolvedValue({ data: { name: 'John' } });` постоянно не обязательно.
Мне вот тоже более интересен практический момент. По опыту, использование enum всегда менее удобно. В основном из-за вот этой ситуации:
enum Actions {
teacher = 'teach',
doctor = 'treat'
}
interface MyInterface {
currentAction: AnotherActions
}
interface MyAnotherInterface {
currentAction: Actions
}
const actions = {
teacher: 'teach',
doctor: 'treat'
} as const
type Role = keyof typeof actions
type AnotherActions = (typeof actions)[Role]
const a = {
currentAction: 'teach'
} satisfies MyAnotherInterface // Type '"teach"' is not assignable to type 'Actions'.(2322)
const b = {
currentAction: 'teach'
} satisfies MyInterface // Ok
Так как обычно мы работаем со строками, то использование литерала удобнее.
При этом проблемы с перебором одинаковые
for (let [key, value] of Object.entries(ProfessionAction)) {
console.log(key, value)
ProfessionAction[key] // Element implicitly has an 'any' type because expression of type 'string' can't be used to index type
}
Кстати, вот любопытно, вы как то сразу перескочили на использование зоопарка из разных ответов, но по моему в воздухе повис вопрос: а когда предпочтительнее использовать такой вот зоопарк вместо исключений?
В ваших примерах проверки результатов выглядят вполне удобно. Но в тот же момент использование исключений тоже выглядит не плохо. Иными словами, если createUser всегда возвращает User, то нормальный поток выглядит максимально просто:
declare class UserError extends Error {
code: 'username_taken'
}
function handleApiCall() {
try {
const user = createUser({ username: 'hunter2' }).value;
return {
status: 200,
user,
}
} catch(e: unknown) {
if (e instanceof UserError) {
switch (e.code) {
case 'username_taken':
return {
status: 400,
message: e.message,
};
}
return {
status: 500,
};
}
}
Судя по неймингу тут у нас шаблон Container/Presentational. С другой стороны демонстрация SRP. Для данного шаблона это норма иметь компонент обертку, которая обращается к различным данным и расфасовывает их по презенташкам. А при предложенной в статье реализации у вас получается контейнер для разных презенташек никак не связанных друг с другом и которые можно переиспользовать с другими компонентами используется один компонент.
Поэтому вы такому собеседнику могли бы так и сказать, что у вас тут шаблон контейнер/презенташка и для каждой презенташки свой компонент в соответствии с темой статьи. И правильно это или нет - это уже не важно, главное наглядно демонстрирует тему статьи.
Мы обсуждаем логику обработки данных в зависимости от их типа (в нашем примере - логику вычисления площади фигуры). Мы не обсуждаем как внутри устроен visitor, также как не обсуждаем как внутри устроен switch case, также как не обсуждаем как внутри устроен полиморфизм.
Как это не обсуждаем как внутри устроен switch/case если без этого в принципе невозможно обсуждать вычисление площади:
function getArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
// Здесь TypeScript точно знает, что 'shape' является типом Circle,
// и автоматически предлагает доступ к свойству 'radius'.
return Math.PI * shape.radius ** 2;
case 'rectangle':
// Аналогично, здесь 'shape' сужается до типа Rectangle,
// предоставляя безопасный доступ к 'width' и 'height'.
return shape.width * shape.height;
}
}
Нам прямо нужно обсуждать как устроен switch/case что бы вычислять площади в зависимости от типа фигуры.
Логика обработки данных для visitor/switch case будет находится на вызывающей стороне.
Это вообще что значит? Вся логика по работе с Shape находится внутри switch/case.
Выше я уже, по сути, поставил и эти отличия нашел и о них рассказал.
И там и там у вас логика на вызывающей стороне, в отличии от полиморфизма, где логика на вызываемой стороне (спрятана за интерфейсом).
Не понял. Посетитель, очевидно, использует идеи полиморфизма, потому что за общим интерфейсом инкапсулированы разные реализации. При использовании Посетителя объекты знают только об абстрактном классе/интерфейсе Посетителя и никак не завязаны на конкретные классы посетителей.
Я выше уже по этому поводу высказался. Я бы так говорить не стал. Этак все что угодно можно обозвать усложненным switch/case. Как раз замена простыни из операторов на систему из взаимодействующих объектов и позволяет говорить о разной архитектуре. Потому что имеем в обоих случаях разные строительные блоки, разные отношения между этими строительными блоками, и соответственно разные подходы к масштабированию сложности.
Есть ровно один интерфейс/абстрактный класс, который описывает Посетителя. В случае с абстрактным классом вы можете задать дефолтное поведение для разных операций. Компилятор действительно подсказывает какие операции вам нужно реализовать. И компилятор не одинаково проверяет реализацию. В случае с абстрактными методами они все должны быть реализован, а в случае с if/else/switch вам самому нужно копипастить всю структуру операторов выбора и не забыть еще привести все к never в конце, что бы не пропустить какой-нибудь случай.
Поэтому в случае с шаблонами проектирования мы не имеем дело с копипастой, а работаем с определенными сущностями. Поэтому проект расширяется предсказуемо.
Так что воспринимайте использование discriminated union, как visitor на минималках.
Так не получится. Потому что шаблоны проектирования это один подход, а копипаста - это другой подход.
Я компонент и я не хочу знать о существовании многих классов, имплементирующих некоторый интерфейс. Хочу знать только об одном интерфейсе логгера {log(): void}, ну или в крайнем случае о базовом классе, поскольку типов в рантайме нет. Если один из родителей хочет установить для своих детей в качестве логгера некоторый логгер, наследующий базовый класс, но отличающийся от зарегистрированного, ему по всей видимости нужно создать свой контейнер зависимостей, зарегать класс, и обернуть детей провайдером.
Вы не поняли идею контейнера. Контейнер - это единственная точка входа для внедряемых зависимостей. Он по определению один на все приложение. Если у вас приложение использует в разных местах разные реализации логеров - это означает, что внедряемых логер является конфигурируемым. И тогда родитель устанавливает конфигурацию логера для своих потомков, но экземпляр логера они получают из DI, а вот конфигурацию из контекста. По сути, тоже самое выше ответили.
Если так присмотреться, до для лучше демонстрации UserProfileView всему этому там делать нечего:
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!user) return <div>No user found</div>;
Ни строки Loading... ни Error: {error.message} не являются чем-то специфическим и будут общими для любого компонента. А No user found похоже на какой-то информационный блок, который катомизируется сообщением
Вы так говорите, будто для каждой новой операции оно не приведёт к написанию реализации методов в каждом подклассе.
Не понял, честно говоря, причем тут реализация методов. В том же Посетителе для каждой новой операции нужно имплементировать/наследовать интерфейс посетителя. А если использовать if/else/switch, то для каждой новой операции нужно использовать копипасту if/else/switch. Т.е. в одном случае вы копируете реализацию выбора нужной операции, а в другом случае вы заполняете шаблон. В этом же суть.
Если бы сделали метод рисования фигур, то всё стало бы на свои места: фигуры не должны знать как их рисуют и на чём (canvas, HTML, PDF), а просто содержат данные о себе - вот тут switch и пригождается.
А разве есть разница? Фигура и про площади ничего может не знать. По идее, какая ей разница как используют ее данные? При использовании if/else/switch идет опрос объекта и после обращение к данным, а при использовании шаблонов создается иерархия классов в которых и определяются новые операции.
Думаю имелся в виду конкретный пример, когда имеет место быть иерархия объектов, которые семантически связаны друг с другом и нужно иметь возможность расширять их поведение с помощью семантически одинаковых операций. И отсюда вопрос, разве использование дискриминированных объединений не приведет к образованию копипасты с if/else/case для каждой новой операции?
В статье получилось показать несколько классов задач. Это семантически один объект, но с разными состояниями и система из объектов к которым применяются одни и те же операции.
Звучит так, что нужна фабрика для создания объектов в одном месте. А в случае с дискриминированными объединениями по всему коду будет тьма различных if/else/case. Т.е. вы правы, действительно есть такие особенности, но что бы не было зоопарка из if/else/case такой подход и использовали. Поэтому мне и любопытно, если конкретнее, было ли ощущение, что if/else/case расползается по всему проекту?
Кстати, если данные пришли из вне в сериализованном виде, то их все равно нужно привести к нормализованному виду.
По моему, если вдохновляться fsd, то какие вещи можно без изменения от туда брать. FSD он не только про размер строительных блоков, но и отношения между ними.
Вот вы пишете: Архитектура CLI. Дальшее идет перечисление ваших слоев и объяснением их семантики. И тут сразу несколько вопросов. Почему вы решили отказаться прям от всех слоев fsd? Каким образом выстроены отношения между слоями? Из представленного описания не очевидно направление зависимостей между слоями. Какие еще строительные блоки есть в вашей архитектуре?
Просто для примера рассмотрим, в fds есть слой app. У вас есть файл-простыня cli.ts. В нем куча захардкоженных сообщений, настроек и что-то мне подсказывает, что если вы заходите это расхардкодить то работать с этим сможет одновременно только один человек. Модульность как раз таких вещей позволяет избежать.
Мне вот любопытно, вам приходилось вместо дискриминированных объединений использовать по другому спроектированную систему. Например, использовать шаблоны проектирования и в частности шаблон "Посетитель", который как раз позволяет уйти от деревьев if/else/case?
Так я и имею в виду, что вместо базового refreshAccessToken
можно использовать свой, который либо использует базовый и сохраняет промис, либо возвращает сохраненный промис. В последнем случае у вас получится очередь из запросов которые будут ожидать этот промис.
Вернёмся к моменту, когда мы проверяем наличие токена:
Так, вроде, сценарий был правильный. Вот эта штука refreshAccessToken();
возвращает промис и этот промис должен быть одинаковый для всех запросов. И тогда получается очередь из запросов, которые ждут разрешения этого промиса.
Не понял всей этой темы с отрицанием необходимости тестировать АПИ, кроме разумеется того как вы это делаете. Так делать, конечно, не стоит.
Если у вас есть юнит-тест, который учитывает схему данных полученных по сети, то вы тестируете всю цепочку от получения данных до отображения на экране. Меняется апи и вам нужно поменять схему зода, если надо модельки, но при этом в самом тесте меняется только мок и только то, что должно поменяться на экране.
Почему не нужно делать вот так:
Если у вас есть сторибук и не один юнит тест, то должна быть отдельная функциональность для генерации данных для моков. Если такая функциональность есть, то вносить изменения нужно в одно место, а не во все юнит тесты. И тогда ваши юнит тесты будут тестировать и схемы zod, и другие изменения данных, которые не влияют на предыдущую функциональность.
И, разумеется, если у вас настроен msw для тестов, то добавлять `axios.get.mockResolvedValue({ data: { name: 'John' } });` постоянно не обязательно.
Мне вот тоже более интересен практический момент. По опыту, использование enum всегда менее удобно. В основном из-за вот этой ситуации:
Так как обычно мы работаем со строками, то использование литерала удобнее.
При этом проблемы с перебором одинаковые
Кстати, вот любопытно, вы как то сразу перескочили на использование зоопарка из разных ответов, но по моему в воздухе повис вопрос: а когда предпочтительнее использовать такой вот зоопарк вместо исключений?
В ваших примерах проверки результатов выглядят вполне удобно. Но в тот же момент использование исключений тоже выглядит не плохо. Иными словами, если
createUser
всегда возвращает User, то нормальный поток выглядит максимально просто:Судя по неймингу тут у нас шаблон Container/Presentational. С другой стороны демонстрация SRP. Для данного шаблона это норма иметь компонент обертку, которая обращается к различным данным и расфасовывает их по презенташкам. А при предложенной в статье реализации у вас получается контейнер для разных презенташек никак не связанных друг с другом и которые можно переиспользовать с другими компонентами используется один компонент.
Поэтому вы такому собеседнику могли бы так и сказать, что у вас тут шаблон контейнер/презенташка и для каждой презенташки свой компонент в соответствии с темой статьи. И правильно это или нет - это уже не важно, главное наглядно демонстрирует тему статьи.
Так мы же и обсуждаем реализацию getArea.
Если честно, я не понимаю, зачем вам пример Посетителя. Но если вам так удобнее
Соответственно, если для другого конкретного action нужен будет другой посетитель, то создается новый конкретный посетитель.
Как это не обсуждаем как внутри устроен switch/case если без этого в принципе невозможно обсуждать вычисление площади:
Нам прямо нужно обсуждать как устроен switch/case что бы вычислять площади в зависимости от типа фигуры.
Это вообще что значит? Вся логика по работе с Shape находится внутри switch/case.
Поэтому не удается понять, что вы имеете в виду.
Выше я уже, по сути, поставил и эти отличия нашел и о них рассказал.
Не понял. Посетитель, очевидно, использует идеи полиморфизма, потому что за общим интерфейсом инкапсулированы разные реализации. При использовании Посетителя объекты знают только об абстрактном классе/интерфейсе Посетителя и никак не завязаны на конкретные классы посетителей.
Я выше уже по этому поводу высказался. Я бы так говорить не стал. Этак все что угодно можно обозвать усложненным switch/case. Как раз замена простыни из операторов на систему из взаимодействующих объектов и позволяет говорить о разной архитектуре. Потому что имеем в обоих случаях разные строительные блоки, разные отношения между этими строительными блоками, и соответственно разные подходы к масштабированию сложности.
Есть ровно один интерфейс/абстрактный класс, который описывает Посетителя. В случае с абстрактным классом вы можете задать дефолтное поведение для разных операций. Компилятор действительно подсказывает какие операции вам нужно реализовать. И компилятор не одинаково проверяет реализацию. В случае с абстрактными методами они все должны быть реализован, а в случае с if/else/switch вам самому нужно копипастить всю структуру операторов выбора и не забыть еще привести все к never в конце, что бы не пропустить какой-нибудь случай.
Поэтому в случае с шаблонами проектирования мы не имеем дело с копипастой, а работаем с определенными сущностями. Поэтому проект расширяется предсказуемо.
Так не получится. Потому что шаблоны проектирования это один подход, а копипаста - это другой подход.
Вы не поняли идею контейнера. Контейнер - это единственная точка входа для внедряемых зависимостей. Он по определению один на все приложение. Если у вас приложение использует в разных местах разные реализации логеров - это означает, что внедряемых логер является конфигурируемым. И тогда родитель устанавливает конфигурацию логера для своих потомков, но экземпляр логера они получают из DI, а вот конфигурацию из контекста.
По сути, тоже самое выше ответили.
Если так присмотреться, до для лучше демонстрации UserProfileView всему этому там делать нечего:
Ни строки Loading... ни Error: {error.message} не являются чем-то специфическим и будут общими для любого компонента. А No user found похоже на какой-то информационный блок, который катомизируется сообщением
XD
=P
Не понял, честно говоря, причем тут реализация методов. В том же Посетителе для каждой новой операции нужно имплементировать/наследовать интерфейс посетителя. А если использовать if/else/switch, то для каждой новой операции нужно использовать копипасту if/else/switch. Т.е. в одном случае вы копируете реализацию выбора нужной операции, а в другом случае вы заполняете шаблон. В этом же суть.
А разве есть разница? Фигура и про площади ничего может не знать. По идее, какая ей разница как используют ее данные? При использовании if/else/switch идет опрос объекта и после обращение к данным, а при использовании шаблонов создается иерархия классов в которых и определяются новые операции.
Думаю имелся в виду конкретный пример, когда имеет место быть иерархия объектов, которые семантически связаны друг с другом и нужно иметь возможность расширять их поведение с помощью семантически одинаковых операций. И отсюда вопрос, разве использование дискриминированных объединений не приведет к образованию копипасты с if/else/case для каждой новой операции?
В статье получилось показать несколько классов задач. Это семантически один объект, но с разными состояниями и система из объектов к которым применяются одни и те же операции.
Звучит так, что нужна фабрика для создания объектов в одном месте. А в случае с дискриминированными объединениями по всему коду будет тьма различных if/else/case. Т.е. вы правы, действительно есть такие особенности, но что бы не было зоопарка из if/else/case такой подход и использовали. Поэтому мне и любопытно, если конкретнее, было ли ощущение, что if/else/case расползается по всему проекту?
Кстати, если данные пришли из вне в сериализованном виде, то их все равно нужно привести к нормализованному виду.
По моему, если вдохновляться fsd, то какие вещи можно без изменения от туда брать. FSD он не только про размер строительных блоков, но и отношения между ними.
Вот вы пишете: Архитектура CLI. Дальшее идет перечисление ваших слоев и объяснением их семантики. И тут сразу несколько вопросов. Почему вы решили отказаться прям от всех слоев fsd? Каким образом выстроены отношения между слоями? Из представленного описания не очевидно направление зависимостей между слоями. Какие еще строительные блоки есть в вашей архитектуре?
Просто для примера рассмотрим, в fds есть слой app. У вас есть файл-простыня cli.ts. В нем куча захардкоженных сообщений, настроек и что-то мне подсказывает, что если вы заходите это расхардкодить то работать с этим сможет одновременно только один человек. Модульность как раз таких вещей позволяет избежать.
Мне вот любопытно, вам приходилось вместо дискриминированных объединений использовать по другому спроектированную систему. Например, использовать шаблоны проектирования и в частности шаблон "Посетитель", который как раз позволяет уйти от деревьев if/else/case?
Так я и имею в виду, что вместо базового
refreshAccessToken
можно использовать свой, который либо использует базовый и сохраняет промис, либо возвращает сохраненный промис. В последнем случае у вас получится очередь из запросов которые будут ожидать этот промис.
Так, вроде, сценарий был правильный. Вот эта штука
refreshAccessToken();
возвращает промис и этот промис должен быть одинаковый для всех запросов. И тогда получается очередь из запросов, которые ждут разрешения этого промиса.
Типа до сих пор этого не произошло?
Так вы же его буквально задаете - поэтому это литерал.
А в каких случаях это `[block1Ref.current, block2Ref.current] ` имеет смысл?