Абстрактные типы данных: тип определяют операции

Модуль "Объекты и ссылки" • урок 5 из 7 • ~7 минут

Суть урока. Абстрактный тип данных (АТД) - это тип, который задается набором операций и их свойствами, а не способом хранения. Программа видит только интерфейс (имена операций), а реализация спрятана внутри - это называется инкапсуляцией. Пока интерфейс не меняется, реализацию можно заменить целиком, не переписав ни строчки в использующих программах.
Историческая справка: как Вирт чинил Паскаль

В стандартном Паскале 1970-х полноценный АТД построить было нельзя: процедуры не существовали отдельно от программы. Из борьбы с этим недостатком родились языки Модула-2 и Ада.

Читать дальше

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

Проблемой озаботился сам автор языка - Никлаус Вирт. В 1976 году он выпустил язык с говорящим названием Модула, а в 1979 - Модула-2, где появились полноценные модули: отдельно компилируемые единицы, объединяющие данные и процедуры над ними и открывающие наружу только интерфейс. Именно там реализация АТД стала по-настоящему возможной. Свое модульное расширение Паскаля - язык Ада - заказало даже Министерство обороны США.

Дальнейшая судьба идеи известна: объектно-ориентированные языки поглотили АТД как неотъемлемую составную часть. Класс в C++ или Python - прямой наследник модуля с абстрактным типом данных: те же операции снаружи и те же спрятанные внутренности.

Что такое абстрактные типы данных

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

Ключевой сдвиг - в точке зрения. Что такое стек? Бесполезно отвечать "это массив с переменной-счетчиком": это уже реализация. Стек - это то, над чем определены операции "втолкнуть элемент", "вынуть верхний", "показать верхний", "проверить, пуст ли". Набор операций и есть определение типа. Такой взгляд называют операционным: АТД часто отождествляют с множеством его операций.

Интерфейс снаружи, реализация внутри

Чтобы АТД можно было отдать другому программисту (или продать), его оформляют как автономную программную единицу - модуль. В самой развитой форме определение АТД состоит из четырех частей:

  1. интерфейс (видимая часть) - имя типа, имена операций, типы их аргументов и результатов;
  2. абстрактное описание операций - что они делают, сформулированное на языке спецификаций, без кода;
  3. конкретная реализация - те же операции, написанные на обычном языке программирования;
  4. обоснование соответствия - объяснение, почему реализация корректно воплощает абстрактное описание.

Сокрытие внутренних, служебных структур реализации за интерфейсом называется инкапсуляцией. Она защищает данные от случайной порчи (нельзя залезть внутрь стека в обход операций) и дает свободу: внутренности можно переделывать, пока снаружи все выглядит по-прежнему.

Абстрактные типы данных на примере стека: программа видит только операции push, pop, top и empty, а реализация скрыта - сплошной массив или цепной список можно заменить, не меняя программу
АТД стек: интерфейс и скрытая реализация

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

Пример: стек как АТД на C

В C роль интерфейса играет заголовочный файл. Все, что нужно знать использующей программе, умещается в несколько строк:

/* stack.h - интерфейс АТД: это видят все */
typedef int TValue;

void   push(TValue v);  /* втолкнуть элемент     */
TValue pop(void);       /* вынуть верхний        */
TValue top(void);       /* показать верхний      */
int    empty(void);     /* пуст ли стек?         */

Реализация живет в отдельном файле stack.c. Слово static прячет внутренние данные от остальной программы - это и есть инкапсуляция средствами C:

/* stack.c - реализация: снаружи не видна */
#include "stack.h"

static TValue body[100];  /* сплошное представление */
static int total = 0;     /* static = снаружи не достать */

void push(TValue v) { body[total++] = v; }
TValue pop(void)    { return body[--total]; }
TValue top(void)    { return body[total - 1]; }
int empty(void)     { return total == 0; }

Программа пользуется стеком только через операции из stack.h. Завтра можно переписать stack.c на цепное представление - узлы с указателями и malloc, чтобы снять ограничение в 100 элементов, - и программа этого даже не заметит: достаточно перекомпилировать проект. Сам стек во всех подробностях нас ждет в модуле 3.

А как в Python? Питонист живет среди готовых АТД: список, словарь, множество - это именно наборы операций, а их внутренности (динамический массив указателей, хеш-таблица) спрятаны в интерпретаторе. Свой АТД оформляют классом, но жесткой инкапсуляции в языке нет - есть соглашение: атрибуты с подчеркиванием (self._body) считаются внутренними, и лезть к ним снаружи не принято.

Коротко: главное

  • Абстрактный тип данных (АТД) - тип, построенный программистом и заданный набором операций, а не способом хранения.
  • Интерфейс - видимая часть АТД: имена операций, типы аргументов и результатов.
  • Инкапсуляция - сокрытие реализации за интерфейсом; в C ее дают пара файлов .h/.c и слово static.
  • Пока интерфейс неизменен, реализацию (например, сплошную на массиве или цепную на указателях) можно заменять, не переписывая использующие программы.
  • Идею АТД довели до совершенства модули (Модула-2, Ада), а затем поглотили классы объектно-ориентированных языков.

Проверьте себя

Внутри стека массив заменили на связный список, а интерфейс (push, pop, top, empty) остался прежним. Что придется переписать в программах, которые пользуются этим стеком?

Как вам урок? Спасибо за отзыв!