Для начала рассмотрим некоторые базовые идеи программирования, которые часто отождествляют с ООП (что бы это ни значило). Некоторые из этих идей существовали не только задолго до появления термина ООП, но и задолго до появления компьютеров как таковых.
Абстракция
В далекой далекой галактике древности, у людей не существовало цифр отдельно от вещей. Два оленя или две палки обозначались отдельными словами и знаками. Так продолжалось до тех пор, пока кто-то не увидел, что можно выделить понятия "два", "три", "десять" и начать применять их ко всем перечислимым предметам. Так появилась абстракция "числа". Мы настолько привыкли к этому, что даже не задумываемся над их абстрактностью, а вот для древнего человека это был прорыв.
В наших программах, вообще, все с чем мы работаем - это абстракция. Более того, я возьмусь утверждать, что большая часть задач, которые решаются с помощью программирования, сводится к тому, чтобы создавать и использовать правильные абстракции. Под словом "правильные" я, ни в коем случае не имею в виду "много". Думаю, здесь собрались взрослые люди, которым не надо объяснять что мир не черный и белый, а во всем хороша золотая середина.
Перейдем от абстрактных рассуждений к конкретным примерам. Самое простое, что можно придумать, это функция.
Абстракция с помощью функций
Попробуйте с трех раз угадать, что происходит в этом коде:
const a = 5;
const b = 10;
const result = a * b;
Вероятно, вы ответите, что это умножение двух чисел, но я туда заложил другой смысл, а именно нахождение площади прямоугольника. И если в данной ситуации, об этом можно было бы догадаться, назови я переменную result
именем area
, то в более сложных ситуациях (даже в этой, но внутри большого блока кода) шансов просто нет.
Очевидно, что можно реализовать функцию и сделать код таким:
const a = 5;
const b = 10;
const area = getRectangleArea(a, b);
Обратите внимание на важнейшую идею абстракции. Первостепенно не сокращение дублирования кода, а создание кода, который выражает ваши мысли прямо. Если нам нужно найти площадь прямоугольника, то именно это мы и делаем в коде. В 2013 году, я придумал специальный термин "ментальное программирование" с таким определением:
Ментальное программирование это написание такого кода, читая который, можно восстановить исходную задачу в первоначальных терминах
Функция как абстракция довольно понятная вещь, особенно приятно если она чистая. А что насчет данных?
Абстракция с помощью Данных
Чтобы не ходить далеко, разберем пример используемый в курсе на хекслете. Опишем код, который позволяет работать с отрезками на координатной плоскости.
const segment1 = [1, 10, 5, 2];
const segment2 = [-5, 0, 9, 10];
const startPoint1 = getStartPoint(segment1); // [1, 10]
const crossed = hasIntersection(segment1, segment2);
const rotatedSegment = rotate(10, segment1);
В этом коде видно применение функций, что резко облегчает его понимание и это хорошо, но кое-что по прежнему очень плохо. Структура отрезка торчит наружу, следовательно если захочется поменять реализацию, то придется найти все места где создаются сегменты и переписать их. В примере выше, скорее всего, захочется изменить структуру на подобную: [[1, 10], [5, 2]]
. То есть здесь мы видим явное выделение понятия "точка". В свою очередь, для точек можно ввести собственную абстракцию, что упростит реализацию функций сегментов, так как они будут опираться на функции для работы с точками. Эта концепция носит название "барьеры абстракции".
Так как же правильно создавать сегменты? И снова нам помогают функции:
const segment1 = makeSegment(1, 10, 5, 2);
// Вариант с точками
const segment2 = makeSegmentWithPoints(makePoint(1, 10), makePoint(5, 2));
У нас есть конструктор makeSegment
, так же у нас есть селекторы (геттеры) getStartPoint
и getFinishPoint
. Напоминает что-нибудь? Получилось, что мы реализовали придуманный нами АТД.
Возникает закономерный вопрос: а что будет, если кто-то поправит данные напрямую, в обход интерфейсных функций?
Справедливости ради замечу, что в любом языке, при желании, можно обойти ограничения абстракции (чем бы она ни была выражена) и, в реальности, это не такая огромная проблема как многим кажется. Не в том смысле, что не нужно соблюдать инварианты, их соблюдать как раз таки нужно, а в том, что если у вас есть интерфейс и до него легко добраться, никто не будет идти в обход. Но, демонстрации ради, я покажу то, как можно добиться по-настоящему серьезной инкапсуляции:
// Определения
const makeSegment = (point1, point2) => (message) => {
switch (message) {
case 'getStartPoint':
return point1;
case 'getFinishPoint':
return point2;
default:
// some handling
}
};
const getStartPoint = segment => segment('getStartPoint');
// Использование
const segment = makeSegment(makePoint(1, 10), makePoint(5, 2));
console.log(getStartPoint(segment)); // [1, 10]
Вероятно, для многих (кто не учится на хекслете, хехе) этот код выглядит шокирующе, хотя в реальности здесь используется только свойство замыкания, и наши данные представлены функциями (!!!). Поэкспериментируйте на repl.it , чтобы хорошо разобраться в этом коде.
Теперь добраться до кишок можно только с помощью посылки сообщения. Напрямую воздействовать на составные части нашего сегмента просто невозможно. Причем для этого не использовались специализированные средства, все получилось на одних функциях.
В итоге мы получили хорошо защищенную абстракцию, которую легко расширять и которой легко пользоваться. Она моделирует заданную предметную область, и говорим мы о сегментах в терминах сегментов, а не концепций языков программирования.
Выше я написал, что в коде используется "посылка сообщений", но не дал объяснения. Исправляюсь. Как правило, в языках вызов конкретной функции - это прерогатива языка, если функция определена, то она будет вызвана, если нет, то возникнет ошибка. В случае с посылкой сообщения ответственность за реакцию ложится на приемник. Технически есть разные способы реализации, один из которых показан выше. Функция принимает на вход строчку-сообщение, и внутри решает как отреагировать на основе конструкции switch
. Важно то, что можно определить какое-то дефолтное поведение, более того, можно строить динамические сообщения и, даже менять состав ожидаемых сообщений после ответа на предыдущее. Но не все языки предоставляют такую возможность: например в java нет посылки сообщений (но можно эмулировать используя reflection), в php и ruby есть, а в js они реализуются с помощью механизма Proxy
.
Как видите ООП нет (а может есть? ;), а многие идеи, которые постоянно приписывают этому трехбуквию, - есть.
Вопросы на самопроверку
Что означает фраза применительно к ОС семейства *NIX:
Все есть файл