Об абстракции в программировании

(выступление на семинаре "Вопросы программирования") 14.12.84

Я, конечно, не собираюсь вскрывать весь пласт проблем, связанных с абстракцией в программировании. Но, с чисто практической точки зрения я пришёл к мысли, что практически все проблемы программирования, в конечном счете, упираются в абстракцию - именно неумение абстрагироваться, отсутствие языковых средств для описания абстрактных объектов, непонимание необходимости выделения таких объектов - всё это, в конечном счете, порождает плохое качество программ (я не говорю здесь о проблемах чисто технологических, которые ещё более усугубляют положение).

Чтобы пояснить эту мысль, я должен вернуться к временам первопрограммистов. Первые машины вынуждали программиста присутствовать на всех уровнях абстракции - от общего (как правило, математического) алгоритма до его программного эквивалента и даже до машинных тонкостей. В его представлении смешивались объекты всех уровней - скажем, обозначение А матрицы, адрес массива ячеек, который воплощает эту матрицу и, наконец, алгоритмы реализации операций над матрицами и более того, алгоритмы доступа к элементам матрицы и даже вопросы их представления. Первые языки программирования позволили программисту абстрагироваться от конкретных машинных деталей и уменьшить количество уровней абстракции. Именно в этом причина их успеха. Фактически эти языки позволяли программисту работать почти на том же уровне абстракции, какой был в прикладной задаче. Дело в том, что задачи решались хорошо математически определённые, сложные в основном по логике, а математические объекты были самые элементарные - переменная, вектор, матрица. Для тех времён такой абстракции было вполне достаточно - и достаточно сейчас для большинства вычислительных задач. Потому супербедный ФОРТРАН до сих пор популярен среди тех, кому нужно просто посчитать. Поэтому нас учили, да и мы, фактически, сейчас учим студентов тому, чтобы уметь записывать логику программы. Ведь при таком подходе программирование - это перенесение логики алгоритма на язык программирования, не более того. Всё сводится к логике.

Крупным достижением тех времён были подпрограммы. Вызывая подпрограммы, мы абстрагируемся от логики их выполнения. Если удачно разбить программу на подпрограммы, мы сможем в каждой из них работать на одном уровне абстракции - скажем, подпрограмма перемножения матриц имеет дело только с элементами их. В головной же программе программист работает уже с матрицами, не опускаясь до уровня элементов, т.е. количество уровней абстракции уменьшается (точнее говоря, уменьшается не само количество этих уровней, а количество уровней, с которыми работает в данный момент программист). Именно в этом главное преимущество подпрограмм, но не все это понимают, и потому выделяют в подпрограмму обычно просто повторяющиеся действия, не связывая их с понятием абстракции. Я не спорю, что это тоже преимущество подпрограмм, но экономия сил в данном случае - не главный выигрыш. Если не выделен абстрактный объект, который "прячется" за подпрограммой, тот эту подпрограмму ждёт нелёгкое будущее - она либо не будет использоваться, либо будет бесконечно переделываться: история системы ДЕЛЬТА тому яркий пример. При разбитии системы на модули шли от логики, а не от объектов, и поэтому все общие модули постоянно переделываются.

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

1.На каждом уровне ничего не известно о свойствах и даже о существовании более высоких уровней.

2.На каждом уровне ничего не известно о внутреннем строении других уровней.

3.Каждый уровень представляет собой группу модулей.

4.Каждый уровень располагает определёнными ресурсами и либо скрывает их от доугих уровней, либо представляет другим уровням некоторые их абстракции.

5.Каждый уровень может обеспечивать некоторую абстракцию данных в системе.

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

7. Связи между уровнями ограничены явными аргументами.

8.Каждый уровень должен иметь высокую прочность и слабое сцепление.

Таким образом, прежде чем проектировать систему, нужно представить её иерархически. Это прямо вытекает из организации человеческого мозга. Он может манипулировать ограниченным количеством объектов (не более семи) и к тому же способен эффективно понимать одновременно не более двух уровней абстракции - текущий и предыдущий. Поэтому, когда мы при написании программы ударяемся в детали, чтобы реализовать некий сегмент, то потом, выйдя из него, с трудом вспоминаем, зачем это было нужно. Нисходящий подход родился именно из этих соображений - нужно было найти путь такой, чтобы во время всего процесса проектирования находиться на одном уровне абстракции и манипулировать, возможно, меньшим количеством объектов. Подход хорош, но не панацея, так как мы не умеем выделять абстрактные объекты, и потому уровни абстракции распределяем в соответствии с логикой программы. Мы по-прежнему абстрагируемся от логики программы, но, как правило, вынуждены точно определять данные, проходящие через несколько сегментов программы. Поэтому уровни абстракции не получаются, получаются просто уровни подпрограмм. И, если в хорошо определённых математических задачах этот фокус проходит, так как там абстракция идёт в основном по логике, то для других задач - нет. В численных задачах был даже такой оптимизм, что мы скоро напишем подпрограммы на все случаи жизни и останется только их вызывать в определённом порядке. Это где-то действительно так, можно набрать побольше пакетов прикладных программ и вычислительные задачи решать без особого труда.

Однако в целом этот оптимизм не оправдался, так как программирование ушло от вычислительных задач и шагнуло к информационным, а тут уже на первое место вышли не логика, а данные. И, если раньше задача была - при бедном наборе типов данных реализовать как можно больше функций над ними, то теперь - при довольно бедном наборе функций охватить как можно больше типов данных. В это направление вписывается, скажем, и наша ДЕЛЬТА. Наши потуги в основном направлены на то, чтобы одну и ту же функцию распространить на разные типы данных, например, посчитать одномерную таблицу не только по дискретным предположениям, но и по непрерывным, и по символьным, причём уметь задавать какие-то структуры внутри этих данных - интервалы, автоматические и ручные, вот теперь группировка альтернатив и всякие так классы эквивалентности - при всём при этом функция остаётся та же - напечатать таблицу распределения частот.

На этом пути создаются всяческие языки, ориентированные на данные. Понятно, что используя достижения предыдущего этапа - от логики мы привычно абстрагируемся подпрограммами. Однако и тут не предел развития - потому что на самом деле (без машины) мы работаем не в терминах логики и/или данных - мы оперируем с объектами, имеющими определённые свойства. Приведу пример. У нас есть кубик, мы его поворачиваем, укладываем и т.д. От кубика, как от данных, мы абстрагируемся легко - можно задать его координатами трёх вершин. Мы можем абстрагироваться и от логики перемещений кубика, написав, например, подпрограммы типа "перевернуть", параметрами которых будут координаты кубика. Однако мы тем самым разъединили понятие "кубик", он перестал существовать как объект. Мы всё время говорим не о кубике, а о координате его. Более близкий и более сложный пример - в системе ДЕЛЬТА мы оперируем с понятием "документ", но для программистов он не существует - есть файлы, есть каталог и т.д. Мы берём какие-то стороны этого объекта и произвольно их изменяем. Возможно (и даже, как правило) объект становится противоречивым. Простой пример - нет информации или FF: мы думаем над тем, как изменить переменную, а меняется-то объект! От того, что мы его не выделили, он не перестал существовать! Вот так мы за деревьями не видим леса.

Я опять-таки прихожу к тому, что нужно выделять объекты и работать с объектами, а не с машинными структурами. На мой взгляд, современное программирование идёт именно к этому. Всем уже стало понятно, что фактически речь идёт об абстрактных типах данных, но я предпочитаю говорить о более широком понятии - объектно-ориентированном подходе к программированию. Это значительно шире, так как, кроме технических средств (языков и т.д.), здесь речь идёт и об изменении мышления программиста. Независимо от того, существуют или нет средства поддержки такого подхода, пора перестраиваться.

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

1.О-О подход заставляет ещё на этом этапе разработки задуматься над тем, какие объекты мы будем выделять, и какими свойствами они будут обладать. Это, во-первых, позволяет избежать многих переделок, а, во-вторых, позволяет заранее увидеть побочные эффекты и их нейтрализовать.

2. Объект становится защищённым от неправильного использования. Это прямое следствие его инкапсулирования. Это существенный момент в больших разработках - программисты не знают, да и не обязаны знать всех свойств объекта, поэтому возникают всякие побочные эффекты. В инкапсулированный объект можно встроить средства защиты - программист, его разрабатывающий, обычно хорошо его знает и знает его внутренние взаимосвязи.

3. Действия над объектом становятся единообразными. Если объект не выделен и не инкапсулирован, каждый работает с ним в меру своей испорченности - может вставить какой-нибудь свой признак и т.д. Типичный пример - печально знаменитый в ДЕЛЬТЕ FF, который и конец разреза, и "нет информации" и чёрт его знает что ещё, причём каждый вставляет его там, где считает нужным. Если же объект инкапсулирован, то все операции над ним реализуются одной и той же программой, и все спецпеременные имеют один и тот же смысл.

4.Программы сокращаются и упрощаются. Понятно, что каждая операция над объектом программируется только один раз. Мало того, что это ведёт к уменьшению количества ошибок, это сокращает и собственно длину курса. Упрощение идёт, понятно, за счёт упрощения структуры системы: она становится более однородна на каждом уровне, т.е. уровни системы соответствуют уровням объектов.

5.Резко увеличивается модульность. Под модульностью здесь понимается лёгкость модификации. Понятно, что при объектно-ориентированном подходе, когда объект не размазан по программе, а сосредоточен в одном модуле, мы можем довольно безболезненно менять структуру объекта, и это вызовет минимальные переделки всей системы. Это очень существенное преимущество, в том числе и для нас - посмотрите, мы год делали систему ДЕЛЬТА, и шесть лет её переделываем, и конца-края этому не видать: а пишем ли мы её так, чтобы легко переделывать было? Ясно, что мы этого хотим, но никаких усилий не прикладываем, некоторое исключение - только ТАБЛ, в котором объектно-ориентированный подход был полуосознанно применён и такие объекты, как БСАП, мы можем менять довольно безболезненно.

6. Появляется возможность диагностики (встроенной). Я уже говорил, что инкапсулированный объект может защищаться от неправильного употребления - это очень ценное свойство. Но, кроме того, обратим внимание на то, что при отладке мы обычно выдаём на печать именно характеристики основных объектов системы. Следовательно, средства эти можно встроить прямо в объекты и инициировать их по мере надобности. Таким образом, мы где-то подходим к неразрушающей отладке - это раз, а во-вторых, мы процесс отладки очень упрощаем, я уже не говорю о том, насколько облегчается сопровождение системы.

Таким образом, О-О подход удовлетворяет как требованиям разработки, так и модификации, сопровождения. Средства, конечно, для его реализации, пока довольно бедные, но это не означает, что от него нужно отказаться. И на ФОРТРАНЕ можно писать структурные программы, хотя в нём нет структурных операторов. В то же время на ПАСКАЛЕ можно писать ужасно неструктурные программы.

Я утверждаю, что проблема в основном - не в средствах.


© Алексей Бабий 1984