суббота, 18 февраля 2012 г.

Основы модульности

В прошлый раз мы с вами подошли к тому, чтобы создать первое приложение (хотя я бы взял это слово в кавычки). В тамошнем примере было показано, как импортировать модуль. Что значит «импортировать модуль»?

Давайте подробно рассмотрим вопросы, связанные с системой модульности в языке.

Модуль, как я писал раньше, это функциональная единица языка. Модули в D это не совсем то, к чему привыкли программисты, использующие C-подобные языки программирования. Если концепция пространства имен это всего лишь абстракция, то модули это жестко привязанная к конкретному файлу реализация. Дело в том, что каждый модуль в программе должен быть именован так, чтобы соответствовать физическому расположению файла.

То есть абсолютно любой файл с расширением .d — это модуль. Даже если вы явно не задаете название, оно будет соответствовать расположению файла.

Начнем с основ. Допустим, вы создаете новый файл, в котором разместите точку входа в приложение. Самая первая строка, которую вы должны написать, это декларация имени этого модуля. Как это сделать? Для этого есть ключевое слово module, которое отвечает за декларирование имени модуля. То, что стоит после него, есть название для модуля:
module name;
В этом примере мы обозначили, что файл, содержащий такую строку, является модулем с именем «name». Еще раз бы хотел напомнить, что имя модуля должно один к одному соответствовать его физическому нахождению на диске, с одной маленькой поправкой: имя может быть относительным. То есть, нет нужды в имя модуля закладывать весь путь.

Рассмотрим такой пример:
module c.code.d.projects.game.main;
Здесь мы представили имя модуля, а, следовательно, и путь до файла main.d в папке C:\Code\D\Projects\Game (записывать название модуля в нижнем регистре — это часть общепринятого стиля именования в D). Отличие в том, что вместо слешей (это которые косые черты — \\) необходимо указывать точку. В отделении элементов точками есть свой смысл, но об этом позже.

Указанное выше имя модуля избыточно, хотя и верно с точки зрения компилятора DMD. Для именования модуля вам необходимо и достаточно определить лишь корневой каталог, относительно которого вы будете строить систему модулей в своей программе (библиотеке).

Допустим, у вас есть некая папка «Projects», там вы заводите еще одну папку под вашу новую библиотеку, скажем, «NeoLight», внутри которой планируете расположить код. Положим, вы решили, что основной функционал вашей библиотеки будет в модуле «core». В таком случае вы создаете новый файл с именем core.d в папке «NeoLight». Название модуля будет «NeoLight.core» или «neolight.core». Думаю, логика вам понятна.

Когда вы определились с именем модуля, можно приступить к наполнению модуля функционалом, то есть фактически, к написанию кода.

Внутри модуля вы можете поместить все, что душе угодно: классы, константы, глобальные методы, структуры данных, интерфейсы, «псевдонимы» (см. Type Aliasing и Alias Declarations), перечисления и другое. Об этом мы поговорим в следующих постах, здесь же рассмотрим только систему модулей, реализованную в D.

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

Другими словами, если пространство имен формируется его элементами, то модуль описан только тем, что внутри его. Это хоть и не значительная, но очень важная особенность системы модульности в D. Если раньше вы порождали горы классов и типов для описания некоего пространства имен, которое решает определенную задачу, то здесь вы должны создавать самодостаточные модули, решающие конкретную задачу.

Еще одна отличительная особенность модуля в том, что у него может быть как конструктор, так и деструктор. Объявить их вы можете таким образом:
static this()
{
}

static ~this()
{
}
Не забывайте, что конструктор или деструктор обязательно должны быть статическими. Внутри одного модуля можно объявить несколько конструкторов и деструкторов, правда не понятно, зачем.

Конструктор модуля вызывается перед точкой входа в приложение, деструктор — после. Деструктор будет вызван только в том случае, если во время вызова конструктора не возникло никаких проблем. Деструктор может быть объявлен и без конструктора (и наоборот).

В случае нескольких конструкторов, они будут вызваны в том порядке, в котором объявлены. Деструкторы же, будут вызваны в обратном конструкторам порядке. Возникает вопрос: а что если конструктора в модуле два, а деструкторов четыре? Во-первых, бить по рукам тех, кто подобным занимается. Во-вторых, проводить эксперименты по выявлению порядка вызова, поскольку документация в этом плане мало что проясняет.


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

Нахождение модулей в одном пакете отлично от, например, нахождения двух классов в одном пространстве имен в C#. Там, это позволяло классам обращаться друг к другу, опуская директивы using. Другими словами, нахождение двух классов в едином пространстве позволяло организовывать взаимодействие так, как будто они описаны в одном файле.

Разделение модулей по пакетам дает совершенно другой результат. Точнее, разделение модулей по пакетам нужно лишь для структуризации вашего кода. То, что модули из одного пакета, не значит ровным счетом ничего, не дает им никаких привилегий. Именно поэтому я говорю о том, что каждый модуль — это самодостаточная единица.

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

Возьмем, например, tango.stdc.stdio. Словесное описание звучит так: модуль «stdio» является частью пакета «stdc», который, в свою очередь, находится внутри пакета «tango». Такая двухуровневая иерархия имеет под собой физическое основание — папка «tango», внутри которой папка «stdc» с указанным модулем. Не забывайте, что деление на пакеты это не абстракция, а отражение физического местонахождения файла.

Таким образом, в названии модуля, самое правое слово — это собственно имя модуля, а все, что слева — описание иерархии.


Взаимодействие модулей
Теперь самое главное: как использовать функционал модуля вне его? Для этого в D есть соответствующий механизм: импортирование модулей. Работает этот механизм следующим образом.

Допустим, есть два модуля: log и main.
module log;

void print(char[] message)
{
   
}
Вот мы объявили функцию print в модуле log, как нам теперь ей воспользоваться? Очень просто, надо всего лишь импортировать содержимое модуля с помощью ключевого слова import:
module main;

import log;

void main()
{
   print("hi");
}
Таким образом, обращение к функциям другого модуля происходит так, словно они были объявлены в данном модуле. Обратите особое внимание на то, что импортировать пакеты нельзя, только конкретные модули.

Давайте наполним print функционалом, чтобы переданная строка выводилась в консоль. Для этого нам понадобится модуль tango.io.Console, который поможет осуществить задуманное.
module log;

import tango.io.Console;

void print(char[] message)
{
   Cout(message).newline;
}
Здесь мы воспользовались методом Cout из указанного модуля. Пускай вас не сбивает с толку .newline после имени метода. Это обращение к свойству возвращаемого методом объекта, позволяющее перевести каретку на новую строку сразу, после вывода сообщения.

Скомпилировав это, на консоли вы увидите заказанный текст.

Архив с проектом.

1 комментарий:

  1. >Таким образом, обращение к функциям другого модуля происходит так, словно они были объявлены в данном модуле.
    Все это так до тех пор, пока мы не импортируем другой модуль, в котором объявлены функции с такими же именами. В этом случае придется обращаться к ним через имя_модуля.имя_функции(), как это сделано в Python.

    ОтветитьУдалить