Статьи

Разработка DSL для описания архитектуры программного обеспечения (часть 2)

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

Создание архитектурных шаблонов

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

artifact Module1
{
    include "Module1/**"

    artifact UI
    {
        include "**/ui/**"
        connect to Business
    }
    artifact Business
    {
        include "**/business/**"
        connect to Persistence
    }
    artifact Persistence
    {
        include "**/persistence/**" 
    }
    public artifact Model
    {
        include "**/model/**"
    }
}

artifact Module2
{
    include "Module2/**"

    artifact UI
    {
        include "**/ui/**"
        connect to Business
    }
    artifact Business
    {
        include "**/business/**"
        connect to Persistence
    }
    artifact Persistence
    {
        include "**/persistence/**" 
    }
    public artifact Model
    {
        include "**/model/**"
    }
}

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

Мы также представили новый модификатор артефакта на лету: public . Все артефакты, помеченные как публичные, могут использоваться всеми непубличными артефактами на одном уровне (братьями и сестрами в дереве артефактов). Таким образом, «UI», «Business» и «Persistence» имеют неявную связь с «Model» (от соединителя по умолчанию до интерфейса по умолчанию).

// File layering.arc
artifact UI 
{ 
    include "**/ui/**"
    connect to Business 
} 
artifact Business 
{ 
    include "**/business/**"
    connect to Persistence 
} 
artifact Persistence 
{ 
    include "**/persistence/**" 
}
public artifact Model
{
    include "**/model/**"
}

// New file modules.arc 
artifact Module1
{
    include "Module1/**"

    apply "layering"
}

artifact Module2
{
    include "Module2/**"

    apply "layering"
}

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

Расширение шаблонных артефактов

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

artifact Module2
{
    include "Module2/**"

    apply "layering"

    // New layer
    artifact BusinessInterface
    {
        include "**/businessinterface/**"
    }
    // Now Business and UI need access to BusinessInterface
    extend Business
    {
        connect to BusinessInterface
    }
    extend UI
    {
        connect to BusinessInterface
        // UI should not use Business directly
        disconnect from Business
    }
}

Расширение артефакта имеет смысл только в контексте директив применения . Это позволяет нам добавлять вложенные элементы в артефакт и / или изменять его соединения с другими артефактами. Внутри расширенного артефакта вы также можете использовать ключевое слово override, чтобы переопределить определения интерфейсов или коннекторов, определенных в исходной версии артефакта:

artifact Module2
{
    // ...
    extend Business
    {
        // This assumes that the template version of Business has an interface named "X"
        override interface X
        {
            // Use other patterns or other exports
            include "**/x/*"
        }
        connect to BusinessInterface
    }
    // ...
}

Это позволяет при необходимости адаптировать элементы архитектуры, полученные из шаблона.

Ограничение типов зависимостей

Иногда вы находитесь в ситуации, когда вы разрешаете одному артефакту использовать другой, но хотели бы ограничить использование зависимостями определенного типа. Например, давайте предположим, что вы не хотите, чтобы уровень пользовательского интерфейса создавал новые экземпляры классов, определенных в слое «Модель». Только «Бизнес» и «Постоянство» позволят создавать «Модельные» экземпляры. Вы можете решить эту проблему, создав новый интерфейс, который ограничивает использование определенных типов зависимостей:

artifact UI 
{ 
    include "**/ui/**"
    connect to Business, Model.UI
} 
artifact Business 
{ 
    include "**/business/**"
    connect to Persistence, Model
} 
artifact Persistence 
{ 
    include "**/persistence/**" 
    connect to Model
}
artifact Model
{
    include "**/model/**"
    interface UI
    {
        include all // everything in "Model"
        exclude dependency-types NEW
    }
}

Теперь это будет помечено как нарушение архитектуры, если класс из уровня пользовательского интерфейса создаст новый экземпляр объекта из уровня модели. Обратите внимание, что нам пришлось удалить публичный модификатор из «Model». Если бы мы сохранили его, то было бы неявное соединение UI с интерфейсом модели по умолчанию, которое бы игнорировало наше специальное ограничение.

В настоящее время язык поддерживает следующий список типов абстрактных зависимостей, не зависящих от языка:

CALL          // all non-virtual function or method calls
VIRTUAL_CALL  // call of a virtual method
EXTENDS       // inheritance
IMPLEMENTS    // interface implementation
NEW           // instance creation
READ          // reading a field or variable
WRITE         // writing to a field or variable
USES          // all other uses

На этом заканчивается вторая статья серии. В следующей части мы рассмотрим еще одну продвинутую концепцию, которая называется «схемы подключения». Опять, пожалуйста, дайте мне знать, что вы думаете. Комментарии приветствуются.