Что я понял за полтора года преподавания

Полтора года назад я провёл своё первое занятие в Нетологии. С тех пор я читал лекции, проверял домашние задания, менторил дипломные проекты и даже сам был студентом на одном из курсов. За это время я смог осознать для себя некоторые штуки о процессе обучения, которыми решил поделиться.

Будьте аккуратнее с новыми терминами

Если вы объясняете новичкам незнакомые термины, то это стоит делать понятиями, которые им уже известны. Следовать этому правилу труднее, чем кажется.

При переходе из разработки в преподавание, многое кажется очевидным. Маржины схлопываются, флоты надо сбрасывать, у боди есть отступы по умолчанию. Разработчики к этому настолько привыкают, что перестают замечать.

Но как только вы начнёте объяснять это другим людям, у них вполне оправданно появится куча вопросов. Что за маржины, что значит «схлопываются», как сбрасывать флоты, зачем они нужны, откуда отступы, зачем они там?

Чтобы объяснить эти тонкости, надо использовать уже знакомые понятия. Потому что иначе вы не только не объясните, что хотели, но и отобьёте интерес к теме. Никому не хочется разбираться в непонятной фигне, по крайней мере — в чересчур непонятной.

Поэтому объяснение нового термина должно:

  • состоять из уже известных понятий;
  • быть последовательным, строиться от целого к частному либо наоборот.

Избегайте путаницы

Если у термина есть синонимы (флот ⇔ плавающий элемент), надо это пояснить на старте.

Можно также объяснить, почему эти термины взаимозаменяемы. (Флот переводится, как плавающий, а используется такой элемент, чтобы его «обтекал» текст. Поэтому он как бы плавающий, это такой грубый перевод.)

Тем не менее, я стараюсь использовать один термин по ходу объяснения, даже если проговариваю синонимы в начале. Просто чтобы ученику не приходилось лишний раз соотносить новое понятие с ещё одним словом.

Автоматизируйте всё, что можно

Большую часть времени я тратил на ревью решений. В них важно не только рассказать, что неправильно и почему другое решение было бы лучше, но и понять, почему ученик сделал именно такую ошибку.

Часть ошибок — заблуждения, которые возникают при погружении в тему. На такие ошибки стоит заранее приготовить развёрнутое объяснение, почему заблуждение — это именно заблуждение, и как решить задачу было бы правильнее.

Нетипичные ошибки более редкие. Они особенно важны, потому что как бы ставят под вопрос рациональность технологии. На них отвечать на них надо ещё более полно.

Грамотно объяснить сложные вещи, будучи уставшим, трудно. Именно чтобы не уставать от ответов на типичные ошибки, лучше на них заранее готовить развёрнутый ответ.

Объясняйте на примерах

Объяснение с примерами понятнее. Я пробовал объяснять и абстрактно, и на примерах — на примерах лучше.

Абстракция — ресурсозатратная штука. Даже чтобы представить что‑то уже знакомое, требуется внимание и сосредоточенность. Чтобы представить какое‑то новое понятие, ресурсов потребуется больше.

Примеры будут экономить ученикам внимание и силы. Кроме этого они сильнее связывают новое понятие с каким‑то процессом или образом. И, возможно, как мне кажется, я не уверен, но так ученики лучше запомнят то, о чём вы говорили.

Делайте перерывы во время занятия

Во время объяснений стоит делать небольшие перерывы.

У меня не всегда получается вовремя вспомнить о них, потому что есть ограничение по времени на лекцию. Иногда я просто забываю о них. Но в последнее время стараюсь делать перерывы чаще и спрашиваю, какие есть вопросы.

Ученикам перерыв даёт время передохнуть и восстановить внимание. Вам — перевести дыхание.

Стимулируйте обратную связь

Задавать вопросы страшно и неловко. Но вопросы надо поощрять и стимулировать. Часто перед ответом на вопрос я говорю, что вопрос хороший, логичный или правильный. Не знаю, насколько это снижает страх задавать вопросы, надеюсь, помогает.

Если вопрос задан расплывчато, и вам непонятно, как ответить, просто уточните, что имелось в виду. И, если в чём‑то сомневаетесь, не стесняйтесь гуглить прямо во время занятия. Я так и говорю: «не уверен, давайте прямо сейчас и загуглим».

Пейте воду во время занятия

Мне трудно долго говорить, особенно если приходится следить за таймингом и проговаривать слова быстро и громко. Поэтому я во время лекции иногда отвлекаюсь на глоток воды. Голос не садится к концу, а ещё это помогает сделать вынужденный перерыв.

Пить воду ещё можно, если чувствуете, что плывёте или сильно волнуетесь. Это работает как микротаймаут: пока пьёте воду, можно перевести дух, сформулировать мысль и продолжить.

Дозируйте время на ревью

Любую работу стоит дозировать и разбивать на кусочки. Если два часа подряд ревьюить домашки, то к концу вы будете чувствовать большую усталость, чем если бы чередовали ревью с задачами по другим проектам.

Дело в монотонности, от неё надо избавляться. Чередовать темы, тасовать задачи, менять вид деятельности. Короче, делать всё, чтобы ревью не приедалось. Иначе отвечать свежо не получится и просто пропустить вопрос, на который стоит дать более развёрнутый ответ, или ошибку в решении.

Получайте пользу

Преподавание учит точнее выражать мысли и задумываться об аспектах разработки, о которых раньше вы бы не стали задумываться. Это почти как заново научиться верстать.

Если есть возможность заниматься менторством, то пробуйте. Вы глубже поймёте то, что знаете, научитесь объяснять сложные понятия и сможете помочь кому‑то разобраться с новыми для них вещами.

Документация

Польза документации по проекту не в только том, чтобы она описывала «что это за хрень», а ещё и «почему эта хрень сделана вот так».

Описывайте причины, по которым приложение работает именно так, как работает. Рассказывайте о проблемах, с которыми столкнулись при разработке, и ходе решения этих проблем.

В идеале новичок, прочитав доки, должен узнать обо всех компромиссах и спорных решениях в проекте. Потому что без них теряется огромная часть контекста, и, скорее всего, новые разработчики будут повторять старые ошибки.

Обработка ошибок в асинхронных функциях

В ES2017 появились асинхронные функции, которые позволяют сделать асинхронные вызовы более плоскими и похожими на синхронные.

Асинхронные функции работают на промисах. Очень грубо говоря await внутри асинхронной функции как бы приостанавливает её выполнение, ждёт, когда зарезолвится промис, указанный после него, и возвращает результат.

И на первый взгляд в асинхронных функциях есть проблемы с обработкой ошибок.

Промисы

Допустим, у нас есть функция loadPost, которая получает с сервера статью и работает на промисах. Метод fetch отправляет запрос на указанный адрес, и возвращает промис, который мы в дальнейшем можем обработать.

Если всё хорошо, получаем данные с помощью .json, который тоже возвращает промис. Если что‑то пошло не так, отловим ошибку в .catch.

const loadPost = (postId) =>
  fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`)
    .then(response => response.json())
    .then(data => console.log(data.title))
    .catch(e => console.log(`Error! ${e}`))

loadPost(1)

Асинхронные функции

Попробуем переписать эту функцию, используя async/await. Делаем её асинхронной с помощью ключевого слова async, без этого бы не сможем использовать await внутри.

Вторая строка совершает запрос, await «разворачивает» промис и возвращает значение, которое записывается в переменную response. Третья строка получает джейсон и записывает значение в переменную data.

const loadPost = async (postId) => {
  const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`)
  const data = await response.json()
  console.log(data.title)
}

loadPost(1)

Пока запрос проходит без ошибок у нас всё хорошо. Но если что‑то пойдёт не так, выскочит исключение:

Uncaught (in promise) TypeError: Failed to fetch

Обработка ошибок

Окей, используем try‑catch, чтобы отловить ошибку:

const loadPost = async (postId) => {
  try {
    const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`)
    const data = await response.json()
    console.log(data.title)
  }
  catch(e) {
    console.log(`Error! ${e}`)
  }
}

loadPost(1)

Вроде нормально, но функция стала больше, да и запросы могут быть разными, а каждый раз писать try‑catch запарно.

Вспоминаем, что асинхронная функция возвращает промис, поэтому можно использовать .catch, чтобы отловить ошибку:

const loadPost = async (postId) => {
  const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`)
  const data = await response.json()
  console.log(data.title)
}

loadPost(1)
  .catch(e => console.log(`Error! ${e}`))

Это решает проблему с повторами try‑catch, но не решает проблему с дублированием кода. Здесь может помочь функция высшего порядка.

const loadPost = async (postId) => {
  const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`)
  const data = await response.json()
  console.log(data.title)
}

// функция высшего порядка по очереди «запоминает»
// обработчик ошибок, сам запрос, аргументы для запроса
const tryCatchWrapper = (handleError) => (reqFn) => (...args) => 
  reqFn(...args).catch(handleError)

// обработчик ошибок
const handleError = e => 
  console.log(`Error! ${e}`)

// запомнили функцию для обработки ошибок
const errorHandlerWrapper = tryCatchWrapper(handleError)

// запомнили, какой запрос хотим выполнить
const safelyLoadPost = errorHandlerWrapper(loadPost)

// выполняем этот запрос
safelyLoadPost(1)

Запрос остался без изменений, но добавилась функция tryCatchWrapper. Она принимает как аргумент функцию handleError, которая будет обрабатывать исключения, и возвращает новую функцию.

Эта новая функция принимает функцию запроса, который мы собираемся отправлять, и возвращает ещё одну функцию. Эта конечная функция принимает параметры, которые будет передавать в функцию запроса, когда мы вызовем её.

По‑умному это ещё называют каррированием: когда мы из одной функции с несколькими аргументами делаем несколько функций, которые принимают по одному аргументу. Так мы можем «запоминать» аргументы, не вызывая функцию сразу, а вызывать её потом.

Всё это вместе позволяет написать несколько обработчиков ошибок, которые будут применять разные функции в зависимости от наших целей. Например, если мы захотим использовать другой обработчик для какого‑то запроса, мы можем передать как аргумент другую функцию:

// первый обработчик
const handleError = e =>  console.log(`Error! ${e}`)
const errorHandlerWrapper = tryCatchWrapper(handleError)

// другой
const handleErrorDifferently = e => console.log(`Wow! It is all different now`)
const otherErrorHandlerWrapper = tryCatchWrapper(handleErrorDifferently)

И при этом не будет дублирования кода, потому что вся обработка находится внутри tryCatchWrapper.

Ссылки по теме

Как мы делали «Фронтенд — это не больно!»

«Фронтенд — это не больно!» — длинная статья, цель которой помочь разработчикам научиться справляться с рутиной и получать удовольствие от работы. В этой заметке я расскажу, как появилась идея и почему этот проект для меня был важен.

Предпосылки

Когда‑то описанные в нём проблемы были у меня. Мне тоже казалось, что найти с дизайнерами общий язык очень трудно, объяснять что‑то менеджерам бесполезно, и проще переписать фронтенд, чем говорить с бекендом. Но со временем я стал умнее и понял, что дело не в команде, а в моём отношении к ситуации и проблемам.

Изменил отношение — нашлись решения проблем. Перестал воспринимать критику на личный счёт — увидел полезные замечания. Начал прокачивать переговоры — понял, как обсуждать проблемы фронтенда на языке, понятном другим людям. Ну и всё такое.

Этого казалось мало, потому что теперь я очутился на другой стороне: видел других фронтендеров с теми же проблемами. У меня был опыт того, как начать работать без напряга. Я хотел им поделиться, но не знал как.

Идея

В феврале 2017 мне написал Вадим с идеей сделать доклад или лекцию для разработчиков о понимании дизайнеров. Мы хотели показать, что разработчики часто упарываются по технологиям, не замечают важного и злятся на дизайнеров. А было бы круто, если все работали сообща и понимали друг друга.

Вначале мы думали в сторону лекции или доклада, чтобы нам можно быть задать вопросы напрямую. Мы начали работать над этим, я набросал первые варианты текста и слайды, но что‑то было не то.

Казалось, что материала слишком много, было трудно сформулировать идеи. Непонятно было даже, с чего начинать доклад. Решили сделать вместо него почтовую рассылку. Её можно править на ходу по отзывам, и прямо в письмах спрашивать, о чём рассказывать дальше. Я начал работать над рассылкой, сделал первую половину первого письма.

После этого решил ещё раз подумать, в чём цель идеи — достучаться до большего количества разработчиков. Рассылки, лекции, доклады — это слишком сложно. На рассылку надо подписываться, на лекцию или доклад идти. А вот у статей охват потенциально больше, потому что их проще читать и распространять. А чем больше охват, тем большему количеству людей они могут помочь.

Тогда я написал Андрею и предложил вместе сделать серию статей. Андрей же предложил сделать нечто вроде пособия. Получилось — вот это.

Запуск, результаты и планы

Статья разлетелась по соцсетям, о ней написали Форвеб и Вебстандарты, обсудили в паре подскастов. Судя по всему, разработчикам она тоже понравилась. Мы даже получили несколько писем с благодарностями, надеемся кому‑то действительно помогло.

Сейчас у нас в планах сделать более весёлое дополнение к проекту. Если вам интересно поучаствовать, то заходите в чат в Телеграме, там уже есть наработки :–)

Лекарство от сломанной обратной совместимости

Фронтендеры любят жаловаться, что бекендеры часто ломают обратную совместимость. Вот, мол, описывают структуры данных, АПИ, мы по нему привязываем интерфейс, а через месяц структура меняется, и ничего не работает. Сегодня расскажу, как немного облегчить себе жизнь в таких ситуациях.

Шаблоны спешат на помощь

Есть такой шаблон проектирования — адаптер. Он помогает подружить сущности, которые должны взаимодействовать друг с другом, но не могут напрямую этого делать. Как переходник для английской вилки на европейскую розетку.

Чаще всего такие сущности — это классы или объекты. Но никто не мешает использовать ту же логику для работы со структурами данных или сетевыми запросами. Смысл в том, чтобы вынести работу со структурой подальше и от запроса, и от интерфейса. Так вносить изменения потом будет проще и быстрее.

Пример

Возьмём абстрактное приложение. У него есть состояние state, где в поле user хранится информация о пользователе.

class State {
  constructor(initialState) {
    this.state = {...initialState}
  }

  update(key, value) {
    this.state = {
      ...this.state,
      [key]: value
    }
  }

  get(key) {
    return this.state[key]
  }
}

const state = new State({user: {}})

Чтобы получить свежую информацию и показать её, клиент обращается к серверу за данными. Если получает, то обновляет состояние:

fetch('/fetch/user.json')
  .then((response) =>
    response.json())
  .then((user) =>
    state.update('user', user))
  .catch(handleError)

Допустим пользователь в приложении описывается таким объектом:

{
  name: 'John',
  lastName: 'Doe',
  birthYear: 1981,
  city: 'Berlin'
}

Пока с сервера приходит подобная структура, наше приложение работает без сбоев. Но вдруг сервер начинает отдавать данные с такой структурой:

{
  fullName: {
    name: 'John',
    lastName: 'Doe'
  },
  birthDate: {
    year: 1981
  },
  address: {
    city: 'Berlin',
    street: '1 Hasselhoff Lane'
  }
}

Если приложение живёт давно, то к полям user.name, user.birthYear, user.city уже привязаны какие‑то его части. Их может быть несколько, и править каждое — не вариант.

Как‑то подгонять структуру ответа в обработчике запроса тоже плохо. Информация о пользователе может запрашиваться не только здесь. Да и код обработчика запроса распухнет.

Поэтому лучше работать со структурой ответа где‑то в другом месте. Напишем адаптер:

class UserToStateAdapter {
  constructor(state) {
    this.state = state
  }

  update(serviceUser) {
    const {fullName, birthDate, address} = serviceUser
    const {name, lastName} = fullName
    const {year} = birthDate
    const {city} = address

    const clientUser = {
      name, 
      lastName, 
      birthYear:year, 
      city,
      address,
    } 

    this.state.update('user', clientUser)
  }
}

const userToStateAdapter = new UserToStateAdapter(state)

Используем:

fetch('/fetch/user.json')
  .then((response) =>
    response.json())
  .then((user) =>
    userToStateAdapter.update(user)
  .catch(handleError)

Теперь всю логику обработки структуры мы вынесли в отдельную часть приложения. Если структура поменяется ещё раз, то нам нужно будет поправить только адаптер, чтобы всё работало.

В жизни

Адаптер может пригодиться не только, когда поломали обратную совместимость, но и когда из плоской структуры надо сделать сложную.

На одном из проектов нам нужно было из двух списков построить два разных дерева. В одном списке — отправители, в другом — получатели. Нужно было сгруппировать данные по отправителям или получателям в зависимости от выбора пользователя:

Начальная структура данных Первый вариант дерева Второй вариант дерева

После того, как мы вынесли всю обработку структур в отдельное место, код стал чище. Кроме этого, мы не гоняем по сети лишние биты, так как не дублируем информацию и запросы.

Но есть пара минусов

Например:

  • добавляется ещё одна абстракция в копилку проекта, из‑за этого увеличивается сложность;
  • при создании нового адаптера нужно найти все места, где требуется его вызвать.

Второй пункт проблематичен, если приложение большое. Хотя такие моменты лучше продумывать ещё на этапе проектирования архитектуры.

Чем хорош и когда использовать

Адаптер помогает избежать дублирования кода и множественных исправлений. Позволяет не переписывать существующую логику на клиенте, если на сервере вдруг что‑то поменялось (по крайней мере не сразу). Даёт возможность чаще использовать плоские структуры данных, а сложные создавать только по мере необходимости.

Да, если новая структура в корне отличается от старой, то одним адаптером, скорее всего, уже не обойтись. Но для задач типа «опять они все поля переназвали» — самое то.

Почитать на тему:

Раньше ↓