Czasem nasz aktor powinien reagować inaczej w zależności od swojego wewnętrznego stanu. Z reguły odebranie określonego typu komunikatu powoduje zmianę stanu i co za tym idzie, zmianę sposobu reakcji na przyszłe komunikaty. Inny komunikat powoduje powrót do pierwotnego zachowania. W poprzednim odcinku zaimplementowaliśmy aktora RandomOrgBuffer w oparciu o flagę waitingForResponse. Niepotrzebnie komplikuje ona i tak już złożoną obsługę nadchodzących komunikatów:


var waitingForResponse = false

def receive = {
  case RandomRequest =>
    preFetchIfAlmostEmpty()
      if(buffer.isEmpty) {
        backlog += sender
      } else {
        sender ! buffer.dequeue()
      }
  case RandomOrgServerResponse(randomNumbers) =>
    buffer ++= randomNumbers
    waitingForResponse = false
    while(!backlog.isEmpty && !buffer.isEmpty) {
      backlog.dequeue() ! buffer.dequeue()
    }
    preFetchIfAlmostEmpty()
  }
  
  private def preFetchIfAlmostEmpty() {
    if(buffer.size <= BatchSize / 4 && !waitingForResponse) {
      randomOrgClient ! FetchFromRandomOrg(BatchSize)
      waitingForResponse = true
    }
  }

Czyż nie byłoby prościej mieć dwie metody receive - jedną wykorzystywaną gdy oczekujemy na odpowiedź z zewnętrznego serwera (waitingForResponse == true) i drugą w przeciwnym wypadku? Właśnie w takich scenariuszach pomocne okazują się metody become() i unbecome(). Domyślnie obsługą komunikatu zajmuje się właśnie receive. Jednak w każdej chwili możemy wywołać become(), której argumentem jest dowolna metoda o sygnaturze zgodnej z recieve(). Każdy kolejny komunikat będzie obsługiwany przez tą nową metodę. Wywołanie unbecome() przywraca oryginalną metodę. Wykorzystując poznany właśnie mechanizm refaktorujemy powyższy kod:

def receive = {
  case RandomRequest =>
    preFetchIfAlmostEmpty()
    handleOrQueueInBacklog()
}

def receiveWhenWaiting = {
  case RandomRequest =>
    handleOrQueueInBacklog()
  case RandomOrgServerResponse(randomNumbers) =>
    buffer ++= randomNumbers
    context.unbecome()
    while(!backlog.isEmpty && !buffer.isEmpty) {
      backlog.dequeue() ! buffer.dequeue()
    }
    preFetchIfAlmostEmpty()
}

def handleOrQueueInBacklog() {
  if (buffer.isEmpty) {
    backlog += sender
  } else {
    sender ! buffer.dequeue()
  }
}

private def preFetchIfAlmostEmpty() {
  if(buffer.size <= BatchSize / 4) {
    randomOrgClient ! FetchFromRandomOrg(BatchSize)
    context become receiveWhenWaiting
  }
}

Z metody receive wyodrębniliśmy kod obsługi podczas oczekujemy na odpowiedź zewnętrznego serwera do metody receiveWhenWaiting. Zwróćcie uwagę na wywołanie become() i unbecome() - zastąpiły one niepotrzebną już flagę waitingForResponse. Jednak najważniejszą zmianą jest zamiana jednej metody z szeregiem warunków na dwie, znacznie prostsze. Nie tylko ułatwi to testowanie, ale przede wszystkim zwiększy czytelność.

Metody become() i unbecome() są tak naprawdę znacznie potężniejsze, ponieważ wewnętrznie modyfikują stos metod. Każde wywołanie become() umieszcza wskazaną metodę na stosie, którego szczyt jest wykorzystywany jako bieżąca metoda obsługi komunikatów. Wywołanie unbecome() zdejmuje jeden element ze szczytu stosu, przywracając poprzednią metodę (niekoniecznie receive!) Możemy zatem wywołać become() wielokrotnie by później stopniowo wracać do poprzednich metod. Mało tego Akka wspiera również model programowania o automaty skończone, gdzie zdefiniować możemy pełen graf przejść między różnymi metodami obsługi. Ale o tym być może kiedy indziej.

Tagged with →  
Share →
Buffer
Przeczytaj poprzedni wpis:
Poznajemy Akka: dwóch aktorów

Zabawa zabawą, nauka nauką, ale zmierzywszy czasy odpowiedzi stworzonej w poprzednim odcinku klasy RandomOrgRandom zauważymy bardzo niepokojące zjawisko (wykres przedstawia...

Zamknij