Эта глава представляет собой первый из нескольких примеров (или парадигм, в первоначальном значении этого слова) программ, разработанных с помощью объектно-ориентированных методов. Программы достаточно малы, поэтому мы сможем привести их версии для каждого обсуждаемого в книге языка. Последующие примеры будут представлены только на одном языке.
После описания задачи мы обсудим, чем объектно-ориентированное решение отличается от других. Глава заканчивается программным кодом на различных языках.
В шахматах ферзь может бить любую фигуру, стоящую в одном с ним ряду, столбце или диагонали. Задача о восьми ферзях является классической логической проблемой. Необходимо расставить на шахматной доске 8 ферзей так, чтобы ни один из них не бил другого. Решение приведено на рис. 5.1, но оно не единственное. Задача о восьми ферзях часто используется для иллюстрации решения логических задач и техники вычислений с возвратом (см., например, [Griswold 1983, Budd 1987, Berztiss 1990]).
Чем объектно-ориентированное решение задачи о восьми ферзях отличается от кода на обычном директивном языке программирования? В традиционном решении для описания позиций фигур использовалась бы какая-нибудь структура данных. Программа решала бы задачу, систематически перебирая значения в этих структурах и проверяя каждую позицию: не удовлетворяет ли она условию?
Можно привести забавную и поучительную метафору о разнице между традиционным и объектно-ориентированным решениями. Традиционная программа подобна человеку, находящемуся над доской и передвигающему безжизненные фигуры. В объектно-ориентированном подходе, напротив, мы наделяем фигуры жизнью, чтобы они решили проблему самостоятельно. То есть вместо одного существа, управляющего процессом, мы разделяем ответственность за нахождение решения среди многих взаимодействующих агентов. Это происходит так, как если бы шахматные фигуры были одушевленными существами, взаимодействующими между собой, которым поручено найти решение.
Таким образом, сущность нашего объектно-ориентированного подхода состоит в создании объектов, представляющих ферзей, и наделении их способностями найти решение. С точки зрения программирования как имитации, приведенной в главе 1, мы создаем мир, определяя в нем поведение его объектов. Когда состояние мира стабилизируется, решение будет найдено.
Как определить поведение каждого ферзя, чтобы группа ферзей, работающих совместно, могла прийти к решению? Первое наблюдение состоит в том, что в любом случае никакие два ферзя не могут занимать один столбец и, следовательно, все столбцы заняты. Поэтому мы можем сразу присвоить каждому ферзю определенный столбец и свести задачу к более простой — найти соответствующие строки.
Чтобы прийти к решению, ферзи должны взаимодействовать друг с другом. Поняв это, мы можем сделать следующее важное наблюдение, которое очень упростит нашу задачу, а именно: каждый ферзь должен знать только о своем соседе слева. Таким образом, данные о ферзе будут состоять из трех значений: столбец, который остается неизменным; строка, меняющаяся в ходе решения; и сосед слева.
Определим приемлемое решение для столбца N как такую конфигурацию столбцов с 1 по N, в которой ни один ферзь из этих столбцов не бьет другого. Каждому ферзю будет поручено найти приемлемое решение для себя и своих соседей слева. Мы получим решение всей задачи, когда самый правый ферзь отыщет приемлемое решение. Описание класса Queen (Ферзь) на CRC-карточке, включая данные о каждом представителе (напомним, что эта информация содержится на обороте), приведено на рис. 5.2.
5.1. Задача о восьми ферзях
Рис. 5.1. Одно из решений задачи о восьми ферзях5.1.1. Создание объектов, решающих «самих себя»
|
Мы разделим задачу на части. Метод initialize устанавливает необходимые начальные условия, в данном случае — просто задает данные. Далее обычно сразу же следует вызов метода findSolution, находящего решение для данного столбца. Поскольку решение часто будет неудовлетворительным для последующих ферзей, сообщение advance (продвинуться) используется для перехода к следующему решению.
Ферзь из столбца N инициализируется присваиванием номера столбца и определением соседнего ферзя (из столбца N–1). На этом уровне анализа мы оставим неопределенными действия самого левого ферзя, не имеющего соседа. Предполагаем, что ферзи-соседи уже инициализированы, это значит, что они нашли взаимно удовлетворяющее решение. Ферзь из текущего столбца просто ставит себя в первую строку. Ниже приведен алгоритм на псевдокоде.
|
Чтобы найти решение, ферзь просто спрашивает своих соседей, могут ли они его атаковать. Если да, то ферзь продвигается. Если дальнейшее передвижение запрещено, возвращается значение «неудача». Если соседи сообщают, что они атаковать не могут, то (локальное) решение найдено.
|
Как мы указывали в главе 4, псевдопеременная self обозначает получателя последнего сообщения. В данном случае мы хотим, чтобы ферзь, которому поручено найти решение, послал сообщение advance самому себе.
Процедура advance распадается на два случая. Если мы еще не достигли конца, ферзь просто увеличивает номер строки на 1. Если ферзь уже попробовал все позиции и не нашел решения, то не остается ничего, кроме как попросить у соседа нового решения и начать со строки 1.
5.2.3. Продвижение на следующую позицию
|
Теперь осталось только напечатать решение. Это делается очень просто с помощью метода print, который рекурсивно повторяется для всех соседей.
|
В этом параграфе мы приведем решение задачи о восьми ферзях для каждого рассматриваемого языка программирования. Сравните тексты программ. Обратите внимание на то, как свойственные языку черты привносят хитроумные изменения в решение задачи. В частности, проанализируйте решения на языках Smalltalk и Objective-C, использующие специальный «караульный» класс, и сравните их с кодом на Object Pascal, C++ или Java, каждый из которых использует нулевой указатель для самого левого ферзя и постоянно вынужден проверять значение указателей.
Задача о восьми ферзях на различных языках программирования
Определение класса для задачи о восьми ферзях на языке Object Pascal версии Apple приведено ниже. Тонкой и важной чертой является то, что это определение рекурсивно — объекты типа Queen содержат поле данных типа Queen. Это достаточно ясно показывает, что объявление и выделение памяти не обязательно связаны. В противном случае для каждого объекта Queen потребовалось бы бесконечно много памяти. Мы сравним это с кодом для C++ и детально рассмотрим связь между объявлением и выделением памяти в главе 12.
5.3.1. Задача о восьми ферзях: Object Pascal
|
Для языка Delphi определение класса будет отличаться совсем немного (оно показано ниже). Вариант языка для фирмы Borland позволяет разбить определение класса на открытую (public) и закрытую (private) части, а также добавить функцию-конструктор, используемую вместо программы initialize.
|
Псевдокод, приведенный в предыдущих параграфах, достаточно близок к варианту на языке Pascal с двумя основными отличиями. Первое: отсутствие в языке Pascal оператора return. Второе: необходимость сначала проверить, есть ли у ферзя сосед, перед посылкой сообщения этому соседу. Функции findSolution и advance, приведенные ниже, демонстрируют эти различия. (Заметим, что язык Delphi Pascal отличается от стандартного Pascal тем, что допускает «быстрое» выполнение директив and (и) и or (или), как и C++. Таким образом, код на языке Delphi может в одном выражении объединить проверку существования ненулевого соседа и посылку сообщения этому соседу.)
|
Основная программа отводит память для каждого из восьми ферзей и инициализирует их, определяя столбец и соседа. Поскольку во время инициализации будет найдено первое (локальное) решение, ферзи должны будут напечатать это решение. Код на языке Apple Object Pascal, выполняющий эту задачу, показан ниже. Здесь neighbor и i — временные переменные, используемые во время инициализации, а lastQueen — последний созданный ферзь.
|
Предоставляя явные конструкторы, объединяющие создание новых объектов и их инициализацию, язык Delphi позволяет ликвидировать одну из временных переменных. Основная программа на Delphi будет такова:
|
Задача о восьми ферзях на различных языках программирования
Наиболее существенной разницей между приведенным ранее описанием алгоритма на псевдокоде и типичным кодом для C++ является непосредственное использование указателей. Ниже приведено описание класса Queen. Каждый представитель класса содержит в себе указатель на другого ферзя. Заметьте, что в отличие от Object Pascal в языке C++ это значение должно быть объявлено как указатель на объект, а не как сам объект.
|
Как и в случае программы на языке Delphi Pascal, мы реализовали метод initialize в конструкторе. Коротко опишем его.
Есть три поля данных. Целочисленное поле column объявлено как const. Это определяет поле как неизменяемое. Третье поле данных имеет тип указателя; оно либо содержит нулевое значение (то есть ни на что не указывает), либо ссылается на другого ферзя.
Поскольку инициализация осуществляется конструктором, основная программа может просто создать восемь объектов-ферзей и потом напечатать решение. Переменная lastQueen указывает на последнего созданного ферзя. Вначале это значение равно null-указателю — ни на что не указывает. Затем в цикле конструируются восемь ферзей, инициализируемых значениями столбца и предыдущего ферзя. Когда цикл закончится, самый левый ферзь будет содержать нулевой указатель в поле neighbor, а все остальные — указатели на своих соседей; lastQueen указывает на самого правого ферзя.
|
Мы опишем только методы, иллюстрирующие важные моменты. Полное решение можно посмотреть в Приложении A.
Конструктор должен использовать в заголовке инициализирующую конструкцию для присваивания значения константе column, так как запрещается присваивать что-либо полям, объявленным как const. Инициализирующая конструкция используется и для поля neighbor, хотя мы не объявляли это поле как константу.
|
Поскольку значение переменной neighbor может быть либо ссылкой на ферзя, либо нулевым, необходима проверка, прежде чем сообщение будет послано соседу. Это показано в методе findSolution. Использование укороченного вычисления логических выражений и возможность выхода из середины процедуры упрощают код по сравнению с версией на Object Pascal — в остальном же они очень похожи.
|
Метод advance подобным же образом должен проверять наличие соседа перед тем, как продвигать его к следующему решению. Посылая сообщение себе, как это делается в рекурсивном findSolution, не обязательно указывать получателя.
|
Задача о восьми ферзях на различных языках программирования
Решение на языке Java во многом напоминает код на C++. Однако в Java тело метода пишется прямо «на месте», и указания public и private помещаются в определение класса. Ниже приводится определение класса Queen; некоторые методы опущены.
|
В отличие от языка C++ в Java связь со следующим ферзем определяется как объект типа Queen, а не как указатель на Queen. Перед посылкой сообщения ферзю, определяемому переменной neighbor, выполняется явная проверка на ненулевое значение.
Поскольку язык Java предоставляет богатое множество графических подпрограмм, решение будет отличаться от прочих тем, что окончательная расстановка ферзей будет нарисована на экране. Метод paint рисует ферзя, потом изображает соседей.
|
В языке Java нет ни глобальных переменных, ни «безклассовых» функций. Как мы опишем более детально в главе 8, программа начинается с определения подкласса системного класса Applet и переопределения некоторых методов. В частности, метод init используется для инициализации приложения, а метод paint — для перерисовки экрана. Мы также определим метод mouseDown, вызываемый при нажатии кнопки мыши, чтобы заставить программу переходить к следующему решению. Назовем класс нашего приложения QueenSolver и определим его так:
|
Заметьте, что класс приложения должен быть объявлен как public, чтобы быть доступным в основной программе.
Задача о восьми ферзях на различных языках программирования
Описание интерфейса нашего класса Queen таково:
5.3.4. Задача о восьми ферзях: Objective-C
|
Каждый ферзь имеет три поля данных: строку, столбец и ферзя-соседа. Последнее объявлено с типом id. Это указывает, что значением переменной будет объект, хотя и не обязательно ферзь.
В действительности мы можем использовать бестиповую сущность переменных в языке Objective-C себе во благо. Применим технику, которая невозможна или, по крайней мере, не так проста в языках со строгими ограничениями на тип (такими, как C++ или Object Pascal). Вспомним, что самый левый ферзь не имеет соседа. В решении на языке C++ признаком этого служило нулевое значение переменной, содержащей указатель на соседа, для самого левого ферзя. В данном решении мы вместо этого создаем новый класс — караульное (sentinel) значение. Самый левый ферзь будет указывать на это караульное значение, тем самым обеспечивая действительного соседа каждому ферзю.
Караульные величины часто используются как маркеры конца; их можно обнаружить в алгоритмах, работающих со списками, такими как наш список ферзей. Различие между объектно-ориентированным «караулом» и более традиционными проверками состоит в том, что первый может быть активным — у него есть поведение, то есть он может отвечать на запросы.
Каково должно быть поведение караульных величин? Вспомним, что ссылки на соседа в нашем алгоритме используются двояко. Во-первых, чтобы убедиться, что интересующая нас позиция не атакована; караульная величина на такой запрос всегда должна отвечать отрицательно, поскольку она не может ничего бить. Второе использование ссылок на соседа — в рекурсивном вызове процедуры print. В этом случае караульная величина просто ничего не делает, так как не имеет никакой информации о решении.
Объединяя вместе все эти рассуждения, приходим к следующей реализации нашего «караульного ферзя»:
|
В полном решении есть часть, реализующая SentinelQueen, но к ней нет интерфейса. Это допустимо, хотя компилятор выдаст предупреждение, поскольку такая ситуация несколько необычна.
Использование караульного объекта позволяет методам класса Queen просто посылать сообщения соседу без предварительного изучения, является ли данный ферзь самым левым. Например, в методе canAttack:
|
Внутри метода посылка сообщения самому себе осуществляется с помощью псевдопеременной self:
|
Остальные методы аналогичны и здесь не рассматриваются.
Задача о восьми ферзях на различных языках программирования
Решение задачи о восьми ферзях на языке Smalltalk во многих отношениях похоже на Objective-C. Как и последний, язык Smalltalk учитывает тот факт, что самый левый ферзь не имеет соседа, вводя специальный караульный (sentinel) класс. Его единственная цель в том, чтобы предоставить получателя сообщений, посланных самым левым ферзем.
Караульная величина является единственным представителем класса SentinelQueen (подкласс класса Queen), который реализует следующие три метода:
5.3.5. Задача о восьми ферзях: Smalltalk
|
Единственное различие между версиями на Objective-C и Smalltalk в том, что программа на языке Smalltalk возвращает результат как список величин, а не выводит их на печать. Техника вывода на печать довольно хитро устроена в Smalltalk и различается от реализации к реализации. Возвращая список, мы можем отделить эти различия от основных методов.
Класс Queen является подклассом класса Object. Представители класса Queen содержат три внутренние переменные: row (строка), column (столбец) и neighbor (сосед). Инициализация осуществляется в методе setColumn : neighbor:.
|
Метод canAttack отличается от аналогичного метода для языка Objective-C только синтаксисом:
|
Вместо того чтобы проверять условие на отрицание, язык Smalltalk предоставляет явный оператор ifFalse, используемый в методе advance:
|
Цикл while в языке Smalltalk должен использовать блок при проверке условия, как в следующем примере:
|
Для получения списка окончательных позиций используется рекурсивный метод. Вспомним, что караульная величина создает пустой список в ответ на сообщение result:
|
Решение будет получено с помощью вызова следующего метода, не являющегося частью класса Queen и относящегося к некоторому другому классу, например, такому как Object:
|
Решение задачи о восьми ферзях, построенное без применения караульной величины, описано в моей более ранней книге по языку Smalltalk [Budd 1987].
Учебный пример: задача о восьми ферзях
Упражнения