Программирование — это дисциплина, которую вы можете освоить со временем.

Общее правило заключается в том, что чем больше времени вы мысленно проводите со своим кодом (не обязательно перед монитором), тем лучшим программистом вы становитесь.

Вот почему мне лично не нравятся такие ярлыки, как исключительный и обычный.

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

Что отличает выдающегося программиста от среднего, так это набор идей, демонстрируемых в процессе собеседования/взаимодействия. Эти идеи подробно описаны в величайших книгах по программированию.

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

Здесь мы стремимся пройти через сущность исключительности. Давай начнем.

# 1: Уточнение масштаба:

Один мудрый человек сказал однажды: «Правильные вопросы — это 50% решение».

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

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

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

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

Они неявно показывают, что кодирование — единственная (чаще последняя) часть решения.

Совершенствуя масштаб, выдающийся программист подталкивает интервьюеров к намеченному решению.

Когда интервьюер спрашивает: «Как рассчитать средний возраст избирателей, учитывая массив жителей города?», хороший программист быстро набросает следующее решение:

function calculateAge(votersArray: [Citizen]) {
   if (votersArray.count < 1) { 
      return 0
   }
   sum = 0
   for person in votersArray {
      sum =+ voter.age
   }
   average = sum / votersArray.count
   return average
}

С другой стороны, исключительный программист задаст следующие вопросы:

  • Каков минимальный возраст для голосования? (помните, что это массив citizen, а не готовый список избирателей!)
  • Есть ли элемент данных, указывающий на то, что избиратель зарегистрирован избирательным органом?

На данный момент большинство интервьюеров не дают конкретных ответов. Они скорее предоставляют кандидату свободу реализовать его наилучшим образом. (Хотя за вопросы мысленно присваивают одни баллы, а за правильные - больше баллов)

Исключительный программист затем закодирует свое решение следующим образом (решение не привязано ни к какому языку программирования):

class Citizen {
    var age: Int
    var isRegistered: Bool
}
function calculateAge(citizensArray: [Citizen], filterFunction: (Voter) -> Bool) {
   if (citizensArray.count < 1) {
     logMessage("Citizen list is empty!")    
     return 0
   }
var votersCount = 0 //tracks only the eligible voters
for citizen in citizensArray {
      if (citizen.age >= MINIMUM_VOTER_AGE && citizen.isRegistered == true)
        sum =+ sum + citizen.age 
        votersCount += 1
      }   
   }
   average = votersCount > 0 ? (sum / votersCount) : 0
   return average
}
// Usage:
citizen1 = Citizen(age: 38, isRegistered: true)
citizen2 = Citizen(age: 30, isRegistered: true)
citizen3 = Citizen(age: 28, isRegistered: false)
citizens = [citizen1, citizen2, citizen3]
//finds average age of voters - returns 34
let average = calculateAge(citizensArray: citizens)

Заметили разницу, которую дает уточнение масштаба? Делая это, выдающийся программист-

  • Демонстрирует свои навыки проектирования объектов, где проверяются абстракция и связность данных (возраст гражданина, регистрация и т. д.).
  • Не только заботится о граничных условиях, но и способствует их определению (15 000 зарегистрированных избирателей против 89 000 граждан). Уточняя масштаб, он/она помогает (и эффективно подталкивает) интервьюеров к намеченному им/ей решению.
  • Сообщает об ошибках. Здесь пустой список граждан сообщается через ведение журнала, но исключения также могут быть другим способом сообщить о них.
  • Демонстрирует преимущества своего решения, включая примеры клиентских вызовов в разделе использования.

Теперь, если вы посмотрите на решение хорошего программиста задним числом, вы заметите, что это правильное решение проблемы, описанной интервьюерами.

Но, похоже, он уходит корнями в среду онлайн-тестирования кодирования, где проверка граничных условий считается конечной целью идеального программирования. Это приводит к ограничению области проблемы.

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

# 2: Чистый код:

Уточнение масштаба приводит к решению 50% проблемы. Чистый код выполняет все остальное.

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

Чистый код — это не черта, а состояние, которое достигается часами рефакторинга.

Если вы из тех, кто копирует решения из StackOverflow и выпускает их без как минимум 3-х итераций, вам, вероятно, предстоит много работы в этой области. Я знаю, потому что я был одним из них и много платил за это на протяжении всей своей карьеры старшего разработчика.

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

Обычно это делается путем преобразования данных API (api/restaurants) в объекты домена (массив Restaurant) и написания кода пользовательского интерфейса, который зависит от этого. объекты домена.

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

var restaurantArray = Array<Restaurant>()
if JSONDeSerializer().decode(httpResponse) != Error {
   restaurantArray = JSONDeSerializer().decode(httpResponse)
} else if XMLDeSerializer().decode(httpResponse) != Error {
   restaurantArray = XMLDeSerializer().decode(httpResponse)
}

Проблема? Мало того, что этот код дважды декодирует ответ (сначала при проверке, а затем при фактическом преобразовании), он также может вскоре превратиться в спагетти. Что, если кроме XML и JSON существует больше форматов десериализации?

Если-иначе — полиморфизм бедняков.

Повторюсь, проблема не в самой конверсии, а в паттерне. Если у вас есть бизнес-логика, которая сегодня занимается проверкой 4 полей, какова вероятность того, что она вместит еще 10, если вы закодируете ее с помощью подхода if-else, показанного выше?

Кто-то справедливо назвал if-else полиморфизмом бедняков.

Исключительный программист уже понимает это и разрабатывает свое решение примерно так:

class APIDownloader {
   APIDownloader(deserializer: Deserializer) {
      this.deserializer = deserializer
   }
   function downloadData() {
      //downloading logic
      deserializer.decode(httpResponse)
   }
}

Что такое Десериализатор? Это интерфейс/протокол, который определяет функцию decode(HttpResponse). Он должен быть реализован/соответствовать классам JSONDeSerializer и XMLDeSerializer, которые предоставляют конкретные реализации функции decode() (в основном, с открытым исходным кодом).

Откуда взялся десериализатор? Извне APIDownloader. APIDownloader не обязан решать, с каким форматом HTTP-ответа он имеет дело. Какой-то другой класс создает объект типа Deserializer (который может быть экземпляром класса JSONDeSerializer или XMLDeSerializer) и передает его APIDownloader. .

Стратегия состоит в том, чтобы вытолкнуть if/else за пределы того самого класса, который вы кодируете, в конечном итоге заменив его на switch/case

Этим каким-то другим классом может быть класс APIClient, который еще до выполнения HTTP-запроса знает, в каком формате будет получен HTTP-ответ. Или это может быть модульный тест для APIDownloader, который четко определяет задачу для APIDownloader: просто загрузите данные и верните мне объекты домена XML/JSON.

Если подумать еще раз, все это упражнение направлено на то, чтобы вытолкнуть if-else за рамки того самого класса, который вы кодируете. APIDownloader делегирует if-else в APIClient/unit test и т. д.. Когда становится абсолютно необходимо его использовать, его можно заменить на переключатель регистра, что довольно упрощает работу компилятора (конечно, в зависимости от языка программирования)

На языке программирования этот метод называется внедрением зависимостей и является одним из краеугольных камней чистого кода.

Это так, потому что он автоматически обеспечивает соблюдение основных принципов чистого кода:

  • Дизайн классов и разделение задач (также известные как S в SOLID)
  • Один уровень абстракции (APIDownloader занимается только загрузкой, а не преобразованием)
  • Полиморфизм (десериализатор абстрактный интерфейс)
  • Разработка через тестирование

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

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

#3: Создавайте возможности, а не функции:

Если вы вернетесь к примеру, который мы видели в № 1, вы обнаружите один вопиющий недостаток исключительного подхода программиста. Демонстрируя возможности уточнения области действия, он добавляет еще два поля (isRegistered и age) в класс Citizen.

Хотя это и важные детали, они нагружают решение дополнительным набором условий if (citizen.age ›= MINIMUM_VOTER_AGE && Citizen.isRegistered == true).

Список атрибутов, которые можно добавить к классу Citizen, может быть бесконечным. Но за время, необходимое для кодирования/представления решения, продемонстрирует ли оно больше возможностей кандидата? Нисколько.

Интервьюеры тоже обязательно заметят это, хотя, когда они отклоняют такого кандидата, все, что они говорят, это: «Кандидат в конечном итоге сделал что-то, выходящее за рамки».».

О чем они могли не сообщить (хотя это был факт), так это о следующем утверждении:

Без демонстрации других возможностей.

Хорошие программисты могут реализовать функции без ошибок, точно так, как указано в требованиях. Когда они делают все возможное, их график сложность/результат становится линейным. Достигая большего, они в конечном итоге усложняют код.

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

График сложность-результат для исключительных программистов был бы лучше, чем линейный — вероятно, экспоненциальный. Другими словами, они достигают гораздо лучших результатов с меньшей дополнительной сложностью.

Во время уточнения области действия исключительный программист может дополнительно спросить:

Интересует ли нас только общий средний возраст или также средний возраст определенной группы избирателей (например, правых избирателей, избирателей-женщин, образованных избирателей и т. д.)

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

Часть it, выделенная полужирным шрифтом и курсивом, в приведенном выше предложении обозначает возможности проверки/оценки любого количества дополнительных атрибутов гражданина без раздувания кода ненужными условиями if/else, придерживаясь к пункту №2 выше.

type CitizenEvaluator = (Citizen) -> Bool
function calculateAge(citizensArray: [Citizen], filterFunction: CitizenEvaluator) {
   if (citizensArray.count < 1) {
     logMessage("Citizen list is empty!")    
     return 0
   }
   var eligibleVotersCount = 0 //tracks only the eligible voters
   for citizen in citizensArray {
      if (fliterFunction(citizen) == true)
        sum =+ sum + citizen.age
        eligibleVotersCount += 1
     }   
   }
   average = eligibleVotersCount > 0 ? (sum / eligibleVotersCount) : 0
   return average
}
// Usage:
ageEligibleFilterFunction = function(citizen) {
   return citizen.age >= MINIMUM_VOTER_AGE
}
registrationFilterFunction = function(citizen) { 
   return citizen.isRegistered == true
}
function maleVoterFilterFunction = function(citizen) { 
    return ageEligibleFilterFunction(citizen) && registrationFilterFunction(citizen) && citizen.gender == MALE
}
femaleVoterGraduateFilterFunction = function(citizen) {
    return ageEligibleFilterFunction(citizen) && registrationFilterFunction(citizen) && (citizen.gender == FEMALE && citizen.education > GRADUATE)
}
citizen1 = Citizen(age: 34, isRegistered: true, gender: MALE, education: GRADUATE)
citizen2 = Citizen(age: 23, isRegistered: true, gender: FEMALE, education: POSTGRADUATE)
citizen3 = Citizen(age: 9, isRegistered: false, gender: FEMALE, education: STUDY)
citizen4 = Citizen(age: 28, isRegistered: false, gender: FEMALE, education: GRADUATE)
citizens = [citizen1, citizen2, citizen3, citizen4]
//finds the average age of male voters
let maleAverage = calculateAge(citizensArray: citizens, filterFunction: maleFilterFunction)
//finds the average age of female, post-graduate voters
let femalePGAverage = calculateAge(citizensArray: citizens, filterFunction: femaleGraduateFilterFunction)

Обратите внимание на тип CitizenEvaluator, который представляет собой псевдоним функции, принимающей аргумент Citizen и возвращающей логическое значение на основе различных атрибутов объекта Citizen. Он добавляется в качестве аргумента в функцию calculateAge(), тем самым вынуждая всех своих клиентов самостоятельно указывать подходящие условия if, тем самым избавляя calculateAge() от любого из if-else безумие.

И ageEligibleFilterFunction, и RegistrationFilterFunction относятся к типуCitizenEvaluator. Функции maleVoterFilterFunction и femaleVoterFilterFunction также имеют тип CitizenEvaluator, но они дополнительно полагаются на ageEligibleFilterFunction и RegistrationFilterFunction — мало чем отличается от шаблона проектирования композиции объектов, где объект может содержать другие вложенные объекты.

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

Используя этот подход, выдающийся разработчик может продемонстрировать несколько качеств:

  • Абстракция в общении. Задавая вопросы, выдающийся программист не обсуждает технические аспекты предполагаемого решения (например, такие вещи, как должен ли я использовать функциональное программирование?). Он/она просто спрашивает, используя термины, связанные с функциональностью. Разговор на уровне кода будет необходим, только если он/она будет обсуждать проблемы/качество кода. Эта способность известна как работа на правильном уровне абстракций, и она крайне необходима в реальной жизни программистов.
  • Понимание функциональности. Решение не только показывает, чего можно добиться с помощью минимальных линий, но и сколько деталей может раскрыть простое среднее статистическое значение набора данных (образование избирателей, пол избирателя и т. д.).
  • Простота. Средние программисты могут линейно добавлять детали, но они не усложняют функцию, разработанную выдающимся программистом. В некотором смысле исключительный программист позволяет добавлять в продукт бесконечное количество функций, и их не нужно добавлять самому. Даже средние программисты могут использовать этот код для создания продукта с дополнительным набором функций.
  • Краткость. Список атрибутов гражданина может быть бесконечным, но двух элементов (пол и образование) достаточно, чтобы продемонстрировать, что он может поставить проблему в правильном контексте, даже не пытаясь ее решить.

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

Заключение:

Достижение функциональности — требование №1 на собеседованиях. Этот вывод во многом основан на школах кодирования интервью, которые обучают кандидатов улучшать свои презентации, разделяя и сортируя вещи, которые им следует решать, чтобы успокоить интервьюеров.

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

Этот кто-то может быть живым памятником вечной цитате Леонардо да Винчи, которая гласит:

Простота есть основа утонченности.

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

Pen Magnet — автор популярной электронной книги с интервью для старших разработчиков, в которой рассматриваются вопросы формата интервью Amazon STAR:

Комплексный подход к интервью со старшими разработчиками (более 40 примеров вопросов)(Для первых 100 читателей Mediumскидка 50%)