12.08.2021

Synkronisen koodin yhdistäminen asynkronisiin operaatioihin

Yksi tapa tuoda asynkronisia operaatioita sovellukseesi on Observer-malli.

Käyttämämme Observer on synkroninen toteutus ja saatavilla sendanor/typescript -repositoriosta. Se on puhdasta TypeScriptiä ja käännettynä toimii missä tahansa JavaScript-ympäristössä.

Ensin määrittelemme FooEvent-enumeroinnin, joka sisältää tapahtumien nimet:

export enum FooEvent {
  UPDATED = 'foo:updated',
  ERROR   = 'foo:error'
}

Seuraavaksi määrittelemme rajapinnan FooDTO, joka kuvaa miltä backendista tulevan data transfer objectin pitää näyttää:

export interface FooDTO {
  readonly name    : string;
  readonly summary : string;
}

Tarvitsemme myös ajonaikaisen testin, jotta TypeScriptin käännösaikainen analyysi vastaa ajonaikaista tilannetta, kun DTO saadaan backendista.

Luodaan ajonaikainen testi:

function isFooDTO (value : any) : value is FooDTO {
  return (
    isObject(value) 
    && isString(value?.name) 
    && isString(value?.summary)
  );
}

Voit saada isObject()- ja isString()-funktiot Lodash-kirjastosta tai käyttää TypeScript-optimoituja testifunktioitamme, jotka on kirjoitettu Lodashin päälle ja ovat saatavilla osoitteessa sendanor/typescript/modules/lodash.

Aloitetaan luomalla yksinkertainen FooService-luokka:

export class FooService {

  private _data     : FooDTO | undefined;
  private _observer : Observer<FooEvent>;

  public constructor () {
    this._data     = undefined;
    this._observer = new Observer<FooEvent>("FooService");
  }

  public getData (): FooDTO | undefined {}

  public refreshData () : void {}

  public on (
      name: FooEvent, 
      callback: ObserverCallback<FooEvent>
  ): ObserverDestructor {}
  
}

foo.getData()-metodia voidaan kutsua mistä tahansa, jotta saat viimeisimmän DTO:n backendista.

Se palauttaa undefined, jos dataa ei ole saatu.

public getData (): FooDTO | undefined {
  return this._data;
}

foo.refreshData() kapseloi asynkronisen HTTP-pyynnön ja muuntaa sen Observer-tapahtumamalliksi:

public refreshData () : void {

  HttpService.get('/path/to').then((response) => {
    
    if (!isFooDTO(response)) {
      throw new TypeError('Not FooDTO: ' + JSON.stringify(response));
    }

    this._data = response.data;

    this._observer.triggerEvent(FooEvent.CHANGED);
    
  }).catch(err => {
    this._observer.triggerEvent(FooEvent.ERROR, err);
  });

}

foo.on(FooEvent, callback)-metodia voidaan käyttää tapahtumien kuunteluun foo-instanssien ulkopuolelta:

public on (name: FooEvent, callback: ObserverCallback<FooEvent>): ObserverDestructor {
  return this._observer.listenEvent(name, callback);
}

Paluuarvona on destruktori, jota voi kutsua kuuntelijan irrottamiseksi.

Tässä koko luokka:

export class FooService {

  private _data     : FooDTO | undefined;
  private _observer : Observer<FooEvent>;
  
  public constructor () {
    this._data     = undefined;
    this._observer = new Observer<FooEvent>("FooService");
  }

  public getData (): FooDTO | undefined {
    return this._data;
  }

  public refreshData () : void {

    HttpService.get('/path/to').then((response) => {
      
      if (!isFooDTO(response)) {
        throw new TypeError('Not FooDTO: ' + JSON.stringify(response));
      }

      this._data = response.data;

      this._observer.triggerEvent(FooEvent.CHANGED);
      
    }).catch(err => {
      this._observer.triggerEvent(FooEvent.ERROR, err);
    });

  }
  
  public on (
      name: FooEvent, 
      callback: ObserverCallback<FooEvent>
  ): ObserverDestructor {
    return this._observer.listenEvent(name, callback);
  }

}

Nyt voimme luoda foo-instansseja:

const fooService = new Foo();

Kun instanssi on luotu, voimme alkaa kuunnella kiinnostavia tapahtumia:

const changeListener = fooService.on(FooEvent.CHANGED, () => {

  const currentData = fooService.getData();

  // ...update view

});

Esimerkiksi tässä voit testata, onko data undefined, ja luoda UI:hin latausspinnerin, ja muuten päivittää näkymän.

Virheiden kuunteluun voimme kuunnella FooEvent.ERROR-tapahtumaa:

const errorListener = fooService.on(FooEvent.ERROR, (ev : FooEvent, error : any) => {
  console.error(`Error: `, err);
});

Asynkronisen HTTP-pyynnön käynnistämiseksi datamallin hakemiseksi voit kutsua foo.refreshData():

fooService.refreshData();

Kun tapahtumia ei enää tarvitse kuunnella, kutsu destruktoreita:

changeListener();
errorListener();

Todellisessa sovelluksessa loisit todennäköisesti UI-komponentin tai väliin palveluluokan, joka yhdistää nämä operaatiot uudelleenkäytettäväksi .start()- ja .stop()-rajapinnaksi.

Siinä kaikki. Toivottavasti tästä oli hyötyä!


Kommentit

Jack hello world!

Jaakko Just testing

Jaakko testing again

Jaakko Testing #3

Jaakko Hello world!