クリーンアーキテクチャにおけるエンドツーエンド型安全性


きれいなアーキテクチャで完全な型の安全なWebアプリケーションを作成しましょう.そのようなシステムは、型付けされていない対応するものよりも大きな大きさのオーダーである.さらに、彼らは理解しやすく、維持し、リファクタです.Tech : GraphSQL、MongoDB、反応します.
💡 サンプルコードはgithubにあります.https://github.com/thekarel/best-practices-example

エンドツーエンドタイプ安全性


このポストでは、アーキテクチャのすべての階層にわたって型セーフな完全なスタックWebアプリケーションを構築する方法を示します.ドメインモデルからサービス、リポジトリ、HTTPトランスポート層(GraphSQL)、およびクライアント側UIから.これは、CodeBaseのすべてのインターフェイスを入力するだけではありません:この例では、すべての重要なインターフェイスと型は、通常より上位のものから派生していますdomain modules .

There is only a single source of truth for types


これは、高レベルインターフェイスへの変更がスタック全体をカスケードすることを意味します.型チェッカーは、レイヤーのいずれかで不一致を見つけることができます.

利益


実際的な利点はとても重要です.
  • あなたがテストを実行したり、単独でアプリを構築する前に
  • もっと書く必要があるunit tests CodeBaseが相互接続型定義に依存するため、それ以外の場合よりも.
  • CodeBaseは同じインターフェースが繰り返されているので理解しやすくなります
  • すべてが入力されるので、コードはそうですself-documenting
  • あなたがコードを変更するとき-修正、リファクタリングまたは改善-あなたは、IDE内のコードベースの健康についての即座のフィードバックを取得するか、または実行してtsc .
  • 経験は、大きなリファクタリングさえ静的なタイプチェックだけに基づいてそのようなコードベースで首尾よく行われることができることを示します.もちろん、それはEnd-to-end tests .
    すべてのすべてで、私はこのようなスタックは、コードベースの複雑さが限界を超えているため、存在しないバグのいくつかの重要なソースを排除すると思います.我々は、すべてのデータの形、型とインターフェイスを覚えていないです.より少ないバグから離れて、あなたはより高い信頼とより速い開発スループットから利益を得ます.ウィンウィン?

    クリーンアーキテクチャドクター


    The architecture この例のClean Architecture 原理
    これは一言で言えば、
  • アプリケーションは深層から始まる、層にスライスされます:ドメイン(エンティティ)、サービス(ユースケース)、トランスポート(この場合はGraphSQL)、リポジトリ(MongoDBの上の抽象化)、UI(反応、ユーザーに最も近い)
  • 厳密に一方向に依存している矢印があります.スタック内の深い層は、外側の層の任意のコードを参照することはできません
  • 第2の規則は、ドメイン・モジュールが決して他の層で定義される何かをインポートするか、参照することを意味しません.サービスは、依存性注入を通してデータ(例えば)を得て、保存するために「ツール」を受け取ります.リポジトリはドメイン実体を知ることができます(しかし、他の多くではありません).トランスポート層はスマートクッキーであり、ドメイン、サービス、リポジトリについて知っています.UIは、理想的には、GraphSQLの種類、および多分ドメイン実体に制限されます.

    元のクリーンアーキテクチャ図.イメージからhttps://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
    ヘッドオーバーhttps://thekarel.gitbook.io/best-practices/the-big-picture/architecture 詳細な処置のために.

    テックスタック


    リマインダー:完全な例をご利用いただけますhttps://github.com/thekarel/best-practices-example
    使用するテクノロジやライブラリは以下の通りです.
  • TypeScript
  • Apollo Server
  • TypeGraphQL
  • GraphQL Codegen
  • React
  • GraphQL Hooks
  • MongoDB
  • 私は、あなたが比較的すでにこれらのツールの大部分に慣れていると仮定します.私は、おそらくそれほど広く使われていない2つのライブラリに焦点を当て、また、いくつかの重要な解決策を強調します.
    各層を一つずつ見て、このスタックがどのようにハングアップするかを見ましょう.

    レイヤー


    ドメイン


    技術的には、これはスタックの最も簡単なスライスです.実体は純粋なtypescriptインタフェースです.例えば、Order 以下のようになります.
    import {Cupcake} from '../cupcake/Cupcake'
    
    export interface Order {
      id: string
      customerName: string
      deliveryAddress: string
      items: Cupcake[]
    }
    
    対応Cupcake is
    import {Feature} from './Feature'
    import {Kind} from './Kind'
    
    export interface Cupcake {
      kind: Kind
      features: Feature[]
      name: string
    }
    
    重要な事実は、すべての以降の層が何らかの形または形でこれらの定義に戻るということです.

    サービス


    The Service layer , ユースケースとして知られているので、エンティティの操作を定義します.この例では、これらは作成と読み込みを含みますOrder s.
    The domain entities 非常に抽象的ですが、あなたは考えているかもしれません:順序を作成することは具体的な操作で、データベースに話をすることができなければなりません.

    Dependency Arrow Rule: Layers that are deeper in the stack can never refer to any code in outer layers


    解決策はサービス層に依存性インターフェイスを定義することです.例えば、OrderService 定義OrderRepository インターフェイス.この方法では、サービス自体が注文の保存方法について何かを知る必要はありませんが、リポジトリから出てくるデータの形を指し示すことができます.
    import {Order} from '@cupcake/domain'
    
    export interface OrderRepository {
      connect(): Promise<void>
      save(order: Order): Promise<void>
      load(id: string): Promise<Order | undefined>
      all(): Promise<Order[]>
    }
    
    エンドツーエンドタイプの安全性の観点からsave メソッドはドメイン順序を取りますload メソッドは1を返します.これにより、契約を破ることなく異なるストレージメソッドを使用することができます.
    ドメインインターフェイスは、スタック全体で同様の方法で再表示されます.

    倉庫


    上記のようにrepository is a data persistence abstraction . それはより高いレベルのインターフェイス定義を実装しているので、我々は状況に応じて我々のアプリで別のストレージ戦略を使用することができます.以下の2つのリポジトリの実装を比較します.

    OrderRepositoryMemory


    import {OrderRepository} from '@cupcake/services'
    import {Order} from '@cupcake/domain'
    
    export class OrderRepositoryMemory implements OrderRepository {
      private orders: Map<string, Order> = new Map()
    
      async connect() {
        return
      }
    
      async save(order: Order) {
        this.orders.set(order.id, order)
      }
    
      async load(id: string) {
        return this.orders.get(id)
      }
    
      async all() {
        return Array.from(this.orders.values())
      }
    }
    

    OrderRepositorymongo


    import {Order} from '@cupcake/domain'
    import {OrderRepository} from '@cupcake/services'
    import {Collection, MongoClient} from 'mongodb'
    
    export class OrderRepositoryMongo implements OrderRepository {
      client: MongoClient
      dbName = 'cupcakes'
      collectionName = 'cupcakes'
      collection?: Collection<Order>
    
      constructor(private readonly url: string) {
        this.client = new MongoClient(this.url, {useUnifiedTopology: true})
      }
    
      async connect() {
        await this.client.connect()
    
        this.collection = this.client.db(this.dbName).collection<Order>(this.collectionName)
      }
    
      async save(order: Order) {
        if (!this.collection) {
          throw new Error('Connect first')
        }
    
        await this.collection.insert(order)
      }
    
      // etc
    }
    

    Look at the example codebase to see how different repositories are injected


    注意するもう一つの等しく重要な事実は、すべてのタイプ定義がドメインとサービス層から拾われるということです.
    おそらく、タイプセーフティコンテキストで最も重要な機能は、データベースエンティティのドメインエンティティに一致するような形を強制するという事実です.
    this.collection = this.client.db(this.dbName).collection<Order>
    
    これはクリーンアーキテクチャの永続性の主なルールを保証するためです.

    A repository takes a domain entity and returns a domain entity or a list of entities


    データベース層自体の型安全性は重要な事実です:我々のシステム(外部の世界から)に入るデータが予想されるドメイン形状と一致することを保証します.言い換えれば、アプリケーション境界内のすべてが既知の形状であることを確認します.

    グラフ


    CodeBaseの例では、transport layer solution .
    GraphSQL型は、"GraphQL schema language" , 例えば、
    type Customer {
      name: String!
      address: String!
    }
    
    スキーマ言語を使用すると、深刻な欠点があります.GraphSQLのスキーマを使用してドメイン型を参照することはできません.…を見る時間だ.

    Type



    TypeGraphQL クラススクリプトクラスを使用してGraphSQLスキーマを定義できます.使用implements その後、ドメインインタフェースに戻ることができます.例えば、これはCupcake インターフェースはexample Graph :
    import {Cupcake as DomainCupcake, Order as DomainOrder} from '@cupcake/domain'
    import {Field, ID, ObjectType} from 'type-graphql'
    import {Cupcake} from '../cupcake/Cupcake'
    
    @ObjectType()
    export class Order implements DomainOrder {
      @Field(() => ID)
      id!: string
    
      @Field()
      customerName!: string
    
      @Field()
      deliveryAddress!: string
    
      @Field(() => [Cupcake])
      items!: DomainCupcake[]
    }
    
    Generating the final schema これらのクラスからは(コンテナのことを心配しないで、型の安全性とは無関係です)
    import {AwilixContainer} from 'awilix'
    import {buildSchemaSync} from 'type-graphql'
    import {OrderResolver} from './order/OrderResolver'
    
    export const generateSchema = (container: AwilixContainer) =>
      buildSchemaSync({
        resolvers: [OrderResolver],
        container: {
          get: (constructor) => container.build(constructor),
        },
      })
    
    グラフはドメイン型定義をインポートし、それらを強力な保証にしますCupcake サーバーにドメインスキーマ(または要求を拒否)に準拠する必要があります.我々がこれで達成するものは倉庫のためにそれと同じ方法で重要です:外の世界から我々のシステムに入って来るデータは、我々の予想と一致するよう保証されます.

    The data coming into our system from the outside world is guaranteed to match our expectations


    UI


    例のアプリは反応UIを使用します-しかし、任意のUIライブラリが動作します.
    重要な問題は代わりに、どのように我々はグラフやドメインエンティティからUIで使用可能な定義にマップするのですか?
    理想的には、UIはグラフインタフェースについてのみ知っています:これらはクライアントに向けて送られる「もの」です、そして、順番に、これはクライアントが送り返すものです.

    Ideally, the UI only knows about the Graph interfaces


    Graphqlはそれが何であるか、そこに他の、より複雑な質問クエリや突然変異に関する質問-それはすぐに複雑になることができます.手動でUIのコードベースにGraphからこれらのインターフェイスを手動でコピーし、それらを更新しておくことは絶望的です.
    したがって、我々はパズルの最後の部分を見ます.

    グラフ式Codegen



    GraphQL Code Generator is a CLI tool that can generate TypeScript typings out of a GraphQL schema.


    実装は比較的簡単です、そして、それはUIプロジェクトだけに触れます.
    まず、定義configuration file インui/codegen.yml\ :
    schema: http://localhost:8888/
    generates:
      src/graphQLTypes.ts:
        hooks:
          afterOneFileWrite:
            - prettier --write
        plugins:
          - typescript
          - typescript-operations
    config:
      namingConvention:
        enumValues: keep
    
    コマンドを追加するpackage.json :
      "scripts": {
        "typegen": "graphql-codegen"
      }
    
    GraphSQLスキーマが変更されたことを知っている場合easy in a monorepo - を実行するtypegen UIのコマンドで、グラフ型のローカル型定義を生成します.任意の手書きのコードと同じようにコードベースにこれらをコミットします.
    これらの型にアクセスすると、UIコンポーネントがグラフ型を参照できるようになりますmaking a request or creating a payload :
    import {Feature, Kind, MutationCreateOrderArgs, Query} from '../graphQLTypes'
    
    // later
    const [fetchOrders, ordersFetchStatus] = useManualQuery<{orders: Query['orders']}>(ordersQuery)
      React.useEffect(() => {
        fetchOrders()
      }, [])
    
    
      const dumbOrderArgs: MutationCreateOrderArgs = {
        deliveryAddress: 'New York',
        customerName: 'Mr. Muffin',
        items: [
          {kind: Kind.savoury, features: [Feature.sugarFree], name: 'One'},
          {kind: Kind.sweet, features: [Feature.vegan], name: 'Two'},
          {kind: Kind.sweet, features: [Feature.exclusive], name: 'Three'},
        ],
      }
    

    終わり


    どんなコード例と同様に、これはわずかな簡素化です.人生は常に少し異なると間違いなくもっと挑戦的です.私は、インターフェイス(データ形)を進化させる話題に触れませんでした.それでも、これらのアイデアやツールはしっかりとした基礎を築くことができます.
    きれいなアーキテクチャとタイプセーフCodeBaseに頼ると、我々はより良いと私たちの生活をより快適にする製品を作る.

    The more complex you build, the more type safety you need!


    何か見逃した?どうぞ