Статьи

Доменные службы и ограниченный контекст с использованием Akka — часть 2


В
первой части этой серии статей вы увидели, как мы можем смоделировать хранилище доменов в качестве актера в Akka. Он предоставляет декларативную семантику транзакций через STM Akka и поддержку подключаемого модуля сохраняемости в различных хранилищах данных. В результате доменная модель становится чище. Созданный вами репозиторий может использовать возможности отказоустойчивости Akka с помощью супервизоров, которые предлагают настраиваемые стратегии жизненного цикла.

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

Используя Akka, вы моделируете сервис как еще одного актера. Доменные сервисы — это грубые абстракции уровня и те, которые получают запросы от клиентов. Он может вызывать другие службы или использовать любые другие права для выполнения работы, которую он должен делать. Неудивительно, что занятый сервис получает запросы от множества потребителей. Он не только должен быть стабильным, но и должен обеспечивать, чтобы все его другие службы, с которыми он сотрудничал, также оставались в живых при обслуживании запросов.

Одним из сервисов, с которыми он взаимодействует, является репозиторий доменов, о котором я говорил в предыдущем посте.

Разрабатывая доменную службу с использованием акторов Akka, вы можете гарантировать, что служба сможет сделать свои совместные службы отказоустойчивыми благодаря декларативному или минимальному программированию. Akka runtime предлагает все механизмы, которые упрощают внедрение отказоустойчивых сервисов.

Рассмотрим следующую службу домена для управления учетными записями, продолжая наш предыдущий пример из последнего поста.

trait AccountServer extends Actor {
// handle crash
faultHandler = Some(OneForOneStrategy(5, 5000))
trapExit = List(classOf[Exception])

// abstract val : the Repository Service
val storage: AccountRepository

// message handler
def receive = {
case Open(no, name) =>
storage ! New(Account(no, name, Calendar.getInstance.getTime, None, 100))
case msg @ Balance(_) => storage forward msg
case msg @ Post(_, _) => storage forward msg
case msg @ OpenM(as) => storage forward msg
}

// shutdown hook
override def shutdown = {
unlink(storage)
storage.stop
}
}

Обработчик сообщений является стандартным, который перенаправляет клиентские запросы в хранилище. Обратите внимание на использование абстрактного хранилища val: AccountRepository, которое помогает вам отложить фиксацию для конкретного класса реализации до создания экземпляра объекта службы.

AccountServer играет роль супервизора для субъекта хранилища. Первые 2 строки кода определяют стратегию надзора. OneForOneStrategy говорит, что
только компонент, который потерпел крах, будет перезапущен. Вы можете сделать это AllForOne также, когда контролирующий актер перезапустит
всех актеров , которых он контролирует, если один из них потерпит крах. trapExit определяет список исключений в связанном действующем субъекте, для которого контролирующий субъект будет выполнять действие.

AccountServer является действующим лицом для хранилища. Когда он выключен, он должен быть отключен от связанных актеров. Это мы делаем в хуке выключения AccountServer.

Но как нам связать актера репозитория с нашим актором доменной службы? Обратите внимание, что AccountServer еще не является конкретным объектом. Нам нужно создать конкретную реализацию AccountRepository и назначить ее для хранения в AccountServer. И свяжите два во время этого экземпляра.

Мы определяем другую черту, которая разрешает абстрактный val, который мы определили в AccountServer, и предоставляет конкретный экземпляр AccountRepository. spawnLink не только запускает экземпляр реализации репозитория на основе Redis, но также связывает субъект репозитория с AccountServer.

trait RedisAccountRepositoryFactory { this: Actor =>
val storage: AccountRepository = spawnLink(classOf[RedisAccountRepository])
}


Now we have all the components needed to instantiate a fault tolerant domain service object.

object AccountService extends
AccountServer with
RedisAccountRepositoryFactory {

// start the service
override def start: Actor = {
super.start
RemoteNode.start("localhost", 9999)
RemoteNode.register("account:service", this)
this
}
}

Посмотрите на метод запуска, который запускает службу на удаленном узле. Мы запускаем удаленный узел, а затем регистрируем текущий сервис под идентификатором
«account: service» . Любой клиент, которому необходимо использовать службу, может получить субъект службы, указав этот идентификатор .. как показано в следующем фрагменте.

class AccountClient(val client: String) { 
import Actor.Sender.Self
val service = RemoteClient.actorFor("account:service", "localhost", 9999)

def open(no: String, name: String) = service ! Open(no, name)
def balance(no: String): Option[Int] =
(service !! Balance(no)).getOrElse(
throw new Exception("cannot get balance from server"))
def post(no: String, amount: Int) = service ! Post(no, amount)
def openMulti(as: List[(String, String)]) = service !!! OpenMulti(as)
}

Сервисы, определенные с использованием Akka, могут быть запущены на удаленных узлах без особых технических проблем. Akka runtime предлагает API для этого. Тем не менее, Акка никогда не пытается скрыть от вас парадигмы распределения. Вы должны знать о своих требованиях к дистрибуции и иерархии процессов и контроля. Akka помогает вам определить их на уровне приложения, выполняя тяжелую работу в рамках своей базовой реализации. Этот принцип основан на философии Эрланга и резко контрастирует с RPC-способом определения API. Наряду с преимуществами вычислений на основе сообщений, которые разъединяют отправителя и получателя, Akka также позволяет обрабатывать состояния с помощью встроенного STM и подключаемого механизма хранения.

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

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