角度でのAPIの消費


当初公開blog.florimondmanca.com 2018年9月4日
前のポストでは、私はについて書きました.これらはほとんどサーバ側で適用可能です.
2018年1月以降、フロントエンドの開発も行っていますAngular , 両方の趣味と専門的に.
今日は、アーキテクチャと共有についてお話したいと思います.デザインパターンによって、残りのAPIをアングルフロントエンドアプリケーションと統合する方法を構築し標準化しました.
TLドクター

Write a model class and use an adapter to convert raw JSON data to this internal representation.


ヒント: Githubの最後のコードを見つけることができます.ng-courses .
飛び込みましょう!

問題
我々が正確に解決しようとしていることは何ですか?
最近では多くの場合、フロントエンドのアプリは、外部のバックエンドサービスと多くのやりとりをする必要があります.
我々が今日興味があるこれの典型的な例は、残りAPIを要求することによってデータを検索しています.
これらのAPIは、与えられたデータ形式でデータを返します.この形式は、時間をかけて変更することができます、それはほとんどの可能性が高い形式で使用される角度のアプリで使用する最適な形式です.
それで、解決しようとしている問題は、
変化の影響を制限して、タイプスクリプトの力を完全に利用している間、我々は角度フロントエンドアプリでAPIを統合することができますか?

コース募集制度
一例として、学生がコースにアクセスし、教材にアクセスするコース購読システムを検討する.
ここでは、私たちが構築しようとしている機能についてのユーザーストーリーです.
「学生として、新しいコースに登録できるようにコースのリストを見たいです」
これを可能にするには、以下のAPIエンドポイントを用意しました.
GET: /courses/
応答データ(JSONフォーマット)は次のようになります.
[
    {
        "id": 1,
        "code": "adv-maths",
        "name": "Advanced Mathematics",
        "created": "2018-08-14T12:09:45"
    },
    {
        "id": 2,
        "code": "cs1",
        "name": "Computer Science I",
        "created": "2018-06-12T18:34:16"
    }
]

なぜかJSON.parse() ?
速くて汚い解決はHTTP呼び出しをすることになっていますJSON.parse() を返します.Object 角コンポーネントとHTMLテンプレートで.として、角のHttpClient あなたのためだけにこれを行うことができますので、我々は多くの仕事をする必要はありません.
実際には、これはどのように簡単ですCourseService HTTPリクエストを行うのは次のようになります.
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({
    providedIn: 'root'
})
export class CourseService {

    constructor(private http: HttpClient) {}

    list(): Observable<any> {
        const url = 'http://api.myapp.com/courses/';
        return this.http.get(url);
    }
}
しかしながら、このアプローチにはいくつかの問題があります.

  • バグが起こります:私たちがtypescriptの静的タイピングを利用しないので、初期に捕えられない潜在的なバグの広い範囲があります.

  • 高結合:APIのデータスキーマへのどんな変更も、それが機能的な変化(例えば、フィールドを変えること)に結果としてならないときでも、コード全体を通して泡を吐きます.
  • その結果、サービス自体が維持しやすく、読みやすいが、それは間違いなく私たちのコードベースの残りの部分と同じではありません.

    適応する必要性
    JSONオブジェクトを使用するだけでは十分ではありません.
    さて、上記の2つの問題について考えましょう.
    最初に、データをモデル化するために、私たちがtypescriptの静的タイピングとOOP機能(クラスとインターフェースのような)を使用しなかったので、コードはバグがちでした.これを修正する1つの方法は、特定のクラスのインスタンスを作成することです.
    第2に、APIデータ形式に関して高い結合に遭遇しました.これは、他のアプリケーションコンポーネントからこのデータ形式を抽象化しなかったためです.この問題を解決するために、1つの方法は、API応答を処理するとき、内部データ形式とマップを作成することです.
    これはモデルアダプタパターンを概念化したところです.2つの要素からなる.
  • モデルのクラスは、実際には本当に利点のトンをtypescriptの領域内に私たちを維持します.
  • アダプタは、APIのデータを摂取するための単一のインターフェイスとして動作し、我々は我々のアプリ全体の信頼を使用することができますモデルのインスタンスを構築します.
  • 私は、図が常に概念をつかむのを助けるとわかります.

    モデルアダプタパターン.

    例によるモデルアダプタ
    モデルアダプターパターンがどのようなものかを見てきたので、私たちはコースシステムでそれを実装しますか?モデルから始めましょう.

    The Course モデル
    これは単純なtypescriptクラスになります.
    // app/core/course.model.ts
    export class Course {
      constructor(
        public id: number,
        public code: string,
        public name: string,
        public created: Date,
      ) { }
    }
    
    ノートcreatedDate APIエンドポイントが私たちにそれを与えるにもかかわらずstring ( ISOフォーマットされた日付).アダプタがこの時点で潜んでいるのを既に見ることができます.
    モデルがあるので、実際のHTTPリクエストが作成されるサービスを書き始めましょう.我々は、我々が何を考え出すかについて見ます.

    スタブアウトCourseServiceHTTPの呼び出しをするので、最初にインポートしましょうHttpClientModule 我々の中でAppModule :
      // app/app.module.ts
      import { BrowserModule } from '@angular/platform-browser';
      import { NgModule } from '@angular/core';
    + import { HttpClientModule } from '@angular/common/http';
    
      import { AppComponent } from './app.component';
    
      @NgModule({
        declarations: [
          AppComponent
        ],
        imports: [
          BrowserModule,
    +     HttpClientModule,
        ],
        providers: [],
        bootstrap: [AppComponent]
      })
      export class AppModule { }
    
    我々は今すぐに作成することができますCourseService . 我々のデザインに従って、我々は定義しますlist() から取得したコースのリストを返すことになっているGET: /courses/ エンドポイント.
    // app/core/course.service.ts
    import { Injectable } from '@angular/core';
    import { Course } from './course.model';
    import { Observable, of } from 'rxjs';
    
    @Injectable({
      providedIn: 'root'
    })
    export class CourseService {
    
      list(): Observable<Course[]> {
        // TODO
        return of([]);
      }
    }
    
    私たちが既に述べたように、アングルのHttpClientModule JSON応答の組み込みサポートがあります.確かにHttpClient デフォルトではJSON本体を解析し、JavaScriptオブジェクトを作成します.ではここで試してみましょう.
    // app/core/course.service.ts
    import { Injectable } from '@angular/core';
    import { HttpClient } from '@angular/common/http';
    import { Course } from './course.model';
    import { Observable } from 'rxjs';
    
    @Injectable({
      providedIn: 'root'
    })
    export class CourseService {
      private apiUrl = 'http://api.myapp.com/courses';
    
      constructor(private http: HttpClient) { }
    
      list(): Observable<Course[]> {
       return this.http.get(this.apiUrl);
      }
    }
    
    この時点では、typemcriptのコンパイラは、残念ながら(これは良い点です).

    タイプスクリプトは幸せではない.
    もちろん!我々は建設していないCourse 検索した生データからのインスタンス.代わりに、我々はまだObservable<Object> . 我々は、この使用を修正することができますRxJS 's map 演算子.データ配列を配列にマップしますCourse 対象:
    // app/core/course.service.ts
    import { Injectable } from '@angular/core';
    import { HttpClient } from '@angular/common/http';
    import { Course } from './course.model';
    import { Observable } from 'rxjs';
    import { map } from 'rxjs/operators';
    
    @Injectable({
      providedIn: 'root'
    })
    export class CourseService {
    
      private apiUrl = 'http://api.myapp.com/courses';
    
      constructor(private http: HttpClient) { }
    
      list(): Observable<Course[]> {
        return this.http.get(this.apiUrl).pipe(
          map((data: any[]) => data.map((item: any) => new Course(
            item.id,
            item.code,
            item.name,
            new Date(item.created),
          ))),
        );
      }
    }
    
    フィル!これはうまく動作し、Observable<Course[]> 予想通り.
    しかし、このコードにはいくつかの問題があります.
  • 適応コードがサービスをcluttersするのを読むのは難しいです.
  • それは乾いていません(自分自身を繰り返さないでください):APIアイテムからコースを構築するために必要な他のすべてのメソッドでこのロジックを再実装する必要があります.
  • それは開発者に高い認知負荷を置きますcourse.model.ts 正しい引数を提供するファイルnew Course() .
  • だから、より良い方法がある必要があります.

    入力アダプタ
    もちろん、そこにある!🎉
    これはアダプタが入った時です.あなたが4つのデザインパターンのギャングに精通しているならば、あなたはBridge こちら

    Decouple an abstraction from its implementation so that the two can vary independently.


    これは我々が必要とするものです.
    アダプタは、本質的にAPIのオブジェクトの表現をそれの内部表現に変換します.
    実際には、このようなアダプタのための汎用インターフェイスを定義することもできます.
    // app/core/adapter.ts
    export interface Adapter<T> {
        adapt(item: any): T;
    }
    
    では、ビルドしましょうCourseAdapter . ITSadapt() メソッドは、単一のコース項目(APIによって返される)を取り、Course モデルのインスタンス.この場所はどこですか.私は、モデルファイル自体の中で推薦します:
    // app/core/course.model.ts
    import { Injectable } from '@angular/core';
    import { Adapter } from './adapter';
    
    export class Course {
        // ...
    }
    
    @Injectable({
        providedIn: 'root'
    })
    export class CourseAdapter implements Adapter<Course> {
    
      adapt(item: any): Course {
        return new Course(
          item.id,
          item.code,
          item.name,
          new Date(item.created),
        );
      }
    }
    
    アダプタが注入可能であることに注意してください.これは、アングルの依存性注入システムを使用することができることを意味します.

    リファクタリングCourseService今、我々はロジックのほとんどを抽象化したCourseAdapter , ここでどのようにCourseService 以下のようになります.
    import { Injectable } from '@angular/core';
    import { HttpClient } from '@angular/common/http';
    import { Course, CourseAdapter } from './course.model';
    import { Observable } from 'rxjs';
    import { map } from 'rxjs/operators';
    
    @Injectable({
      providedIn: 'root'
    })
    export class CourseService {
    
      private apiUrl = 'http://api.myapp.com/courses';
    
      constructor(
        private http: HttpClient,
        private adapter: CourseAdapter,
      ) { }
    
      list(): Observable<Course[]> {
        return this.http.get(this.apiUrl).pipe(
          // Adapt each item in the raw data array
          map((data: any[]) => data.map(item => this.adapter.adapt(item))),
        );
      }
    }
    
    注意:
  • コードはよりドライです:我々は、我々が意志で再利用することができる別々の要素(アダプター)で論理を抽象化しました.
  • The CourseService は結果として読みやすいです.

  • モデルとその適応論理が同じファイルに保持されるので,認知負荷は減少する.
  • 私はまた、モデルアダプタのパターンは、APIと我々のアプリの間のカップリングを減らすのに役立つことを約束した.我々が遭遇するならば、起こることを見ましょう...

    データ形式変更!
    APIチームからの人々はAPIのデータ形式に変更をしました.ここではどのようにcourse 次のようになります.
    {
        "id": 1,
        "code": "adv-maths",
        "label": "Advanced Mathematics",
        "created": "2018-08-14T12:09:45"
    }
    
    彼らはname フィールドlabel !
    以前は、どこでも我々のフロントエンドのアプリはname フィールド、我々はそれを変更する必要がありますlabel .
    しかし、我々は今安全です!APIの表現と内部表現を正確にマップするためのアダプターを持っているので、内部の使い方を変更することができますname を取得します.
    @Injectable({
      providedIn: 'root'
    })
    export class CourseAdapter implements Adapter<Course> {
    
      adapt(item: any): Course {
        return new Course(
          item.id,
          item.code,
    -     item.name,
    +     item.label,
          new Date(item.created),
        );
      }
    }
    
    ワンラインチェンジ!と我々のアプリの残りの部分を使用し続けることができますname いつも通り.ブリリアント.✨

    良い原則は常に適用されます
    私は残りのAPIの相互作用を含む私の角度プロジェクトの大部分のモデルアダプタパターンを使用しています.
    それは私が結合を減らして、typescriptの力を完全に利用するのを助けました.
    すべてのすべてで、それは本当にソフトウェア工学の良いolの原則に付着するために沸騰します.ここで重要なのは、単一の責任原理である.あらゆるコード要素は一つのことをしなければならない.
    興味があれば、Githubの最終コードを見つけることができます.ng-courses .
    私はこのシンプルなアーキテクチャのヒントは、角度のアプリでAPIを統合する方法を改善することを願っています.しかし、もしあなたが別のアプローチを自分で成功してきた、私はそれについて聞いてみたい!
    UPDATE(2019年3月1日):この記事のフォローアップ記事を書きました.使い方を説明しますCourseService 角成分でここで作成されます.



    タッチで滞在!
    あなたがこのポストを楽しむならば、あなたは最新版、発表とニュースを缶詰にします.🐤