クリーンアーキテクチャにおけるエンドツーエンド型安全性
💡 サンプルコードは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
これは、高レベルインターフェイスへの変更がスタック全体をカスケードすることを意味します.型チェッカーは、レイヤーのいずれかで不一致を見つけることができます.
利益
実際的な利点はとても重要です.
There is only a single source of truth for types
実際的な利点はとても重要です.
tsc
. すべてのすべてで、私はこのようなスタックは、コードベースの複雑さが限界を超えているため、存在しないバグのいくつかの重要なソースを排除すると思います.我々は、すべてのデータの形、型とインターフェイスを覚えていないです.より少ないバグから離れて、あなたはより高い信頼とより速い開発スループットから利益を得ます.ウィンウィン?
クリーンアーキテクチャドクター
The architecture この例のClean Architecture 原理
これは一言で言えば、
元のクリーンアーキテクチャ図.イメージから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インタフェースです.例えば、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!
何か見逃した?どうぞ
Reference
この問題について(クリーンアーキテクチャにおけるエンドツーエンド型安全性), 我々は、より多くの情報をここで見つけました
https://dev.to/thekarel/end-to-end-type-safety-in-clean-architecture-48la
テキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol
import {Cupcake} from '../cupcake/Cupcake'
export interface Order {
id: string
customerName: string
deliveryAddress: string
items: Cupcake[]
}
import {Feature} from './Feature'
import {Kind} from './Kind'
export interface Cupcake {
kind: Kind
features: Feature[]
name: string
}
Dependency Arrow Rule: Layers that are deeper in the stack can never refer to any code in outer layers
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[]>
}
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())
}
}
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
type Customer {
name: String!
address: String!
}
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[]
}
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),
},
})
The data coming into our system from the outside world is guaranteed to match our expectations
Ideally, the UI only knows about the Graph interfaces
GraphQL Code Generator is a CLI tool that can generate TypeScript typings out of a GraphQL schema.
schema: http://localhost:8888/
generates:
src/graphQLTypes.ts:
hooks:
afterOneFileWrite:
- prettier --write
plugins:
- typescript
- typescript-operations
config:
namingConvention:
enumValues: keep
"scripts": {
"typegen": "graphql-codegen"
}
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!
何か見逃した?どうぞ
Reference
この問題について(クリーンアーキテクチャにおけるエンドツーエンド型安全性), 我々は、より多くの情報をここで見つけました https://dev.to/thekarel/end-to-end-type-safety-in-clean-architecture-48laテキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol