Базовый CS с нуля
Что такое абстракция
Каждый урок до сих пор что-то разбирал на части. Ты видел число, разбитое на биты, инструкцию, разбитую на код операции и операнды, вызов функции, разбитый на положенный кадр стека с сохранённым адресом возврата. Разбирать вещи на части — это то, как ты узнал, чем они на самом деле являются.
Но ни один работающий программист не держит всё это в голове сразу. Когда ты пишешь
total = add(a, b), ты не выводишь заново, как CPU складывает два числа. Ты не
представляешь, как кладётся кадр стека. Ты просто пишешь add и доверяешь этому.
У этого доверия есть имя: абстракция. Это единственная идея, которая позволяет программе вырасти за пределы нескольких строк, не став невозможной для осмысления. Ты пользовался абстракциями девять разделов подряд. Этот урок наконец называет эту вещь и показывает её две половины — часть, которой ты пользуешься, и часть, которую ты не замечаешь.
После этого урока ты сможешь определить абстракцию как сокрытие детали за именем, объяснить разницу между интерфейсом и реализацией, указать на вызов функции и имя переменной как на абстракции, которыми ты уже пользовался, и сказать, почему сокрытие детали облегчает осмысление программы.
Абстракция: имя, которое скрывает деталь. Абстракция — это имя (или небольшой набор имён), которое позволяет пользоваться вещью, не зная, как эта вещь работает внутри.
Это слово называет сделку, которую ты заключаешь. Ты отказываешься от прямого знания внутренней детали. Взамен ты получаешь что-то короткое и устойчивое, на что можно ссылаться. После сделки ты думаешь об имени, а не о механизме за ним.
Ты уже заключал эту сделку много раз. В разделе 08 ты вызывал функцию add(a, b) и
получал сумму. В месте вызова ты не думал о том, как add произвела эту сумму. Имя add
заменяло собой всю эту деталь. Эта замена — имя вместо детали — и есть абстракция, и это
единственная причина, по которой большая программа вообще может быть написана.
Интерфейс: то, чем ты пользуешься. У каждой абстракции две стороны. Первая — это интерфейс: маленькая видимая поверхность, с которой ты взаимодействуешь — имя, входы, которые он ожидает, и результат, который он отдаёт.
Для функции add интерфейс ровно такой: имя add, факт, что она принимает два числа, и
факт, что она возвращает их сумму. Это всё, что нужно вызывающей стороне. Вспомни из
урока 03 раздела 08, что параметры функции — это её объявленные входы, а возвращаемое
значение — результат, который она отдаёт; вместе они и есть интерфейс функции.
Интерфейс — это контракт. Это обещание: «дай мне эти входы, и я верну тебе этот результат». Вызывающая сторона полагается на это обещание и ни на что больше.
Реализация: скрытое как. Вторая сторона — это реализация: настоящие инструкции, переменные и шаги, которые выполняют то, что обещает интерфейс.
Для add реализация — это тело функции, машинные инструкции, которые фактически
вычисляют сумму. Вызывающая сторона никогда не смотрит в тело. Оно скрыто за именем.
Ключевое свойство: реализация может полностью измениться, и вызывающая сторона этого не
заметит — при условии, что интерфейс держит своё обещание. add могла бы вычислять
сумму одной инструкцией или десятью; её можно было бы переписать целиком. Каждая
вызывающая сторона всё равно пишет add(a, b) и всё равно получает сумму. Интерфейс
остался неизменным; реализация была свободна меняться. Это разделение — неизменный
интерфейс, заменяемая реализация — и делает абстракцию полезной, а не просто аккуратной.
Почему это работает
Почему имя переменной — тоже абстракция. Вспомни из раздела 06, что переменная — это
имя для ячейки памяти. Когда ты пишешь score = 10, ты никогда не набираешь числовой
адрес ячейки. Имя score и есть абстракция: её интерфейс — это имя и значение, которое
можно читать или записывать; её реализация — настоящий адрес, выбранный рантаймом. Если
бы рантайм разместил score по другому адресу, твой код не изменился бы. Ты полагался на
эту абстракцию с раздела 06, не называя её так.
Сокрытие детали — это то, что делает программу осмысляемой. Зачем вообще отделять интерфейс от реализации? Потому что человек способен держать в уме лишь несколько вещей сразу.
Если бы использование add заставляло тебя думать ещё и о её инструкциях, а
использование этих инструкций заставляло думать о вентилях под ними, каждая строка кода
тянула бы за собой всю машину. Никакая программа сколько-нибудь значимого размера не
могла бы быть написана.
Абстракция разрывает эту цепь. Как только add работает и её интерфейс зафиксирован, ты
можешь пользоваться add, думая только об интерфейсе — два числа на вход, сумма на выход.
Реализация по-прежнему существует и по-прежнему выполняется, но её больше нет в твоей
голове. У тебя одна вещь для осмысления (add) вместо сотен (каждая инструкция внутри
неё). Это сокращение и есть вся выгода, а остаток этого раздела — про применение её в
большем масштабе.
Разделение одной функции на интерфейс и реализацию.
Вот крошечная функция и её вызывающая сторона:
function half(n: number): number {
let result = n / 2;
return result;
}
let x = half(10); // x становится 5Найди интерфейс. От чего на самом деле зависит вызывающая сторона в последней строке?
- Имя
half. - Что она принимает одно число.
- Что она возвращает это число, делённое на два.
Это весь интерфейс — три факта. Вызывающая сторона пишет half(10) и ожидает 5.
Найди реализацию. Что скрыто от вызывающей стороны?
- Локальная переменная
resultвнутри тела. - Факт, что деление происходит перед возвратом.
- Точная инструкция, которую CPU использует для деления.
Вызывающая сторона не зависит ни от чего из этого. result можно переименовать, две
строки тела можно слить в return n / 2, и вызывающая сторона не изменится ни на один
символ.
Проверка. Измени реализацию, сохрани интерфейс: работает ли вызывающая сторона
по-прежнему? Перепиши тело одной строкой return n / 2. Вызов half(10) всё равно
возвращает 5. Интерфейс выдержал; реализация сдвинулась. Это абстракция, работающая
ровно так, как задумано.
Частая ошибка
Распространённая ошибка — думать, что абстракция означает, будто деталь исчезает. Это
не так. Реализация add по-прежнему выполняется на CPU каждый раз, когда ты вызываешь
функцию, — каждый вентиль по-прежнему переключается. Абстракция скрывает деталь от
твоего внимания, а не от машины. Механизм по-прежнему на месте, по-прежнему делает
работу. Тебе просто дали имя, чтобы ты не смотрел на него.
У абстракции две стороны. Сторона, с которой ты взаимодействуешь — имя, входы, результат — имеет название. Введи 1, если эта сторона называется интерфейс, введи 0, если она называется реализация.
Функция add(a, b) возвращает сумму. Её тело переписали так, что сумма вычисляется по-другому, но она по-прежнему возвращает ту же сумму для тех же входов. Сколько вызывающих сторон add нужно изменить?
Когда ты пишешь score = 10, сколько числовых адресов памяти ты набираешь в своём коде?
Интерфейс функции состоит из её имени, её объявленных входов и её возвращаемого значения. Функция half принимает 1 число и возвращает 1 число. Считая имя плюс входы плюс возвращаемое значение, сколько частей у её интерфейса?
Верно или неверно: когда абстракция скрывает деталь, эта деталь перестаёт выполняться на CPU. Введи 1 за «верно», 0 за «неверно».
В чём разница между интерфейсом и реализацией?
Абстракция — это сокрытие детали за именем, чтобы пользоваться вещью, не зная, как она работает внутри. У каждой абстракции две стороны. Интерфейс — это видимая поверхность, от которой зависит пользователь: для функции это её имя, её входы и её возвращаемое значение; это контракт, обещание. Реализация — это скрытый механизм, выполняющий это обещание: тело функции и инструкции, которые оно запускает. Реализацию можно свободно переписывать, пока интерфейс держит своё обещание, поэтому вызывающие стороны никогда не меняются. Ты пользовался абстракциями с раздела 06: имя переменной скрывает адрес памяти, а вызов функции скрывает тело. Абстракция не удаляет деталь — машина по-прежнему её выполняет — она убирает деталь из твоего внимания, и именно это позволяет программе вырасти достаточно большой, чтобы быть полезной.