<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	
	xmlns:georss="http://www.georss.org/georss"
	xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#"
	>

<channel>
	<title>TypeScript | みんたく</title>
	<atom:link href="https://mintaku-blog.net/category/develop/typescript/feed/" rel="self" type="application/rss+xml" />
	<link>https://mintaku-blog.net</link>
	<description>みんたくの技術ブログ</description>
	<lastBuildDate>Sun, 20 Oct 2024 23:33:17 +0000</lastBuildDate>
	<language>ja</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.0.11</generator>

<image>
	<url>https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2018/06/cropped-ipad-820272_640.jpg?fit=32%2C32&#038;ssl=1</url>
	<title>TypeScript | みんたく</title>
	<link>https://mintaku-blog.net</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">144480658</site>	<item>
		<title>TypeScriptでSupabaseのテーブルからリレーションを使ってデータを取得する</title>
		<link>https://mintaku-blog.net/ts-supabase-relation/</link>
					<comments>https://mintaku-blog.net/ts-supabase-relation/#respond</comments>
		
		<dc:creator><![CDATA[みんたく]]></dc:creator>
		<pubDate>Sat, 07 Sep 2024 09:26:33 +0000</pubDate>
				<category><![CDATA[TypeScript]]></category>
		<guid isPermaLink="false">https://mintaku-blog.net/?p=2532</guid>

					<description><![CDATA[<p>TypeScriptでリレーションを使用してSupabaseのテーブルからデータを取得する方法についてまとめました。 リレーションとは、データベース内の複数 …</p>
The post <a href="https://mintaku-blog.net/ts-supabase-relation/">TypeScriptでSupabaseのテーブルからリレーションを使ってデータを取得する</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></description>
										<content:encoded><![CDATA[<p>TypeScriptでリレーションを使用してSupabaseのテーブルからデータを取得する方法についてまとめました。</p>
<p>リレーションとは、データベース内の複数のテーブル間で関係を構築することを指します。これにより、複数のテーブルから関連データを一度に取得しやすくなります。</p>
<p>この記事では、1対1、1対多、多対多のリレーションを設定し、実際にデータを取得する手順を例を交えて紹介します。</p>
<p>SupabaseはPostgreSQLをベースにしているため、リレーション機能をサポートしています。これにより、データの結合や集約が容易に行え、効率的なデータ操作が可能になります。</p>
<h2>今回使用するテーブル構成</h2>
<p>• users テーブル<br />
• id<br />
• username<br />
• profile_id<br />
• profiles テーブル<br />
• id<br />
• name<br />
• posts テーブル<br />
• id<br />
• user_id<br />
• title<br />
• user_roles テーブル<br />
• user_id<br />
• role_id<br />
• roles テーブル<br />
• id<br />
• role_name</p>
<h2>外部キーを設定する</h2>
<p>Supabaseでリレーションを設定するには、データベーステーブルの定義において適切な外部キー（foreign key）制約を追加する必要があります。</p>
<p>以下のようにusersテーブルにprofile_idカラムを追加し、これをprofilesテーブルのidカラムに関連付けることで、リレーションを貼ることができます。</p><pre class="crayon-plain-tag">CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  username VARCHAR(255)
);


CREATE TABLE posts (
  id SERIAL PRIMARY KEY,
  title VARCHAR(255),
  user_id INTEGER,
  FOREIGN KEY (user_id) REFERENCES users (id)
);</pre><p>こんな感じで、他のテーブルにも外部キーを貼っていきます。</p>
<p>&nbsp;</p>
<h2>実際にリレーションを使ってデータを取得する</h2>
<h3>1対1のリレーション</h3>
<p>users と profiles のリレーションを取得する例です。</p><pre class="crayon-plain-tag">const { data, error } = await supabase
  .from('users')
  .select(`
    id,
    username,
    profiles (
      id,
      name
    )
  `)


if (error) {
  console.error('Error fetching users with profiles:', error)
} else {
  console.log('Users with profiles:', data)
}</pre><p>&nbsp;</p>
<p>レスポンス例</p><pre class="crayon-plain-tag">[
  {
    "id": 1,
    "username": "user1",
    "profiles": {
      "id": 1,
      "name": "Profile 1"
    }
  },
  {
    "id": 2,
    "username": "user2",
    "profiles": {
      "id": 2,
      "name": "Profile 2"
    }
  }
]</pre><p>&nbsp;</p>
<h3>1対多のリレーション</h3>
<p>users と posts のリレーションを取得する例です。</p><pre class="crayon-plain-tag">const { data, error } = await supabase
  .from('users')
  .select(`
    id,
    username,
    posts (
      id,
      title
    )
  `)


if (error) {
  console.error('Error fetching users with posts:', error)
} else {
  console.log('Users with posts:', data)
}</pre><p>レスポンス例</p><pre class="crayon-plain-tag">[
  {
    "id": 1,
    "username": "user1",
    "posts": [
      {
        "id": 1,
        "title": "Post 1"
      },
      {
        "id": 2,
        "title": "Post 2"
      }
    ]
  },
  {
    "id": 2,
    "username": "user2",
    "posts": [
      {
        "id": 3,
        "title": "Post 3"
      }
    ]
  }
]</pre><p>&nbsp;</p>
<h3>多対多のリレーション</h3>
<p>users と roles の多対多のリレーションを user_roles テーブルを通じて取得する例です。</p><pre class="crayon-plain-tag">const { data, error } = await supabase
  .from('users')
  .select(`
    id,
    username,
    user_roles (
      roles (
        id,
        role_name
      )
    )
  `)


if (error) {
  console.error('Error fetching users with roles:', error)
} else {
  console.log('Users with roles:', data)
}</pre><p>レスポンス例</p><pre class="crayon-plain-tag">[
  {
    "id": 1,
    "username": "user1",
    "user_roles": [
      {
        "roles": {
          "id": 1,
          "role_name": "Admin"
        }
      },
      {
        "roles": {
          "id": 2,
          "role_name": "User"
        }
      }
    ]
  },
  {
    "id": 2,
    "username": "user2",
    "user_roles": [
      {
        "roles": {
          "id": 2,
          "role_name": "User"
        }
      }
    ]
  }
]</pre><p></p>
<h3>複数のリレーションの組み合わせ</h3>
<p>複数のリレーションを組み合わせてデータを取得することも可能で、users、profiles、postsを同時に取得する場合は以下のようになります。</p><pre class="crayon-plain-tag">const { data, error } = await supabase
  .from('users')
  .select(`
    id,
    username,
    profiles (
      id,
      name
    ),
    posts (
      id,
      title
    )
  `)


if (error) {
  console.error('Error fetching users with profiles and posts:', error)
} else {
  console.log('Users with profiles and posts:', data)
}</pre><p>レスポンス例</p><pre class="crayon-plain-tag">[
  {
    "id": 1,
    "username": "user1",
    "profiles": {
      "id": 1,
      "name": "Profile 1"
    },
    "posts": [
      {
        "id": 1,
        "title": "Post 1"
      },
      {
        "id": 2,
        "title": "Post 2"
      }
    ]
  },
  {
    "id": 2,
    "username": "user2",
    "profiles": {
      "id": 2,
      "name": "Profile 2"
    },
    "posts": [
      {
        "id": 3,
        "title": "Post 3"
      }
    ]
  }
]</pre><p></p>
<h2>INNER JOINとLEFT JOINで取得する場合</h2>
<p>SupabaseはPostgreSQLを基盤としているため、PostgreSQLでサポートされているJOINの種類を利用できます。</p>
<h3>INNER JOIN</h3>
<p>INNER JOINは、両方のテーブルに共通するデータのみを取得します。</p><pre class="crayon-plain-tag">const { data, error } = await supabase
  .from('users')
  .select(`
    id,
    username,
    profiles!inner (
      id,
      name
    )
  `)


if (error) {
  console.error('Error fetching users with profiles:', error)
} else {
  console.log('Users with profiles:', data)
}</pre><p></p>
<h3>LEFT JOIN</h3>
<p>LEFT JOINは、左側のテーブルのすべてのデータと、右側のテーブルに一致するデータを取得します。一致しない場合、右側のテーブルのデータはNULLになります。</p><pre class="crayon-plain-tag">const { data, error } = await supabase
  .from('users')
  .select(`
    id,
    username,
    profiles!left (
      id,
      name
    )
  `)


if (error) {
  console.error('Error fetching users with profiles:', error)
} else {
  console.log('Users with profiles:', data)
}</pre><p>&nbsp;</p>
<h2>おわりに</h2>
<p>リレーションを利用することで、複数のテーブルにまたがる複雑なデータを効率的に扱うことができます。</p>
<p>Supabaseの柔軟なクエリ機能を活用することで、INNER JOINやLEFT JOINなどの様々なJOINを使ってデータを取得することもできます。</p>
<p>TypeScriptとSupabaseを使ってリレーションを設定し、データを取得する方法について個人的な備忘録としても兼ねて紹介しました。</p>The post <a href="https://mintaku-blog.net/ts-supabase-relation/">TypeScriptでSupabaseのテーブルからリレーションを使ってデータを取得する</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></content:encoded>
					
					<wfw:commentRss>https://mintaku-blog.net/ts-supabase-relation/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2532</post-id>	</item>
		<item>
		<title>ドメインのstaticメソッドをmockする</title>
		<link>https://mintaku-blog.net/mock-static/</link>
					<comments>https://mintaku-blog.net/mock-static/#respond</comments>
		
		<dc:creator><![CDATA[みんたく]]></dc:creator>
		<pubDate>Wed, 03 Jan 2024 03:13:37 +0000</pubDate>
				<category><![CDATA[TDD]]></category>
		<category><![CDATA[TypeScript]]></category>
		<guid isPermaLink="false">https://mintaku-blog.net/?p=2442</guid>

					<description><![CDATA[<p>ドメインのファクトリメソッドを呼び出しているメソッドのテストをするときに、いい感じにmockする方法を学んだのでメモしておきます。 ドメインのstaticメ …</p>
The post <a href="https://mintaku-blog.net/mock-static/">ドメインのstaticメソッドをmockする</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></description>
										<content:encoded><![CDATA[<p>ドメインのファクトリメソッドを呼び出しているメソッドのテストをするときに、いい感じにmockする方法を学んだのでメモしておきます。</p>
<h2>ドメインのstaticメソッドをmockする方法</h2>
<p>Interface &amp; Adapter層のGatewayでエンティティをドメインに変換する際に、ドメインの</p>
<p>Driverからデータを取得して、ドメインに変換するメソッドをテストする場合はこんな感じになります。</p>
<p>・NewsGateway.ts</p><pre class="crayon-plain-tag">async getNews(): Promise&lt;News&gt; {
  const newsEntity = await this.newsDriver.getNews();
  return this.convertNews(newsEntity)
}</pre><p>・NewsGateway.test.ts</p><pre class="crayon-plain-tag"> ...

describe('getNews', () =&gt; {
  it('ニュース一覧を取得することができる', async () =&gt; {
    const newsEntity = mock&lt;NewsEntity[]&gt;()
    const news = mock&lt;News&gt;()
      
    const getNewsMock = jest.fn()
    when(getNewsMock).calledWith().defaultReturnValue(newsEntity)
    newsDriver.getNews = getNewsMock

    const convertNewsMock = jest.fn()
    when(convertNewsMock).calledWith(newsEntity).defaultReturnValue(news)
    newsGateway['convertNews'] = convertNewsMock

    const actual = await newsGateway.getNews()

    expect(actual).toEqual(news)
    expect(newsDriver.getNews).nthCalledWith(1)
    expect(newsGateway['convertNews']).nthCalledWith(1, newsEntity)
  })
})

 ...</pre><p>&nbsp;</p>
<p>convertNewsの単体テストにおいて、Newsドメインにファクトリメソッドであるfromメソッドをmockする方法がわかりました。</p><pre class="crayon-plain-tag">private convertNews(newsEntity: NewsEntity[]): News {
  const news = News.from(newsEntity)

  return news
}</pre><p></p><pre class="crayon-plain-tag">...

describe('convertNews', () =&gt; {
  it('ニュースドメインに変換することができる', async () =&gt; {
    const newsEntity = mock&lt;NewsEntity[]&gt;()
    const news= mock&lt;News&gt;()
    const from = jest.spyOn(News, 'from')
      .mockImplementation((_: NewsEntity[]): News =&gt; {
          return news;
      })

    const actual = newsGateway['convertNews'](newsEntity)

    expect(actual).toEqual(news)
    expect(from).toBeCalled()
  })
})

...
</pre><p>まずspyOnでNewsドメインのfromメソッドをmockします。mockImplementation() 関数の引数は、以下のようになっています。</p>
<ul>
<li>第1引数：モックするメソッドの引数型</li>
<li>第2引数：モックするメソッドの戻り値型</li>
<li>第3引数：モックするメソッドの処理本体</li>
</ul>
<p>この場合、第1引数は NewsEntity[] 型、第2引数はNews型、第3引数はNews型のオブジェクトを返す処理です。つまり、from() メソッドにNewsEntity[]型のエンティティを渡すと、常にnewsを返すように設定しています。</p>
<p>convertNewsはprivateメソッドなので、newsEntityを引数rにnewsGateway[&#8216;convertNews&#8217;]の形で呼び出します。</p>
<p>あとは結果が正しいかと、mockしたfromメソッドが呼び出されたかを確認しています。</p>The post <a href="https://mintaku-blog.net/mock-static/">ドメインのstaticメソッドをmockする</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></content:encoded>
					
					<wfw:commentRss>https://mintaku-blog.net/mock-static/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2442</post-id>	</item>
		<item>
		<title>Googleログイン認証をTypeScriptで実装する</title>
		<link>https://mintaku-blog.net/google-auth-ts/</link>
					<comments>https://mintaku-blog.net/google-auth-ts/#respond</comments>
		
		<dc:creator><![CDATA[みんたく]]></dc:creator>
		<pubDate>Thu, 17 Aug 2023 07:29:54 +0000</pubDate>
				<category><![CDATA[Firebase]]></category>
		<category><![CDATA[TypeScript]]></category>
		<guid isPermaLink="false">https://mintaku-blog.net/?p=2397</guid>

					<description><![CDATA[<p>メモ的な感じでGoogleログイン認証をTypeScriptで実装した内容をまとめます。認証はFirebase Authenticationを使用しています …</p>
The post <a href="https://mintaku-blog.net/google-auth-ts/">Googleログイン認証をTypeScriptで実装する</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></description>
										<content:encoded><![CDATA[<p>メモ的な感じでGoogleログイン認証をTypeScriptで実装した内容をまとめます。認証はFirebase Authenticationを使用しています。</p>
<h2>UseCase層でGoogle認証のロジックを実装する</h2>
<p>流れとしてはFirebase AuthenticationでGoogle認証した後、DBにユーザー情報を問い合わせます。</p>
<p>ユーザー情報がなかったら新規登録としてユーザ登録します。ユーザーログイン認証完了後はmypageに遷移し、認証情報をstateに登録します。</p><pre class="crayon-plain-tag">async signInWithGoogle(auth: Auth) {
  const userWithGoogle: CredentialUser = await this.authGateway.signInWithGoogle(auth)
  const userAccount: UserAccount | null = await this.userGateway.getUserByUid(userWithGoogle.uid)

  if (userAccount) {
    this.toastPresenter.successToast(MESSAGES.AUTH.LOGIN_SUCCESS)
  } else {
    await this.userGateway.createUser(userWithGoogle)
    this.toastPresenter.successToast(MESSAGES.AUTH.REGISTER_SUCCESS)
  }
  await this.storeAuthState(userWithGoogle.uid)
  this.navigationPresenter.navigateTo('/mypage')
} catch(error: Error) {
  console.error('signInWithGoogle error: ', error);
  this.toastPresenter.errorToast(`${MESSAGES.UNEXPECTED_ERROR}: ${error.message}`)
}

private async storeAuthState(uid: string) {
  const authAccount = await this.userGateway.getUserByUid(uid)
  this.authPresenter.storeAuth(authAccount!)
}ち</pre><p>ちなみにUseCaseのUTはこんな感じになりました。あと登録パターンなどのシナリオがいくつかあるといった感じです。</p>
<p>Clean Architecutureをベースにレイヤリングしているので、テストは書きやすかったです。</p><pre class="crayon-plain-tag">it('Google認証でログインすることができる', async () =&gt; {
  const auth = mock&lt;Auth&gt;();
    
  const signInWithGoogleMock = jest.fn()
  when(signInWithGoogleMock).calledWith(auth).mockReturnValueOnce(credentialUser)
  authGateway.signInWithGoogle = signInWithGoogleMock

  const getUserByUidMock = jest.fn()
  when(getUserByUidMock).calledWith(credentialUser.uid).mockReturnValueOnce(userAccount)
  userGateway.getUserByUid = getUserByUidMock

  const successToastMock = jest.fn()
  when(successToastMock).calledWith(MESSAGES.AUTH.LOGIN_SUCCESS)
  toastPresenter.successToast = successToastMock

  const navigateToMock = jest.fn()
  when(navigateToMock).calledWith('/mypage')
  navigationPresenter.navigateTo = navigateToMock

  await authUsecase.signInWithGoogle(auth);

  expect(authGateway.signInWithGoogle).nthCalledWith(1, auth);
  expect(userGateway.getUserByUid).nthCalledWith(1, credentialUser.uid);
  expect(toastPresenter.successToast).nthCalledWith(1, MESSAGES.AUTH.LOGIN_SUCCESS);
});</pre><p>&nbsp;</p>
<h2>GatewayでDomainに変換する</h2>
<p>UseCaseのthis.authGateway.signInWithGoogle(auth)でFirebase AuthenticationにGoogle認証しているところをピックアップして、Gatewayの実装を取り上げます。</p>
<p>Gatewayとしては、DriverでサードパーティであるFirebase Authenticationを呼び出した返り値を、このアプリケーション内のコアとなるDomainに変換してUseCaseに返します。</p>
<p>つまり、Gatewayが外部とアプリケーションの境界部分を担っている感じです。今回は、CredentialUserというDomainに変換して、UseCaseに返しています。</p><pre class="crayon-plain-tag">async signInWithGoogle(auth: Auth): Promise&lt;CredentialUser&gt; {
  const userCredentialWithGoogle = await this.authDriver.signInWithGoogle(auth)
  return {
    uid: userCredentialWithGoogle.user.uid,
    displayName: userCredentialWithGoogle.user.displayName,
    email: userCredentialWithGoogle.user.email,
    photoURL: userCredentialWithGoogle.user.photoURL,
  } as CredentialUser
}</pre><p>&nbsp;</p>
<h2>Driverで実際にGoogle認証する</h2>
<p>実際にGoogle認証するのはこのDriverになります。DriverではGoogleAuthProvider()からsignInWithPopup()を呼び出し、認証した結果を返します。</p>
<p>本来はDriverで使用するエンティティを用意して、その型に詰めてからGatewayに返してあげる方が、アプリケーションとサードパーティとの切り分けができてより良い気もしますが、一旦ここではそのままGatewayに返しています。</p><pre class="crayon-plain-tag">async signInWithGoogle(auth: Auth): Promise&lt;UserCredential&gt; {
  const provider = new GoogleAuthProvider()
  const userCredentialWithGoogle = await signInWithPopup(auth, provider)
  return userCredentialWithGoogle
}</pre><p>すんごいざっくりですが、メモ的な感じでまとめました。</p>The post <a href="https://mintaku-blog.net/google-auth-ts/">Googleログイン認証をTypeScriptで実装する</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></content:encoded>
					
					<wfw:commentRss>https://mintaku-blog.net/google-auth-ts/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2397</post-id>	</item>
		<item>
		<title>Nuxt3に乗り換えてみてどう変わったか簡単にまとめる</title>
		<link>https://mintaku-blog.net/about-nuxt3/</link>
					<comments>https://mintaku-blog.net/about-nuxt3/#respond</comments>
		
		<dc:creator><![CDATA[みんたく]]></dc:creator>
		<pubDate>Sun, 06 Mar 2022 12:46:03 +0000</pubDate>
				<category><![CDATA[Nuxt.js]]></category>
		<category><![CDATA[TypeScript]]></category>
		<guid isPermaLink="false">https://mintaku-blog.net/?p=2162</guid>

					<description><![CDATA[<p>TypeScriptでフロント開発するにあたって、TypeScriptファーストなNuxt3を使いNuxt2からどう変わったのかなどを簡単にまとめてみました …</p>
The post <a href="https://mintaku-blog.net/about-nuxt3/">Nuxt3に乗り換えてみてどう変わったか簡単にまとめる</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></description>
										<content:encoded><![CDATA[<p>TypeScriptでフロント開発するにあたって、TypeScriptファーストなNuxt3を使いNuxt2からどう変わったのかなどを簡単にまとめてみました。</p>
<p>公式ドキュメント：<a href="https://v3.nuxtjs.org/" target="_blank" rel="noopener">https://v3.nuxtjs.org/</a></p>
<h2>Nuxt3でどう変わった</h2>
<p>Nuxt3になったことで、JSX/TSXやSCSSの対応、APIサーバー作成などの機能がデフォルトで含まれています。Nuxt2もセットアップなしで開発を始められるフレームワークでしたが、TypeScriptやSCSSで書く場合は別途追加する必要がありました。</p>
<p>Nuxt3によってそれらが内包され素早いフロント開発に着手できるようになりました。</p>
<p>※ JSXはJavaScripでHTMLっぽいのが書けるようJavaScriptを拡張したものです。TSXはJSXのTypeScript版です。</p>
<h3>サーバー起動とホットリロードが早い</h3>
<p>Nuxt3は開発時のビルドツールにViteが採用されています。そのため開発時のサーバー起動とHMRが早くなりました。</p>
<p>これは実際に使ってみて早さを実感しており、これだけでもNuxt3に乗り換える価値がありそうです。</p>
<h3>Composition APIの採用</h3>
<p>Composition APIは関数ベースでコンポーネントを実装する機能です。関数ベースであるため機能の再利用性を高める役割を持っています。</p>
<p>Vueでの新しい書き方としてプラグインでVue 2にも先行提供されていたComposition APIはNuxt3では標準採用されています。</p>
<p>従来の記法であるOptions APIと比較してTypeScriptの相性がいいことが挙げられます。 Options APIにTypeScript を導入しても thisやdataなどのoptionsの定義が複雑であるため、型定義が難しかったのですが、Composition APIを使用することで型推論でき開発が効率的になりました。</p>
<h3>状態管理としてuseStateとpiniaが追加</h3>
<p>Vue3ではVuexに代わってuseStateとpiniaが推奨されています。</p>
<p>Nuxt 2では主要機能の1つとして組み込まれていたVuexですが、グローバルな状態管理ストアにアプリが依存するリスクやTypeScript、単体テストとの相性の悪さなどがVuexが推奨されなくなった背景とされています。</p>
<p>&nbsp;</p>
<h2>Nuxt3でプロジェクトを作成する</h2>
<p>Nuxt3 からは create-nuxt-app の代わりにNuxt CLIでNuxtプロジェクトを作成します。</p>
<p>npx create-nuxt-appによる質問がなくなったため、すぐにプロジェクトを作成できます。Nuxt3ではTypeScriptはデフォルトになっており、TypeScriptファーストなフレームワークになりました。</p><pre class="crayon-plain-tag">$ npx nuxi init sample-nuxt3-app</pre><p>Nuxt3では生成されるファイルが.tsで作られていることがわかります。</p><pre class="crayon-plain-tag">.gitignore
README.md
app.vue
nuxt.config.ts
package-lock.json
package.json
tsconfig.json</pre><p></p>
<h3>JSX・TSXを使うなら</h3>
<p>JSX・TSXを使う場合は、tsconfig.jsonにcompilerOptionsを追記することが使用することができるようになります。</p><pre class="crayon-plain-tag">{
  // https://v3.nuxtjs.org/concepts/typescript
  "extends": "./.nuxt/tsconfig.json",
  + "compilerOptions": {
  +   "jsx": "preserve"
  + }
}</pre><p></p>The post <a href="https://mintaku-blog.net/about-nuxt3/">Nuxt3に乗り換えてみてどう変わったか簡単にまとめる</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></content:encoded>
					
					<wfw:commentRss>https://mintaku-blog.net/about-nuxt3/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2162</post-id>	</item>
		<item>
		<title>TypeScriptの型とクラスについて学び直す</title>
		<link>https://mintaku-blog.net/ts-class/</link>
					<comments>https://mintaku-blog.net/ts-class/#respond</comments>
		
		<dc:creator><![CDATA[みんたく]]></dc:creator>
		<pubDate>Mon, 07 Feb 2022 13:34:13 +0000</pubDate>
				<category><![CDATA[まとめ系]]></category>
		<category><![CDATA[TypeScript]]></category>
		<guid isPermaLink="false">https://mintaku-blog.net/?p=2146</guid>

					<description><![CDATA[<p>TypeScriptハンズオンとTypeScript Deep Diveを読んでTypeScriptの型とクラスについて学び直しました。 TypeScrip …</p>
The post <a href="https://mintaku-blog.net/ts-class/">TypeScriptの型とクラスについて学び直す</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></description>
										<content:encoded><![CDATA[<p>TypeScriptハンズオンとTypeScript Deep Diveを読んでTypeScriptの型とクラスについて学び直しました。</p>
<h2>TypeScriptの型システム</h2>
<p>型は、コードの品質と読みやすさを高めることができます。</p>
<p>また、型はリファクタリングをする際の開発スピードを高め、型があることによって、コードを書いている時点でエラーに気づくことができます。開発中に早い段階でエラーに気づけるのは大きなメリットです。</p>
<h2>暗黙的な型推論</h2>
<p>型推論とは、TypeScriptがソースコードを解析し、そのコードの流れから変数や関数などの型を推測してくれる仕組みのことです。</p>
<p>TypeScriptは型を推論することによって、不明確なコードに対しエラーを表示してくれます。</p>
<h2>明示的な型指定</h2>
<p>TypeScriptは型を推論しますが、型推論の結果が正しくない場合や正確でない場合は、開発者が、明示的にコード上で、型を指定する(型アノテーションを書く)ことができます。</p>
<p>型アノテーションを書くことによって、開発者にとってのドキュメントにもなります。</p>
<h2>TypeScriptの型の深堀り</h2>
<h3>型エイリアス</h3>
<p>指定した型に別名を設定することができます。エイリアスという名の通り、新しい型というよりかは既にある型に別の名前をつけているといった感じです。</p>
<p><mark>type 新型名 = 型名</mark></p><pre class="crayon-plain-tag">type name = string
type age = number

type person = [name, age]

const taro: person = ['taro', 20]</pre><p></p>
<h3>リテラル型</h3>
<p>リテラルというのは直接ソースコードに書かれる値のことです。以下のようにokだけしか値がない型が作れます。</p>
<p><mark>type ok = &#8216;ok&#8217;</mark></p>
<h3>条件型(Conditional Types)</h3>
<p>複数の型を許容する新しい型エイリアスが作成できます。</p>
<p><mark>type 型名 = 型1 | 型2 | …</mark></p>
<p>リテラル型と条件型を組み合わせることで、複数の値のいずれかを許容するenumのような働きをする型が作れます。</p>
<p><mark>type msg = &#8216;hello&#8217; | &#8216;bye&#8217;</mark></p>
<h3>ユーティリティ型</h3>
<p>変数のさまざまな性質を付加する特殊な型が用意されており、これらをユーティリティ型といいます。以下のようなユーティリティ型があります。</p>
<p><mark>Partial&lt;T&gt;</mark></p>
<ul>
<li>Partial&lt;T&gt;はTの全てのプロパティをOptional(任意)のプロパティにしてくれる</li>
</ul>
<p><mark>Required&lt;T&gt;</mark></p>
<ul>
<li>Required&lt;T&gt;はTの全てのプロパティを必須のプロパティにしてくれる</li>
</ul>
<p><mark>Readonly&lt;T&gt;</mark></p>
<ul>
<li>Readonly&lt;T&gt;はTの全てのプロパティをreadonlyのプロパティにしてくれる</li>
</ul>
<p>他のユーティリティ型などは以下を参照してください。</p>
<p><a href="https://qiita.com/k-penguin-sato/items/e2791d7a57e96f6144e5" target="_blank" rel="noopener">https://qiita.com/k-penguin-sato/items/e2791d7a57e96f6144e5</a></p>
<h3>総称型(ジェネリクス)</h3>
<p>総称型は値の型を特定せずに使用するための仕組みです。</p>
<p>&lt;T&gt;というのが、ここで使われている総称型の指定です。総称型は&lt;&gt;の中に抽象的な型を表す名前を指定します。このTは別にTでなくても良いが、TypeScriptではTから始まるアルファベットを使う習慣があるそうです。</p>
<p>複数の総称型を指定したい場合は&lt;T, U, V…&gt;という具合でカンマ区切りで記述します。</p>
<p><mark>function 関数 &lt;T&gt; (引数) : 戻り値</mark></p>
<p>ジェネリクスでUnitテストで使うmockを作ると以下のように書けそうです。</p><pre class="crayon-plain-tag">function mock&lt;T&gt;(): T {
  return ({} as unknown) as T
}</pre><p>&nbsp;</p>
<h2>TypeScriptのクラスの深堀り</h2>
<h3>インターフェース</h3>
<p>インターフェースはオブジェクト構造を記述するための仕組みです。インターフェースではオブジェクトに用意するプロパティやメソッドを用意できます。</p>
<p>ただあくまでインターフェースは構造の定義をするもので、そのまま使ってオブジェクトを作ることはできません。</p>
<p><mark>interface 名前 {</mark><br />
<mark>  プロパティ: 型</mark><br />
<mark>  メソッド(引数): 型</mark><br />
<mark>  …</mark><br />
<mark>}</mark></p>
<h3>インターフェースの実装</h3>
<p>用意されたインターフェースは、クラスに指定することで利用されます。クラス名の後にimplementsというキーワードを付けます。</p>
<p>このようにimplementsするとそのクラスでは組み込まれたインターフェースに用意されているプロパティやメソッドを全て用意しなければならないです。</p>
<p>つまりインターフェースはクラスにプロパティやメソッドの実装を保証するものです。</p>
<p><mark>class 名前 implementes インターフェース</mark></p>
<p>クリーンアーキテクチャで考えると、Portレイヤーにインターフェースを定義し、Gatewayレイヤーで実装する感じになりそうです。</p><pre class="crayon-plain-tag">interface SamplePort {
  find(): Promise&lt;number&gt;
}

export class SampleGateway implements SamplePort {
  async find(): Promise&lt;number&gt; {
    …
  }

}</pre><p></p>
<h3>インターフェースの継承</h3>
<p>インターフェースを定義するときに「extends インターフェース」と指定することで、既にあるインターフェースを継承した新たなインターフェースを作成できます。</p>
<p>継承して作られたインターフェースはimplementesする際に継承もとのインターフェースに用意されているプロパティやメソッドまで含めて全て実装する必要があります。</p>
<h3>抽象クラス</h3>
<p>インターフェースと似たような働きをするものに抽象クラスがあります。抽象クラスは具体的な処理を持たない抽象的な存在としてのクラスです。</p>
<p>クラス定義の冒頭にabstractを付けることで抽象クラスになります。またメソッドもabstractをつけることで抽象メソッドになります。</p>
<p><mark>abstract class クラス名 {</mark><br />
<mark>  abstract メソッド(): 型</mark><br />
<mark>  …</mark><br />
<mark>}</mark></p>
<h3>抽象クラスとインターフェースの違い</h3>
<p>抽象クラスとインターフェースはかなり似ています。ほぼimplementesするかextendsするかの違いですが、それでも以下のような違いがあります。</p>
<h4>他にクラスを継承する必要があるか</h4>
<p>もし他のクラスを継承するのであれば、抽象クラスは使えません。extendsで継承できるのは1つのクラスのみであるため、抽象クラスと他のクラスを同時に継承できません。</p>
<h4>プロパティを義務付けるか</h4>
<p>抽象クラスは基本的にメソッドを定義するものです。抽象プロパティはないため実装クラスに必ずプロパティを用意させたければインターフェースを使う必要があります。</p>
<h3>静的メンバー</h3>
<p>基本的にプロパティやメソッドはインスタンスを作成して利用するという共通点があります。クラスは基本的にインスタンスを作って利用するものであるため当然とも言えます。</p>
<p>ただ場合によってはインスタンスが必要ないクラスもあります。こうした場合クラスのプロパティやメソッドを静的メンバーとして用意することで、クラスから直接使えるようにできます。</p>
<p>この静的メンバーは「static」というキーワードを使って作成します。</p>
<p><mark>static プロパティ: 型</mark><br />
<mark>static メソッド(引数): 型</mark></p>
<h3>パラメータプロパティ</h3>
<p>読み取りのみのプロパティを扱う場合、パラメータプロパティと呼ばれる機能を使う方法があります。</p>
<p>パラメータプロパティはコンストラクタの引数をそのままプロパティとして扱えるようにする機能です。コンストラクタにreadonlyを指定したプロパティを用意するとそれは「変更不可のプロパティ」として使えるようになります。</p><pre class="crayon-plain-tag">class People {
  constructor(
    readonly name: string
  ) {}
}</pre><p>&nbsp;</p>The post <a href="https://mintaku-blog.net/ts-class/">TypeScriptの型とクラスについて学び直す</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></content:encoded>
					
					<wfw:commentRss>https://mintaku-blog.net/ts-class/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2146</post-id>	</item>
		<item>
		<title>Clean Architectureで実装したAPIにJestテストを導入する</title>
		<link>https://mintaku-blog.net/api-jest/</link>
					<comments>https://mintaku-blog.net/api-jest/#respond</comments>
		
		<dc:creator><![CDATA[みんたく]]></dc:creator>
		<pubDate>Fri, 31 Dec 2021 15:25:36 +0000</pubDate>
				<category><![CDATA[アーキテクチャ]]></category>
		<category><![CDATA[TypeScript]]></category>
		<guid isPermaLink="false">https://mintaku-blog.net/?p=2136</guid>

					<description><![CDATA[<p>こちらの記事は前回の続きです。 https://mintaku-blog.net/ddd-api Jestを導入する 本来ならTDD的にテストを書いてから実 …</p>
The post <a href="https://mintaku-blog.net/api-jest/">Clean Architectureで実装したAPIにJestテストを導入する</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></description>
										<content:encoded><![CDATA[<p>こちらの記事は前回の続きです。</p>

<div class="ys-blog-card">
	<div class="ys-blog-card__container">
					<figure class="ys-blog-card__image">
				<img width="560" height="315" src="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/12/6b2f869ef9af567e5ee01601e028944f.png?fit=560%2C315&amp;ssl=1" class="attachment-large size-large wp-post-image" alt="" loading="lazy" srcset="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/12/6b2f869ef9af567e5ee01601e028944f.png?w=560&amp;ssl=1 560w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/12/6b2f869ef9af567e5ee01601e028944f.png?resize=300%2C169&amp;ssl=1 300w" sizes="(max-width: 560px) 100vw, 560px" data-attachment-id="2130" data-permalink="https://mintaku-blog.net/ddd-api/help-raise-heart-disease-awareness%e3%81%ae%e3%82%b3%e3%83%92%e3%82%9a%e3%83%bc%e3%81%ae%e3%82%b3%e3%83%92%e3%82%9a%e3%83%bc%e3%81%ae%e3%82%b3%e3%83%92%e3%82%9a%e3%83%bc-4-6/" data-orig-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/12/6b2f869ef9af567e5ee01601e028944f.png?fit=560%2C315&amp;ssl=1" data-orig-size="560,315" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="Help raise Heart Disease Awarenessのコピーのコピーのコピー (4)" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/12/6b2f869ef9af567e5ee01601e028944f.png?fit=300%2C169&amp;ssl=1" data-large-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/12/6b2f869ef9af567e5ee01601e028944f.png?fit=560%2C315&amp;ssl=1" />			</figure>
				<div class="ys-blog-card__text">
			<p class="ys-blog-card__title">
				<a class="ys-blog-card__link" href="https://mintaku-blog.net/ddd-api">【Typescript】Clean Architecture + DDDでAPIを実装してみた</a>
			</p>
							<div class="ys-blog-card__dscr">
					Typescript + ​​Clean Architecture + DDDで&hellip;				</div>
										<div class="ys-blog-card__domain">mintaku-blog.net</div>
					</div>
	</div>
</div>

<h2>Jestを導入する</h2>
<p>本来ならTDD的にテストを書いてから実装するのですが、元々の目的がClean Architecture + DDDのAPIをつくるのが目的だったため、後からテストを導入することになりました。</p>
<p>APIはTypescriptで実装されているため、Jestを導入しました。また前回同様、本記事ではメンバー関連を中心に取り扱います。APIしか作ってないのでE2EはなくUnitテストのみです。</p>
<p>&nbsp;</p>
<h2>型のモックの実現</h2>
<p>テストにおいて型のモックは型を抽象化するジェネリックを使って実現します。特に実値で書く必要のない値に関してはモックを使用しています。</p><pre class="crayon-plain-tag">export function mock&lt;T&gt;(): T {
  return ({} as unknown) as T
}</pre><p>&nbsp;</p>
<h2>Domainのテスト</h2>
<p>ドメインのテストを書いていきます。</p>
<p>バリデーションやドメインへの変換処理などをテストしていきます。誕生日から年齢を取得するドメインメソッドのテストは現在日から取得しているため1年経つとテストが落ちるようになってしまうのが悩みどころです。上手い具合にテストできればいいのですが。。</p>
<p>・Member.spec.ts</p><pre class="crayon-plain-tag">import { Member, MemberBirthday, MemberGender, MemberId, MemberName, MemberProfile } from '../Member'
import { MemberRelation, MemberRelationAttribute } from '../MemberRelation'
import { DepartmentId } from '../Department'
import { PostId } from '../Post'
import { EmploymentStatusId } from '../EmploymentStatus'

describe('MemberDomain', () =&gt; {
  describe('MemberName #isMaxSize', () =&gt; {
    test('メンバー名の上限文字数チェックfalse', async () =&gt; {
      const memberName = new MemberName('test')
      expect(memberName.isMaxSize()).toEqual(false)
    })
    test('メンバー名の上限文字数チェックtrue', async () =&gt; {
      const memberName = new MemberName('testtesttesttesttesttesttesttest')
      expect(memberName.isMaxSize()).toEqual(true)
    })
  })

  describe('MemberBirthday #getAge', () =&gt; {
    test('誕生日から年齢を取得', async () =&gt; {
      const memberBirthday = new MemberBirthday(new Date('2000/12/10'))
      expect(memberBirthday.getAge()).toEqual(21)
    })
  })

  describe('MemberGender #getName', () =&gt; {
    test('GenderIdから性別名を取得', async () =&gt; {
      const memberGender = new MemberGender(1)
      expect(memberGender.getName()).toEqual('男性')
    })
  })

  describe('MemberRelationAttribute #update', () =&gt; {
    test('更新用のMemberRelationドメインに変換する', async () =&gt; {
      const memberId = new MemberId(1)
      const memberRelationAttribute = new MemberRelationAttribute(
        new MemberProfile(
          new MemberName('test'),
          new MemberBirthday(new Date('1900/8/29')),
          new MemberGender(1)
        ),
        new DepartmentId(1),
        new PostId(1),
        new EmploymentStatusId(1)
      )
      const expected = new MemberRelation(
        new Member(
          new MemberId(1),
          new MemberProfile(
            new MemberName('test'),
            new MemberBirthday(new Date('1900/8/29')),
            new MemberGender(1)
          )
        ),
        new DepartmentId(1),
        new PostId(1),
        new EmploymentStatusId(1)
      )
      expect(memberRelationAttribute.update(memberId)).toEqual(expected)
    })
  })
  
  describe('MemberRelation #from', () =&gt; {
    test('MemberRelationドメインに変換する', async () =&gt; {
      const id = 1
      const name = 'test'
      const birthday = new Date('2000/12/10')
      const gender = 1
      const departmentId = 1
      const postId = 1
      const employmentStatusId = 1

      const expected = new MemberRelation(
        new Member(
          new MemberId(1),
          new MemberProfile(
            new MemberName('test'),
            new MemberBirthday(new Date('2000/12/10')),
            new MemberGender(1)
          )
        ),
        new DepartmentId(1),
        new PostId(1),
        new EmploymentStatusId(1)
      )
      expect(MemberRelation.from(
        id,
        name,
        birthday,
        gender,
        departmentId,
        postId,
        employmentStatusId
      )).toEqual(expected)
    })
  })
})</pre><p>&nbsp;</p>
<h2>UseCaseのテスト</h2>
<p>次にUseCaseのテストを書いていきます。</p>
<p>MemberPortをモックし、メソッドが呼び出されたら返却されるレスポンスを定義して返してあげるようにします。レスポンスデータはモックできるものはモックし、登録系などバリデーションするものは実値で書いてます。</p>
<p>・MemberUseCase.spec.ts</p><pre class="crayon-plain-tag">import { MemberPort } from '../../port/MemberPort'
import { MemberUseCase } from '../MemberUseCase'
import "reflect-metadata"
import { mock } from './helper'
import { when } from 'jest-when'
import { MemberRelation, MemberRelationAttribute, MemberRelations } from '../../domain/MemberRelation'
import { Member, MemberId, MemberProfile } from '../../domain/Member'
import { MemberName } from '../../domain/Member'
import { MemberBirthday } from '../../domain/Member'
import { MemberGender } from '../../domain/Member'
import { DepartmentId } from '../../domain/Department'
import { PostId } from '../../domain/Post'
import { EmploymentStatusId } from '../../domain/EmploymentStatus'
import { PaymentAmount, PaymentDate, PaymentType, Remark, SalaryAttribute } from '../../domain/Salary'

const memberPort = mock&lt;MemberPort&gt;()

describe('MemberUseCase', () =&gt; {
  describe('find', () =&gt; {
    test('memberIdから1人のメンバーを取得する', async () =&gt; {
      const memberId = mock&lt;MemberId&gt;()
      const expected = mock&lt;MemberRelation&gt;()

      const memberFindFn = jest.fn()
      when(memberFindFn).calledWith(memberId).mockReturnValue(expected)
      memberPort.find = memberFindFn

      const target = new MemberUseCase(memberPort)
      const result = await target.find(memberId)

      expect(memberFindFn).toHaveBeenCalledTimes(1)
      expect(result).toEqual(expected)
    })
  })

  describe('findAll', () =&gt; {
    test('全てのメンバーを取得する', async () =&gt; {
      const expected = mock&lt;MemberRelations&gt;()

      const memberFindAllFn = jest.fn()
      when(memberFindAllFn).mockReturnValue(expected)
      memberPort.findAll = memberFindAllFn

      const target = new MemberUseCase(memberPort)
      const result = await target.findAll()

      expect(memberFindAllFn).toHaveBeenCalledTimes(1)
      expect(result).toEqual(expected)
    })
  })

  describe('create', () =&gt; {
    test('メンバーを新規登録する', async () =&gt; {
      const member = new MemberRelationAttribute(
        new MemberProfile(
          new MemberName('test'),
          new MemberBirthday(new Date()),
          new MemberGender(1)
        ),
        new DepartmentId(1),
        new PostId(1),
        new EmploymentStatusId(1)
      )

      const memberCreateFn = jest.fn()
      memberPort.create = memberCreateFn

      const target = new MemberUseCase(memberPort)

      await target.create(member)

      expect(memberCreateFn).toHaveBeenCalledTimes(1)
    })
  })

  describe('createSalary', () =&gt; {
    test('メンバーの給与情報を登録する', async () =&gt; {
      const memberId = mock&lt;MemberId&gt;()
      const salary = new SalaryAttribute(
        new PaymentAmount(10000),
        new PaymentType('bonus'),
        new Remark('test'),
        new PaymentDate(new Date())
      )

      const memberCreateSalaryFn = jest.fn()
      memberPort.createSalary = memberCreateSalaryFn

      const target = new MemberUseCase(memberPort)
      await target.createSalary(salary, memberId)

      expect(memberCreateSalaryFn).toHaveBeenCalledTimes(1)
    })
  })

  describe('update', () =&gt; {
    test('メンバーを更新する', async () =&gt; {
      const memberId = mock&lt;MemberId&gt;()
      const targetMember = new MemberRelation(
        new Member(
          new MemberId(1),
          new MemberProfile(
            new MemberName('test'),
            new MemberBirthday(new Date()),
            new MemberGender(1)
          )
        ),
        new DepartmentId(1),
        new PostId(1),
        new EmploymentStatusId(1)
      )

      const updateMember = new MemberRelation(
        new Member(
          new MemberId(1),
          new MemberProfile(
            new MemberName('test'),
            new MemberBirthday(new Date()),
            new MemberGender(1)
          )
        ),
        new DepartmentId(1),
        new PostId(1),
        new EmploymentStatusId(1)
      )

      const memberRelationAttribute = new MemberRelationAttribute(
        new MemberProfile(
          new MemberName('test'),
          new MemberBirthday(new Date()),
          new MemberGender(1)
        ),
        new DepartmentId(1),
        new PostId(1),
        new EmploymentStatusId(1)
      )

      const memberUpdateFn = jest.fn()
      memberPort.update = memberUpdateFn

      const memberRelationAttributeUpdateFn = jest.spyOn(memberRelationAttribute, "update")

      when(memberPort.find).calledWith(memberId).mockResolvedValue(targetMember)
      when(memberRelationAttributeUpdateFn).calledWith(targetMember.member.id).mockReturnValue(updateMember)
      when(memberPort.update).calledWith(updateMember)

      const target = new MemberUseCase(memberPort)
      
      await target.update(memberId, memberRelationAttribute)

      expect(memberUpdateFn).toHaveBeenCalledTimes(1)
    })
  })

  describe('delete', () =&gt; {
    test('メンバーを削除する', async () =&gt; {
      const memberId = mock&lt;MemberId&gt;()
      const memberDeleteFn = jest.fn()
      memberPort.delete = memberDeleteFn

      const target = new MemberUseCase(memberPort)
      await target.delete(memberId)

      expect(memberDeleteFn).toHaveBeenCalledTimes(1)
    })
  })
})</pre><p>&nbsp;</p>
<h2>Gateway・Controllerのテスト</h2>
<p>Gateway・Controllerのテストを書いていきます。これらのレイヤら主に変換処理を行うためテストが長くなりがちです。</p>
<p>やっていることはシンプルでGatewayだったらUseCaseから受け取ったドメインをDriver用にプリミティブなオブジェクトに変換して渡してあげ、Driverから受け取ったレスポンスデータをドメインに詰め直す処理をしています。テストとしてはそれらがちゃんと期待しているデータに変換され期待しているメソッドが呼び出されているかをアサートしています。</p>
<p>privateなメソッドに関してはtarget[&#8220;hoge&#8221;](fuga)のようにアクセスしてテストを行っています。</p>
<p>・MemberGateway.spec.ts</p><pre class="crayon-plain-tag">import { mock } from './helper'
import { when } from 'jest-when'
import { MemberDriver, MemberEntity } from '../../driver/MemberDriver'
import { MemberGateway } from '../MemberGateway'
import { Member, MemberBirthday, MemberGender, MemberId, MemberName, MemberProfile } from '../../domain/Member'
import { MemberRelation, MemberRelationAttribute, MemberRelations } from '../../domain/MemberRelation'
import { DepartmentId } from '../../domain/Department'
import { PostId } from '../../domain/Post'
import { EmploymentStatusId } from '../../domain/EmploymentStatus'
import { PaymentAmount, PaymentDate, PaymentType, Remark, SalaryAttribute } from '../../domain/Salary'
const memberDriver = mock&lt;MemberDriver&gt;()

describe('MemberGateway', () =&gt; {
  describe('find', () =&gt; {
    test('memberIdから1人のメンバーをDriverから取得してDomainに変換する', async () =&gt; {
      const memberId = new MemberId(1)
      const memberEntity: MemberEntity = {
        member: {
          id: 1,
          name: 'test',
          birthday: new Date("1900/8/29"),
          gender: 1
        },
        departmentId: 1,
        postId: 1,
        employmentStatusId: 1
      }
      const expected = new MemberRelation(
        new Member(
          new MemberId(1),
          new MemberProfile(
            new MemberName('test'),
            new MemberBirthday(new Date("1900/8/29")),
            new MemberGender(1)
          )
        ),
        new DepartmentId(1),
        new PostId(1),
        new EmploymentStatusId(1)
      )

      const target = new MemberGateway(memberDriver)

      const memberFindFn = jest.fn()
      memberDriver.find = memberFindFn
      const convertMemberValueFn = jest.fn()
      target['convertMemberValue'] = convertMemberValueFn

      when(memberFindFn).calledWith(1).mockResolvedValue(memberEntity)
      when(convertMemberValueFn).calledWith(memberEntity).mockResolvedValue(expected)

      const result = await target.find(memberId)

      expect(memberFindFn).toHaveBeenCalledTimes(1)
      expect(convertMemberValueFn).toHaveBeenCalledTimes(1)
      expect(result).toEqual(expected)
    })
  })

  describe('findAll', () =&gt; {
    test('Driverから全てのメンバーを取得しDomainに変換する', async () =&gt; {
      const memberEntity: MemberEntity[] = [
        {
          member: {
            id: 1,
            name: 'test1',
            birthday: new Date("1900/8/29"),
            gender: 1
          },
          departmentId: 1,
          postId: 1,
          employmentStatusId: 1
        },
        {
          member: {
            id: 2,
            name: 'test2',
            birthday: new Date("1900/8/29"),
            gender: 2
          },
          departmentId: 2,
          postId: 2,
          employmentStatusId: 2
        }
      ]
      const expected = new MemberRelations(
        [
          new MemberRelation(
            new Member(
              new MemberId(1),
              new MemberProfile(
                new MemberName('test1'),
                new MemberBirthday(new Date("1900/8/29")),
                new MemberGender(1)
              )
            ),
            new DepartmentId(1),
            new PostId(1),
            new EmploymentStatusId(1)
          ),
          new MemberRelation(
            new Member(
              new MemberId(2),
              new MemberProfile(
                new MemberName('test2'),
                new MemberBirthday(new Date("1900/8/29")),
                new MemberGender(2)
              )
            ),
            new DepartmentId(2),
            new PostId(2),
            new EmploymentStatusId(2)
          )
        ]
      )

      const target = new MemberGateway(memberDriver)

      const memberFindAllFn = jest.fn()
      memberDriver.findAll = memberFindAllFn
      when(memberFindAllFn).calledWith().mockResolvedValue(memberEntity)

      const result = await target.findAll()

      expect(memberFindAllFn).toHaveBeenCalledTimes(1)
      expect(result).toEqual(expected)
    })
  })

  describe('create', () =&gt; {
    test('メンバーを登録する', async () =&gt; {
      const member = new MemberRelationAttribute(
        new MemberProfile(
          new MemberName('test'),
          new MemberBirthday(new Date("1900/8/29")),
          new MemberGender(1)
        ),
        new DepartmentId(1),
        new PostId(1),
        new EmploymentStatusId(1)
      )
      const target = new MemberGateway(memberDriver)

      const memberCreateFn = jest.fn()
      memberDriver.create = memberCreateFn

      await target.create(member)

      expect(memberCreateFn).toHaveBeenCalledTimes(1)
    })
  })

  describe('createSalary', () =&gt; {
    test('メンバーの給与情報を登録する', async () =&gt; {
      const memberId = new MemberId(1)
      const salary = new SalaryAttribute(
        new PaymentAmount(10000),
        new PaymentType('bonus'),
        new Remark('test'),
        new PaymentDate(new Date())
      )
      const target = new MemberGateway(memberDriver)

      const memberCreateSalaryFn = jest.fn()
      memberDriver.createSalary = memberCreateSalaryFn

      await target.createSalary(salary, memberId)

      expect(memberCreateSalaryFn).toHaveBeenCalledTimes(1)
    })
  })

  describe('update', () =&gt; {
    test('メンバーを更新する', async () =&gt; {
      const member = new MemberRelation(
        new Member(
          new MemberId(1),
          new MemberProfile(
            new MemberName('test1'),
            new MemberBirthday(new Date("1900/8/29")),
            new MemberGender(1)
          )
        ),
        new DepartmentId(1),
        new PostId(1),
        new EmploymentStatusId(1)
      )
      const target = new MemberGateway(memberDriver)

      const memberUpdateFn = jest.fn()
      memberDriver.update = memberUpdateFn

      await target.update(member)

      expect(memberUpdateFn).toHaveBeenCalledTimes(1)
    })
  })

  describe('delete', () =&gt; {
    test('メンバーを削除する', async () =&gt; {
      const memberId = new MemberId(1)
      const target = new MemberGateway(memberDriver)

      const memberDeleteFn = jest.fn()
      memberDriver.delete = memberDeleteFn

      await target.delete(memberId)

      expect(memberDeleteFn).toHaveBeenCalledTimes(1)
    })
  })

  describe('convertMemberValue', () =&gt; {
    test('MemberValueドメインに変換する', async () =&gt; {
      const member: MemberEntity = {
        member: {
          id: 1,
          name: 'test',
          birthday: new Date("1900/8/29"),
          gender: 1
        },
        departmentId: 1,
        postId: 1,
        employmentStatusId: 1
      }

      const expected = new MemberRelation(
        new Member(
          new MemberId(1),
          new MemberProfile(
            new MemberName('test'),
            new MemberBirthday(new Date("1900/8/29")),
            new MemberGender(1)
          )
        ),
        new DepartmentId(1),
        new PostId(1),
        new EmploymentStatusId(1)
      )
      
      const target = new MemberGateway(memberDriver)
      const result = await target["convertMemberValue"](member)

      expect(result).toEqual(expected)
    })
  })
})</pre><p>&nbsp;</p>
<p>・MemberController.spec.ts</p><pre class="crayon-plain-tag">import { mock } from './helper'
import { when } from 'jest-when'
import { Member, MemberBirthday, MemberGender, MemberId, MemberName, MemberProfile } from '../../domain/Member'
import { MemberRelation, MemberRelationAttribute, MemberRelations } from '../../domain/MemberRelation'
import { DepartmentId } from '../../domain/Department'
import { PostId } from '../../domain/Post'
import { EmploymentStatusId } from '../../domain/EmploymentStatus'
import { MemberUseCase } from '../../usecases/MemberUseCase'
import { CreateMemberParams, CreateMemberSalaryParams, MemberController, UpdateMemberParams } from '../MemberController'
import { SalaryUseCase } from '../../usecases/SalaryUseCase'
import { MemberSalary } from '../../domain/MemberSalary'
import { PaymentAmount, PaymentDate, PaymentType, Remark, Salaries, Salary, SalaryAttribute, SalaryId } from '../../domain/Salary'
const memberUseCase = mock&lt;MemberUseCase&gt;()
const salaryUseCase = mock&lt;SalaryUseCase&gt;()

describe('MemberController', () =&gt; {
  describe('findMember', () =&gt; {
    test('memberIdから1人のメンバーを取得する', async () =&gt; {
      const id = 1
      const memberId = new MemberId(id)
      const memberRelation = new MemberRelation(
        new Member(
          new MemberId(1),
          new MemberProfile(
            new MemberName('test1'),
            new MemberBirthday(new Date('1900/8/29')),
            new MemberGender(1)
          )
        ),
        new DepartmentId(1),
        new PostId(1),
        new EmploymentStatusId(1)
      )
      const expected = {
        id: 1,
        name: 'test1',
        birthday: new Date('1900/8/29'),
        age: 121,
        gender: '男性',
        department_id: 1,
        post_id: 1,
        employment_status_id: 1
      }


      const memberFindFn = jest.fn()
      memberUseCase.find = memberFindFn

      const target = new MemberController(memberUseCase, salaryUseCase)
      const serializeMemberForDetailFn = jest.fn()
      target['serializeMemberForDetail'] = serializeMemberForDetailFn

      when(memberFindFn).calledWith(memberId).mockResolvedValue(memberRelation)
      when(serializeMemberForDetailFn).calledWith(memberRelation).mockResolvedValue(expected)

      const result = await target.findMember(id)

      expect(memberFindFn).toHaveBeenCalledTimes(1)
      expect(serializeMemberForDetailFn).toHaveBeenCalledTimes(1)
      expect(result).toEqual(expected)
    })
  })

  describe('findMemberSalaries', () =&gt; {
    test('memberIdから1人のメンバーの給与情報を取得する', async () =&gt; {
      const id = 1
      const memberId = new MemberId(id)
      const memberSalary = new MemberSalary(
        new Member(
          new MemberId(1),
          new MemberProfile(
            new MemberName('test'),
            new MemberBirthday(new Date()),
            new MemberGender(1)
          )
        ),
        new Salaries(
          [
            new Salary(
              new SalaryId(1),
              new SalaryAttribute(
                new PaymentAmount(10000),
                new PaymentType('salary'),
                new Remark('remark1'),
                new PaymentDate(new Date('2001/2/29'))
              )
            ),
            new Salary(
              new SalaryId(2),
              new SalaryAttribute(
                new PaymentAmount(20000),
                new PaymentType('bonus'),
                new Remark('remark2'),
                new PaymentDate(new Date('2001/2/29'))
              )
            )
          ] 
        )
      )

      const expected = {
        member_id: 1,
        member_name: 'test',
        salaries: 
          [
            {
              id: 1,
              payment_amount: 10000,
              payment_type: 'salary',
              remark: 'remark1',
              payment_date: new Date('2001/2/29'),
            },
            {
              id: 2,
              payment_amount: 20000,
              payment_type: 'bonus',
              remark: 'remark2',
              payment_date: new Date('2001/2/29'),
            }
          ]
      }

      const target = new MemberController(memberUseCase, salaryUseCase)
      const memberFindMemberSalariesFn = jest.fn()
      salaryUseCase.findByMemberId = memberFindMemberSalariesFn

      const serializeMemberSalariesFn = jest.fn()
      target['serializeMemberSalaries'] = serializeMemberSalariesFn

      when(memberFindMemberSalariesFn).calledWith(memberId).mockResolvedValue(memberSalary)
      when(serializeMemberSalariesFn).calledWith(memberSalary).mockResolvedValue(expected)

      const result = await target.findMemberSalaries(id)

      expect(memberFindMemberSalariesFn).toHaveBeenCalledTimes(1)
      expect(serializeMemberSalariesFn).toHaveBeenCalledTimes(1)
      expect(result).toEqual(expected)
    })
  })

  describe('findAllMembers', () =&gt; {
    test('メンバー全員を取得する', async () =&gt; {
      const memberRelations = new MemberRelations(
        [
          new MemberRelation(
            new Member(
              new MemberId(1),
              new MemberProfile(
                new MemberName('test1'),
                new MemberBirthday(new Date('1900/8/29')),
                new MemberGender(1)
              )
            ),
            new DepartmentId(1),
            new PostId(1),
            new EmploymentStatusId(1)
          ),
          new MemberRelation(
            new Member(
              new MemberId(2),
              new MemberProfile(
                new MemberName('test2'),
                new MemberBirthday(new Date('1900/8/29')),
                new MemberGender(1)
              )
            ),
            new DepartmentId(2),
            new PostId(2),
            new EmploymentStatusId(2)
          )
        ]
      )
      const expected = [
        {
          name: 'test1',
          department_id: 1,
          post_id: 1,
          employment_status_id: 1
        },
        {
          name: 'test2',
          department_id: 2,
          post_id: 2,
          employment_status_id: 2
        }
      ]

      const target = new MemberController(memberUseCase, salaryUseCase)

      const memberFindAllMembersFn = jest.fn()
      memberUseCase.findAll = memberFindAllMembersFn
      const serializeMemberForListFn = jest.fn()
      target['serializeMemberForList'] = serializeMemberForListFn

      when(memberFindAllMembersFn).calledWith().mockResolvedValue(memberRelations)
      when(serializeMemberForListFn).calledWith(memberRelations).mockResolvedValue(expected)

      const result = await target.findAllMembers()

      expect(memberFindAllMembersFn).toHaveBeenCalledTimes(1)
      expect(serializeMemberForListFn).toHaveBeenCalledTimes(1)
      expect(result).toEqual(expected)
    })
  })

  describe('createMember', () =&gt; {
    test('メンバーを登録する', async () =&gt; {
      const createParams = mock&lt;CreateMemberParams&gt;()
      const createMember = mock&lt;MemberRelationAttribute&gt;()

      const memberCreateFn = jest.fn()
      when(memberCreateFn).calledWith(createMember)
      memberUseCase.create = memberCreateFn

      const target = new MemberController(memberUseCase, salaryUseCase)
      await target.createMember(createParams)

      expect(memberCreateFn).toHaveBeenCalledTimes(1)
    })
  })

  describe('createMemberSalary', () =&gt; {
    test('メンバーの給与を更新する', async () =&gt; {
      const createSalaryParams = mock&lt;CreateMemberSalaryParams&gt;()
      const createSalary = mock&lt;SalaryAttribute&gt;()
      const memberId = mock&lt;MemberId&gt;()

      const createMemberSalaryFn = jest.fn()
      when(createMemberSalaryFn).calledWith(createSalary, memberId)
      memberUseCase.createSalary = createMemberSalaryFn

      const target = new MemberController(memberUseCase, salaryUseCase)
      await target.createMemberSalary(createSalaryParams)

      expect(createMemberSalaryFn).toHaveBeenCalledTimes(1)
    })
  })

  describe('updateMember', () =&gt; {
    test('メンバーを更新する', async () =&gt; {
      const id = mock&lt;number&gt;()
      const memberId = mock&lt;MemberId&gt;()

      const deleteMemberFn = jest.fn()
      when(deleteMemberFn).calledWith(memberId)
      memberUseCase.delete = deleteMemberFn

      const target = new MemberController(memberUseCase, salaryUseCase)
      await target.deleteMember(id)

      expect(deleteMemberFn).toHaveBeenCalledTimes(1)
    })
  })

  describe('deleteMember', () =&gt; {
    test('メンバーを削除する', async () =&gt; {
      const id = mock&lt;number&gt;()
      const updateMemberParams = mock&lt;UpdateMemberParams&gt;()
      const updateMember = mock&lt;MemberRelationAttribute&gt;()
      const memberId = mock&lt;MemberId&gt;()

      const updateMemberFn = jest.fn()
      when(updateMemberFn).calledWith(memberId, updateMember)
      memberUseCase.update = updateMemberFn

      const target = new MemberController(memberUseCase, salaryUseCase)
      await target.updateMember(id, updateMemberParams)

      expect(updateMemberFn).toHaveBeenCalledTimes(1)
    })
  })

  describe('serializeMemberForList', () =&gt; {
    test('メンバー一覧用にドメインをオブジェクトに変換する', async () =&gt; {
      const members = new MemberRelations(
        [
          new MemberRelation(
            new Member(
              new MemberId(1),
              new MemberProfile(
                new MemberName('test1'),
                new MemberBirthday(new Date('1900/8/29')),
                new MemberGender(1)
              )
            ),
            new DepartmentId(1),
            new PostId(1),
            new EmploymentStatusId(1)
          ),
          new MemberRelation(
            new Member(
              new MemberId(2),
              new MemberProfile(
                new MemberName('test2'),
                new MemberBirthday(new Date('1900/8/29')),
                new MemberGender(2)
              )
            ),
            new DepartmentId(1),
            new PostId(1),
            new EmploymentStatusId(1)
          )
        ]
      )

      const expected = [
        {
          name: 'test1',
          department_id: 1,
          post_id: 1,
          employment_status_id: 1
        },
        {
          name: 'test2',
          department_id: 1,
          post_id: 1,
          employment_status_id: 1
        }
      ]

      const target = new MemberController(memberUseCase, salaryUseCase)
      const result = await target["serializeMemberForList"](members)

      expect(result).toEqual(expected)
    })
  })

  describe('serializeMemberForDetail', () =&gt; {
    test('メンバー詳細用にドメインをオブジェクトに変換する', async () =&gt; {
      const member = new MemberRelation(
        new Member(
          new MemberId(1),
          new MemberProfile(
            new MemberName('test1'),
            new MemberBirthday(new Date('1900/8/29')),
            new MemberGender(1)
          )
        ),
        new DepartmentId(1),
        new PostId(1),
        new EmploymentStatusId(1)
      )

      const expected = {
        id: 1,
        name: 'test1',
        birthday: new Date('1900/8/29'),
        age: 121,
        gender: '男性',
        department_id: 1,
        post_id: 1,
        employment_status_id: 1
      }

      const target = new MemberController(memberUseCase, salaryUseCase)
      const result = await target["serializeMemberForDetail"](member)

      expect(result).toEqual(expected)
    })
  })

  describe('serializeMemberSalaries', () =&gt; {
    test('メンバー給与情報用にドメインをオブジェクトに変換する', async () =&gt; {
      const memberSalary = new MemberSalary(
        new Member(
          new MemberId(1),
          new MemberProfile(
            new MemberName('test'),
            new MemberBirthday(new Date()),
            new MemberGender(1)
          )
        ),
        new Salaries(
          [
            new Salary(
              new SalaryId(1),
              new SalaryAttribute(
                new PaymentAmount(10000),
                new PaymentType('salary'),
                new Remark('remark1'),
                new PaymentDate(new Date('2001/2/29'))
              )
            ),
            new Salary(
              new SalaryId(2),
              new SalaryAttribute(
                new PaymentAmount(20000),
                new PaymentType('bonus'),
                new Remark('remark2'),
                new PaymentDate(new Date('2001/2/29'))
              )
            )
          ] 
        )
      )

      const expected = {
        member_id: 1,
        member_name: 'test',
        salaries: [
          {
            id: 1,
            payment_amount: 10000,
            payment_type: 'salary',
            remark: 'remark1',
            payment_date: new Date('2001/2/29')
          },
          {
            id: 2,
            payment_amount: 20000,
            payment_type: 'bonus',
            remark: 'remark2',
            payment_date: new Date('2001/2/29')
          }
        ]
      }

      const target = new MemberController(memberUseCase, salaryUseCase)
      const result = await target["serializeMemberSalaries"](memberSalary)

      expect(result).toEqual(expected)
    })
  })
})</pre><p>&nbsp;</p>
<p>&nbsp;</p>
<h2>テスト実行</h2>
<p>最後にテスト実行していきます。テストが通ると以下のようにpassedにカウントされます。</p><pre class="crayon-plain-tag">$ npm run test

PASS src/domain/tests/Member.spec.ts
PASS src/usecases/tests/MemberUseCase.spec.ts
PASS src/controller/tests/MemberController.spec.ts
PASS src/gateway/tests/MemberGateway.spec.ts

Test Suites: 4 passed, 4 total
Tests: 29 passed, 29 total
Snapshots: 0 total
Time: 2.212 s, es</pre><p></p>The post <a href="https://mintaku-blog.net/api-jest/">Clean Architectureで実装したAPIにJestテストを導入する</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></content:encoded>
					
					<wfw:commentRss>https://mintaku-blog.net/api-jest/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2136</post-id>	</item>
		<item>
		<title>【Typescript】Clean Architecture + DDDでAPIを実装してみた</title>
		<link>https://mintaku-blog.net/ddd-api/</link>
					<comments>https://mintaku-blog.net/ddd-api/#respond</comments>
		
		<dc:creator><![CDATA[みんたく]]></dc:creator>
		<pubDate>Sat, 18 Dec 2021 14:38:16 +0000</pubDate>
				<category><![CDATA[アーキテクチャ]]></category>
		<category><![CDATA[TypeScript]]></category>
		<guid isPermaLink="false">https://mintaku-blog.net/?p=2120</guid>

					<description><![CDATA[<p>Typescript + ​​Clean Architecture + DDDでAPIを実装してみました。 ソフトウェア要件の理解 今回は簡単なメンバー管理 …</p>
The post <a href="https://mintaku-blog.net/ddd-api/">【Typescript】Clean Architecture + DDDでAPIを実装してみた</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></description>
										<content:encoded><![CDATA[<p>Typescript + ​​Clean Architecture + DDDでAPIを実装してみました。</p>
<h2>ソフトウェア要件の理解</h2>
<p>今回は簡単なメンバー管理システムとして作っていきました。まずソフトウェア要件としてどんなものがあるか挙げていきます。</p>
<p>このように要件を挙げていくことで曖昧な要件から明確な仕様へと発展させていきます。本来なら開発するソフトウェアに詳しい人からヒアリングするのですが、今回は個人開発なので自分で挙げていきました。</p>
<ul>
<li>メンバーを管理するシステム</li>
<li>メンバーは1つの部署に所属し、役職が付いている</li>
<li>メンバーの基本情報(名前、性別、生年月日など)を入力して登録する</li>
<li>メンバーの情報は更新や削除ができる</li>
<li>メンバー一覧や詳細を見ることができる</li>
<li>部署一覧と所属しているメンバーの人数を見ることができる</li>
<li>役職一覧と役職ごとのメンバーの人数を見ることができる</li>
<li>メンバーにはそれぞれ雇用形態がある</li>
<li>雇用形態一覧と雇用形態ごとのメンバーの人数を見ることができる</li>
<li>メンバーの給与情報を登録・削除できる</li>
<li>給与情報一覧を見ることができる</li>
</ul>
<p>&nbsp;</p>
<h2>ユースケース図の作成</h2>
<p><img data-attachment-id="2125" data-permalink="https://mintaku-blog.net/ddd-api/image1-16/" data-orig-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/12/image1.png?fit=582%2C1112&amp;ssl=1" data-orig-size="582,1112" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="image1" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/12/image1.png?fit=157%2C300&amp;ssl=1" data-large-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/12/image1.png?fit=536%2C1024&amp;ssl=1" loading="lazy" src="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/12/image1.png?resize=536%2C1024&#038;ssl=1" alt="" width="536" height="1024" class="aligncenter size-large wp-image-2125" srcset="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/12/image1.png?resize=536%2C1024&amp;ssl=1 536w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/12/image1.png?resize=157%2C300&amp;ssl=1 157w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/12/image1.png?w=582&amp;ssl=1 582w" sizes="(max-width: 536px) 100vw, 536px" data-recalc-dims="1" /></p>
<p>ソフトウェア要件を挙げたらユースケース図を作っていきます。このソフトウェアで何ができるのかをユーザー目線で表現します。</p>
<p>今回は管理者のみ使う想定となっており、主にメンバー管理に関するユースケースが想定されます(認証系は省略しています)。</p>
<p>&nbsp;</p>
<h2>エンティティの抽出</h2>
<p><img data-attachment-id="2124" data-permalink="https://mintaku-blog.net/ddd-api/image2-13/" data-orig-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/12/image2.png?fit=1068%2C776&amp;ssl=1" data-orig-size="1068,776" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="image2" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/12/image2.png?fit=300%2C218&amp;ssl=1" data-large-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/12/image2.png?fit=800%2C581&amp;ssl=1" loading="lazy" src="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/12/image2.png?resize=800%2C581&#038;ssl=1" alt="" width="800" height="581" class="aligncenter size-large wp-image-2124" srcset="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/12/image2.png?resize=1024%2C744&amp;ssl=1 1024w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/12/image2.png?resize=300%2C218&amp;ssl=1 300w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/12/image2.png?resize=768%2C558&amp;ssl=1 768w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/12/image2.png?w=1068&amp;ssl=1 1068w" sizes="(max-width: 800px) 100vw, 800px" data-recalc-dims="1" /></p>
<p>ソフトウェア要件やユースケース図が作成できたら、モデリング(エンティティの抽出)していきます。今回は以下のエンティティ候補が抽出されました。またユビキタス言語として用語を統一させておきます。</p>
<p>その後、モデリングの流れとしてエンティティを識別する属性・振る舞いやバリデーションの検討などがありますが、割愛します。</p>
<p>&nbsp;</p>
<h2>Typescript + Clean Architecture + DDDでAPIを開発する</h2>
<p>本記事ではメンバー関連の一連の処理を中心に取り扱います。Clean Architectureの中心部分から説明していきます。</p>
<h3>Enterprise Business Rules</h3>
<p>Enterprise Business Rulesではアプリケーション全体の最重要ビジネスルールをカプセル化したドメインを格納しています。</p>
<p>まず先程定義したMemberエンティティをドメインのエンティティとしてどう定義していく考えていきます。</p>
<p>Memberの情報として必要なデータを値オブジェクトとして定義し、それらをまとめたMemberProfileという値オブジェクトを作成します。このMemberProfileにはMember自身の情報のみ持つようにしています。</p>
<p>次にMemberProfileに一意の識別子であるMemberIdをもっとMemberというエンティティを作成しました。</p>
<p>次にMember自身の情報として持っていないが、役職や部署などMemberに関連づけられたデータを持ったMemberRelationというエンティティとMemberの識別子を持っていないMemberRelationAttributeという値オブジェクトを作成しました。</p>
<p>MemberRalationエンティティのstaticなfromメソッドはプリミティブなオブジェクトからエンティティに変換する際に使われます。</p>
<p>Memberに関連する値オブジェクトとエンティティを作成したらそれらの振る舞いとバリデーションを実装していきます。</p>
<p>・Member.ts</p><pre class="crayon-plain-tag">import { FCC } from './Fcc'

export class MemberProfile {
  constructor(
    readonly name: MemberName,
    readonly birthday: MemberBirthday,
    readonly gender: MemberGender
  ) {}
}

export class Member {
  constructor(
    readonly id: MemberId,
    readonly member: MemberProfile
  ) {}
}

export class MemberId {
  constructor(readonly value: number) {}
}

export class MemberName {
  constructor(readonly value: string) {}

  isMaxSize() {
    return this.value.length &gt; 30
  }
}

export class MemberBirthday {
  constructor(readonly value: Date) {}

  getAge(): number {
    const nowDate = new Date()
    const birthNumber = 
      this.value.getFullYear() * 10000 
      + (this.value.getMonth() + 1 ) * 100 
      + this.value.getDate();
    const nowNumber =
      nowDate.getFullYear() * 10000 
      + (nowDate.getMonth() + 1 ) * 100 
      + nowDate.getDate();
 
    return Math.floor( (nowNumber - birthNumber) / 10000 );
  }
}

export class MemberGender {
  constructor(readonly value: number) {}

  readonly GENDER = {
    MEN: 1,
    WOMAN: 2
  } as const

  getName(): string {
    return this.value == this.GENDER.MEN ? '男性' : '女性'
  }
}

export class Members extends FCC&lt;Member&gt; {
  constructor(values: Member[]) {
    super(values)
  }
}
</pre><p>&nbsp;</p>
<p>・MemberRelation.ts</p><pre class="crayon-plain-tag">import { FCC } from "./Fcc"
import { DepartmentId } from "./Department"
import { MemberProfile, MemberId, MemberBirthday, MemberGender, MemberName, Member } from "./Member"
import { PostId } from "./Post"
import { EmploymentStatusId } from "./EmploymentStatus"

export class MemberRelationAttribute {
  constructor(
    readonly memberProfile: MemberProfile,
    readonly departmentId: DepartmentId,
    readonly postId: PostId,
    readonly employmentStatusId: EmploymentStatusId
  ) {}

  update(id: MemberId): MemberRelation {
    return new MemberRelation(
      new Member(
        new MemberId(id.value),
        new MemberProfile(
          new MemberName(this.memberProfile.name.value),
          new MemberBirthday(this.memberProfile.birthday.value),
          new MemberGender(this.memberProfile.gender.value)
        )
      ),
      new DepartmentId(this.departmentId.value),
      new PostId(this.postId.value),
      new EmploymentStatusId(this.employmentStatusId.value)
    )
  }
}

export class MemberRelation {
  constructor(
    readonly member: Member,
    readonly departmentId: DepartmentId,
    readonly postId: PostId,
    readonly employmentStatusId: EmploymentStatusId
  ) {}

  static from(
    id: number,
    name: string,
    birthday: Date,
    gender: number,
    departmentId: number,
    postId: number,
    employmentStatusId: number
  ) {
    return new MemberRelation(
      new Member(
        new MemberId(id),
        new MemberProfile(
          new MemberName(name),
          new MemberBirthday(birthday),
          new MemberGender(gender)
        )
      ),
      new DepartmentId(departmentId),
      new PostId(postId),
      new EmploymentStatusId(employmentStatusId)
    )
  }
}

export class MemberRelations extends FCC&lt;MemberRelation&gt; {
  constructor(values: MemberRelation[]) {
    super(values)
  }
}</pre><p>&nbsp;</p>
<h3>Application Business Rules</h3>
<p>Application Business Rulesではアプリケーション固有のビジネスルールが含まれ、入出力するデータの流れを調整します。</p>
<p>MemberPortにインターフェイスを定義し、MemberGatewayで具象クラスを実装するようにしています。</p>
<p>MemberUseCaseではMemberControllerから受け取ったドメインをバリデーションなどで精査し、MemberGatewayの具象メソッドを呼び出すようにしています。ちなみにDIライブラリとしてTSyringeを使用しています。</p>
<p>・MemberPort.ts</p><pre class="crayon-plain-tag">import { SalaryAttribute } from '../domain/Salary'
import { MemberId } from '../domain/Member'
import { MemberRelation, MemberRelationAttribute, MemberRelations } from '../domain/MemberRelation'

export interface MemberPort {
  findAll(): Promise&lt;MemberRelations&gt;
  find(id: MemberId): Promise&lt;MemberRelation&gt;
  create(member: MemberRelationAttribute): Promise&lt;void&gt;
  createSalary(salary: SalaryAttribute, memberId: MemberId): Promise&lt;void&gt;
  update(member: MemberRelation): Promise&lt;void&gt;
  delete(id: MemberId): Promise&lt;void&gt;
}</pre><p>&nbsp;</p>
<p>・MemberUseCase.ts</p><pre class="crayon-plain-tag">import { MemberPort } from '../port/MemberPort'
import { MemberId } from '../domain/Member'
import { MemberRelation, MemberRelationAttribute, MemberRelations } from '../domain/MemberRelation'
import "reflect-metadata"
import { inject, singleton } from "tsyringe"
import { SalaryAttribute } from '../domain/Salary'

@singleton()
export class MemberUseCase {
  constructor(@inject("MemberPort") private memberPort: MemberPort) {}

  find(id: MemberId): Promise&lt;MemberRelation&gt; {
    return this.memberPort.find(id)
  }

  findAll(): Promise&lt;MemberRelations&gt; {
    return this.memberPort.findAll()
  }

  create(member: MemberRelationAttribute): Promise&lt;void&gt; {
    if (member.memberProfile.name.isMaxSize()) {
      throw new Error("名前は30文字以下です")
    }
    return this.memberPort.create(member)
  }

  createSalary(salary: SalaryAttribute, memberId: MemberId): Promise&lt;void&gt; {
    if (salary.remark.isMaxSize()) {
      throw new Error("備考は300文字以下です")
    }

    return this.memberPort.createSalary(salary, memberId)
  }

  async update(id: MemberId, member: MemberRelationAttribute): Promise&lt;void&gt; {
    const targetMember = await this.memberPort.find(id)
    const updateMember = member.update(targetMember.member.id)

    if (updateMember.member.member.name.isMaxSize()) {
      throw new Error("名前は30文字以下です")
    }
    return this.memberPort.update(updateMember)
  }

  delete(id: MemberId): Promise&lt;void&gt; {
    return this.memberPort.delete(id)
  }
}</pre><p>&nbsp;</p>
<h3>Interface &amp; Adapter</h3>
<p>Interface &amp; Adapterではドメインやユースケースに便利な形式からDBに便利な形式にデータを変換します。</p>
<p>MemberGatewayではMemberUseCaseから渡されたドメインをプリミティブなJson形式のオブジェクトに詰め替えてMemberDriverを呼び出しています。</p>
<p>なぜならDriver層にはドメインの知識を流出させたくないためであり、あくまでDriverに必要なデータだけをプリミティブな形で渡すようにします。</p>
<p>そしてDriverから返ってきた値をドメインに詰め替えてからUseCaseに返すようにします。</p>
<p>MemberControllerではリクエストされたパラメーターをドメインに詰め替えてUseCaseに渡し、UseCaseから返ってきたドメインをレスポンスとして返したい形式に変換して返します。またレスポンスとして返す時もリスポンスの型を定義し、変換して返すようにしています。</p>
<p>このようにこのレイヤでは主に入出力用に変換する処理を行っています。</p>
<p>・MemberGateway.ts</p><pre class="crayon-plain-tag">import { MemberPort } from '../port/MemberPort'
import { MemberDriver, MemberEntity } from '../driver/MemberDriver'
import { MemberRelation, MemberRelationAttribute, MemberRelations } from '../domain/MemberRelation'
import { MemberId, MemberGender, MemberName, MemberBirthday, MemberProfile, Member } from '../domain/Member'
import { DepartmentId } from '../domain/Department'
import { PostId } from '../domain/Post'
import { inject, singleton } from "tsyringe"
import { EmploymentStatusId } from '../domain/EmploymentStatus'
import { SalaryAttribute } from '../domain/Salary'

@singleton()
export class MemberGateway implements MemberPort {
  constructor(@inject("MemberDriver") private memberDriver: MemberDriver) {}

  async find(id: MemberId): Promise&lt;MemberRelation&gt; {
    const member: MemberEntity = await this.memberDriver.find(id.value)

    return this.convertMemberValue(member)
  }

  async findAll(): Promise&lt;MemberRelations&gt; {
    const members: MemberEntity[] = await this.memberDriver.findAll()

    return new MemberRelations(
      members.map((member: MemberEntity) =&gt; this.convertMemberValue(member))
    )
  }

  async create(member: MemberRelationAttribute): Promise&lt;void&gt; {
    await this.memberDriver.create({
      name: member.memberProfile.name.value,
      birthday: member.memberProfile.birthday.value,
      gender: member.memberProfile.gender.value,
      departmentId: member.departmentId.value,
      postId: member.postId.value,
      employmentStatusId: member.employmentStatusId.value
    })
  }

  async createSalary(salary: SalaryAttribute, memberId: MemberId): Promise&lt;void&gt; {
    await this.memberDriver.createSalary({
      paymentAmount: salary.paymentAmount.value,
      paymentType: salary.paymentType.value,
      remark: salary.remark.value,
      paymentDate: salary.paymentDate.value
    }, memberId.value)
  }

  async update(member: MemberRelation): Promise&lt;void&gt; {
    await this.memberDriver.update({
      id: member.member.id.value,
      name: member.member.member.name.value,
      birthday: member.member.member.birthday.value,
      gender: member.member.member.gender.value,
      departmentId: member.departmentId.value,
      postId: member.postId.value,
      employmentStatusId: member.employmentStatusId.value
    })
  }

  async delete(id: MemberId): Promise&lt;void&gt; {
    await this.memberDriver.delete(id.value)
  }

  private convertMemberValue(member: MemberEntity) {
    const converMember = MemberRelation.from(
      member.member.id,
      member.member.name,
      member.member.birthday,
      member.member.gender,
      member.departmentId,
      member.postId,
      member.employmentStatusId
    )

    return converMember
  }

}</pre><p>&nbsp;</p>
<p>・MemberController.ts</p><pre class="crayon-plain-tag">import { MemberUseCase } from '../usecases/MemberUseCase'
import { SalaryUseCase } from '../usecases/SalaryUseCase'
import { MemberRelation, MemberRelationAttribute, MemberRelations } from '../domain/MemberRelation'
import { MemberBirthday, MemberGender, MemberId, MemberName, MemberProfile } from '../domain/Member'
import { DepartmentId } from '../domain/Department'
import { PostId } from '../domain/Post'
import { inject, singleton } from "tsyringe"
import { EmploymentStatusId } from '../domain/EmploymentStatus'
import { MemberSalary } from '../domain/MemberSalary'
import { PaymentAmount, PaymentDate, PaymentType, Remark, SalaryAttribute } from '../domain/Salary'

@singleton()
export class MemberController {
  constructor(
    @inject("MemberUseCase") private memberUseCase: MemberUseCase,
    @inject("SalaryUseCase") private salaryUseCase: SalaryUseCase
  ) {}

  async findMember(id: number): Promise&lt;MemberForDetailResponse&gt; {
    const memberId = new MemberId(id)
    const member: MemberRelation = await this.memberUseCase.find(memberId)
    
    return this.serializeMemberForDetail(member)
  }

  async findMemberSalaries(id: number): Promise&lt;MemberSalaries&gt; {
    const memberId = new MemberId(id)
    const memberSalary: MemberSalary = await this.salaryUseCase.findByMemberId(memberId)

    return this.serializeMemberSalaries(memberSalary)
  }

  async findAllMembers(): Promise&lt;MemberForListResponse[]&gt; {
    const members: MemberRelations = await this.memberUseCase.findAll()

    return this.serializeMemberForList(members)
  }

  async createMember(createParams: CreateMemberParams): Promise&lt;void&gt; {
    const { name, birthday, gender, department_id, post_id, employment_status_id } = createParams
    const createMember = new MemberRelationAttribute(
      new MemberProfile(
        new MemberName(name),
        new MemberBirthday(birthday),
        new MemberGender(gender)
      ),
      new DepartmentId(department_id),
      new PostId(post_id),
      new EmploymentStatusId(employment_status_id)
    )
    await this.memberUseCase.create(createMember)
  }

  async createMemberSalary(createSalaryParams: CreateMemberSalaryParams): Promise&lt;void&gt; {
    const { member_id, payment_amount, payment_type, remark, payment_date } = createSalaryParams
    const createSalary = 
      new SalaryAttribute(
        new PaymentAmount(payment_amount),
        new PaymentType(payment_type),
        new Remark(remark),
        new PaymentDate(payment_date)
      )
    const memberId = new MemberId(member_id)
    await this.memberUseCase.createSalary(createSalary, memberId)
  }

  async updateMember(id: number, updateMemberParams: UpdateMemberParams): Promise&lt;void&gt; {
    const memberId = new MemberId(id)
    const { name, birthday, gender, department_id, post_id, employment_status_id } = updateMemberParams
    const updateMember = new MemberRelationAttribute(
      new MemberProfile(
        new MemberName(name),
        new MemberBirthday(birthday),
        new MemberGender(gender)
      ),
      new DepartmentId(department_id),
      new PostId(post_id),
      new EmploymentStatusId(employment_status_id)
    )
    await this.memberUseCase.update(memberId, updateMember)
  }

  async deleteMember(id: number): Promise&lt;void&gt; {
    const memberId = new MemberId(id)
    await this.memberUseCase.delete(memberId)
  }

  private serializeMemberForList(members: MemberRelations) {
    return members.map((member: MemberRelation) =&gt; {
      return {
        name: member.member.member.name.value,
        department_id: member.departmentId.value,
        post_id: member.postId.value,
        employment_status_id: member.employmentStatusId.value
      }
    })
  }

  private serializeMemberForDetail(member: MemberRelation) {
    return {
      id: member.member.id.value,
      name: member.member.member.name.value,
      birthday: member.member.member.birthday.value,
      age: member.member.member.birthday.getAge(),
      gender: member.member.member.gender.getName(),
      department_id: member.departmentId.value,
      post_id: member.postId.value,
      employment_status_id: member.employmentStatusId.value
    }
  }

  private serializeMemberSalaries(memberSalary: MemberSalary) {
    return {
      member_id: memberSalary.member.id.value,
      member_name: memberSalary.member.member.name.value,
      salaries: memberSalary.salaries.map(salary =&gt; {
        return {
          id: salary.id.value,
          payment_amount: salary.salaryAttribute.paymentAmount.value,
          payment_type: salary.salaryAttribute.paymentType.value,
          remark: salary.salaryAttribute.remark.value,
          payment_date: salary.salaryAttribute.paymentDate.value,
        }
      })
    }
  }
}

export interface CreateMemberParams {
  name: string,
  birthday: Date,
  gender: number,
  department_id: number,
  post_id: number,
  employment_status_id: number,
}

export interface CreateMemberSalaryParams {
  member_id: number,
  payment_amount: number,
  payment_type: string,
  remark: string,
  payment_date: Date
}

export interface UpdateMemberParams {
  name: string,
  birthday: Date,
  gender: number,
  department_id: number,
  post_id: number,
  employment_status_id: number,
}

export interface MemberForDetailResponse {
  id: number,
  name: string,
  birthday: Date,
  age: number,
  gender: string,
  department_id: number,
  post_id: number,
  employment_status_id: number
}

export interface MemberForListResponse {
  name: string,
  department_id: number,
  post_id: number,
  employment_status_id: number
}

export interface MemberSalaries {
  member_id: number,
  member_name: string,
  salaries: 
    {
      id: number,
      payment_amount: number,
      payment_type: string,
      remark: string,
      payment_date: Date,
    }[]
}</pre><p>&nbsp;</p>
<h3>Framework &amp; Driver</h3>
<p>Framework &amp; Driverはフレームワークやツールで構成されています。</p>
<p>このレイヤにあるMemberDriverはDBから欲しいデータを取得しMemberGatewayに返すようにしています。今回はDBから取得(ORMはSequelizeを使用)していますが、APIならここでAPIリクエストをして返ってきたデータを返します。返すデータはEntityとして詰め直してGatewayに返します。</p>
<p>MemberGatewayでドメインからJson形式に変換したJsonオブジェクトの型とMemberGatewayに返すEntityの型をここで定義しています。</p>
<p>他にもRouterなどありますがここでは割愛しています。</p>
<p>・MemberDriver.ts</p><pre class="crayon-plain-tag">import { singleton } from "tsyringe"
const db = require('../../models')

@singleton()
export class MemberDriver {
  async find(id: number): Promise&lt;MemberEntity&gt; {
    const member = await db.member.findByPk(id, {
      include: [
        { 
          model: db.department,
          as: 'department'
        },
        { 
          model: db.post,
          as: 'post'
        }
      ]
    })
 
    return {
      member: {
        id: member.id,
        name: member.name,
        birthday: member.birthday,
        gender: member.gender,
      },
      departmentId: member.department_id,
      postId: member.post_id,
      employmentStatusId: member.employment_status_id
    }
  }

  async findAll(): Promise&lt;MemberEntity[]&gt; {
    const members = await db.member.findAll({
      include: [
        { 
          model: db.department,
          as: 'department'
        },
        { 
          model: db.post,
          as: 'post'
        }
      ]
    })

    return　members.map((member: MemberModelEntity) =&gt; {
      return {
        member: {
          id: member.id,
          name: member.name,
          birthday: member.birthday,
          gender: member.gender,
        },
        departmentId: member.department_id,
        postId: member.post_id,
        employmentStatusId: member.employment_status_id
      }
    }) 
  }

  async create(member: MemberValueProfileJson): Promise&lt;void&gt; {
    await db.member.create({
      name: member.name,
      birthday: member.birthday,
      gender: member.gender,
      department_id: member.departmentId,
      post_id: member.postId,
      employment_status_id: member.employmentStatusId
    })
  }

  async createSalary(salary: SalaryJson, memberId: number): Promise&lt;void&gt; {
    await db.salary.create({
      member_id: memberId,
      payment_amount: salary.paymentAmount,
      payment_type: salary.paymentType,
      remark: salary.remark,
      payment_date: salary.paymentDate,
    })
  }

  async update(memberJson: MemberJson): Promise&lt;void&gt; {
    const targetMember = await db.member.findByPk(memberJson.id)
    targetMember.name = memberJson.name
    targetMember.birthday = memberJson.birthday
    targetMember.gender = memberJson.gender
    targetMember.department_id = memberJson.departmentId
    targetMember.post_id = memberJson.postId
    targetMember.employment_status_id = memberJson.employmentStatusId

    await targetMember.save()
  }

  async delete(id: number): Promise&lt;void&gt; {
    const targetMember = await db.member.findByPk(id)
    await targetMember.destroy()
  }
}

export interface MemberJson {
    id: number,
    name: string,
    birthday: Date,
    gender: number,
    departmentId: number,
    postId: number,
    employmentStatusId: number
}

export interface MemberValueProfileJson {
  name: string,
  birthday: Date,
  gender: number,
  departmentId: number,
  postId: number,
  employmentStatusId: number
}

export interface SalaryJson {
  paymentAmount: number,
  paymentType: string,
  remark: string | null,
  paymentDate: Date
}

export interface MemberEntity {
  member: {
    id: number,
    name: string,
    birthday: Date,
    gender: number
  },
  departmentId: number,
  postId: number,
  employmentStatusId: number
}

export interface MemberModelEntity {
  id: number,
  name: string,
  birthday: Date,
  gender: number,
  department_id: number,
  post_id: number,
  employment_status_id: number
}</pre><p>&nbsp;</p>The post <a href="https://mintaku-blog.net/ddd-api/">【Typescript】Clean Architecture + DDDでAPIを実装してみた</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></content:encoded>
					
					<wfw:commentRss>https://mintaku-blog.net/ddd-api/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2120</post-id>	</item>
		<item>
		<title>【Typescript】Clean ArchitectureでToDo APIを実装する</title>
		<link>https://mintaku-blog.net/ts-clean-architecture/</link>
					<comments>https://mintaku-blog.net/ts-clean-architecture/#respond</comments>
		
		<dc:creator><![CDATA[みんたく]]></dc:creator>
		<pubDate>Sat, 02 Oct 2021 06:46:26 +0000</pubDate>
				<category><![CDATA[アーキテクチャ]]></category>
		<category><![CDATA[TypeScript]]></category>
		<guid isPermaLink="false">https://mintaku-blog.net/?p=2050</guid>

					<description><![CDATA[<p>Clean Architectureを読んで、実際にTSでToDoアプリのAPIを実装してみました。 クリーンアーキテクチャとは クリーンアーキテクチャはソ …</p>
The post <a href="https://mintaku-blog.net/ts-clean-architecture/">【Typescript】Clean ArchitectureでToDo APIを実装する</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></description>
										<content:encoded><![CDATA[<p>Clean Architectureを読んで、実際にTSでToDoアプリのAPIを実装してみました。</p>
<h2>クリーンアーキテクチャとは</h2>
<p><img data-attachment-id="2056" data-permalink="https://mintaku-blog.net/ts-clean-architecture/image1-12/" data-orig-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/09/image1.jpg?fit=1999%2C1500&amp;ssl=1" data-orig-size="1999,1500" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="image1" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/09/image1.jpg?fit=300%2C225&amp;ssl=1" data-large-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/09/image1.jpg?fit=800%2C600&amp;ssl=1" loading="lazy" src="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/09/image1.jpg?resize=800%2C600&#038;ssl=1" alt="" width="800" height="600" class="aligncenter size-large wp-image-2056" srcset="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/09/image1.jpg?resize=1024%2C768&amp;ssl=1 1024w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/09/image1.jpg?resize=300%2C225&amp;ssl=1 300w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/09/image1.jpg?resize=768%2C576&amp;ssl=1 768w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/09/image1.jpg?resize=1536%2C1153&amp;ssl=1 1536w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/09/image1.jpg?w=1999&amp;ssl=1 1999w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/09/image1.jpg?w=1600&amp;ssl=1 1600w" sizes="(max-width: 800px) 100vw, 800px" data-recalc-dims="1" /></p>
<p>クリーンアーキテクチャはソフトウェアをレイヤーに分割することで、関心事の分離を実現し、以下の特性を持ったシステムを生み出します。</p>
<ul>
<li>フレームワーク非依存：システムをフレームワークの制約で縛るのではなく、フレームワークをツールとして使用する</li>
<li>テスト可能：ビジネスルールはUIやDB、サーバー、その他の外部要素がなくてもテストできる</li>
<li>UI非依存：UIはシステムの他の部分を変更することなく、簡単に変更できる</li>
<li>データベース非依存：ビジネスルールはDBに束縛されていない</li>
<li>外部エージェント非依存：ビジネスルールは外界のインターフェイスについて何も知らない</li>
</ul>
<p>一般的に縁の中央に近づくほどソフトウェアのレベルが上がり、縁の外側は仕組みで内側は方針となっています。</p>
<p>縁の内側は外側について何も知らず、外側で宣言された関数やクラス、変数などは内側にあるコードで触れてはならず、依存性は外から中だけに向かっていきます。</p>
<p>&nbsp;</p>
<h2>Clean ArchitectureでAPIを実装する</h2>
<p>今回はToDoアプリのAPIをClean Architectureで実装しました。</p>
<p>具体的にはTaskテーブルを作成し、そこにToDoのデータが格納され、一覧や詳細、登録などをこのAPIで実現できるようになっています。</p>
<p>中身はシンプルなCRUDで、どの処理をどこに書くかを意識して実装しました。また、ドメイン層ではDDDを意識した作りにしました。といっても今回はドメインがTaskしかないため振る舞いはほどんどなく、簡易的なものになっています。</p>
<p>&nbsp;</p>
<h2>Enterprise Business Rules(企業のビジネスルール)</h2>
<p>このレイヤーにはアプリケーション全体の最重要ビジネスルールをカプセル化したドメインを格納しています。</p>
<p>今回はToDoアプリを想定したAPIとなっており、ビジネスロジックは更新時のみで、データ構造とアクセサメソッドのみ定義しています。</p>
<p>またDDDを意識した実装にし、それぞれのプロパティを値オブジェクトとして定義しています。</p>
<p>コンストラクタの引数が関数内で変更されないため、readonlyプロパティをつけています。こうすることで、引数が読み込み専用で変更されないことが担保されます。</p>
<p>・Domain</p><pre class="crayon-plain-tag">import moment from 'moment'
import { FCC } from './Fcc'

export class Task {
  constructor(
    readonly id: TaskId,
    readonly content: TaskContent,
    readonly status: TaskStatus,
    readonly createdAt: TaskCreatedAt,
    readonly updatedAt: TaskUpdatedAt
  ) {}

  readonly STATUS = {
    TODO: 1,
    DONE: 2,
  } as const;

  updateStatus(status: number) {
    const task = new Task(
      new TaskId(this.id.value),
      new TaskContent(this.content.value),
      new TaskStatus(Number(status)),
      new TaskCreatedAt(this.createdAt.value),
      new TaskUpdatedAt(this.updatedAt.value)
    )

    return task
  }
}

export class TaskId {
  constructor(readonly value: number) {}
}

export class TaskContent {
  constructor(readonly value: string) {}
}

export class TaskStatus {
  constructor(readonly value: number) {}
}

export class TaskCreatedAt {
  constructor(readonly value: moment.Moment) {}
}

export class TaskUpdatedAt {
  constructor(readonly value: moment.Moment) {}
}

export class Tasks extends FCC&lt;Task&gt; {
  constructor(values: Task[]) {
    super(values)
  }
}
</pre><p>&nbsp;</p>
<h2>Application Business Rules(アプリケーションのビジネスルール)</h2>
<p>このレイヤーにはアプリケーション固有のビジネスルールが含まれ、入出力するデータの流れを調整します。</p>
<p>このレイヤーの変更がドメインに影響を与えることはなく、またこのレイヤーがDBやUI、共通のフレームワークなどの外部の変更の影響を受けることもありません。</p>
<p>ここでは依存関係を中心に向かうようにしたいため、依存関係逆転の原則(DIP)を使ってPortにインターフェイスを定義しています。そうすることでUseCaseがPortというインターフェイスに依存することになり、Portのインスタンス化をGatewayで行うようにしています。</p>
<p>具体的にはコンストラクタでTaskGatewayをTaskPortとして受け取ることで、インターフェイスを継承しています。</p>
<p>・UseCase</p><pre class="crayon-plain-tag">import { TaskPort } from '../port/TaskPort'

export class TaskUseCase {
  private taskPort: TaskPort

  constructor(taskPort: TaskPort) {
    this.taskPort = taskPort
  }

  getById(id: number) {
    return this.taskPort.find(id)
  }

  getList() {
    return this.taskPort.findAll()
  }

  create(content: string) {
    return this.taskPort.create(content)
  }

  async update(id: number, status: number) {
    const task = await this.taskPort.find(id)
    const updateTask = task.updateStatus(status)
    return this.taskPort.update(updateTask)
  }

  async delete(id: number) {
    return this.taskPort.delete(id)
  }
}
</pre><p>&nbsp;</p>
<p>・Port</p><pre class="crayon-plain-tag">import { Task, Tasks } from '../domain/Task'

export interface TaskPort {
  findAll(): Promise&lt;Tasks&gt;
  find(id: number): Promise&lt;Task&gt;
  create(content: String): Promise&lt;any&gt;
  update(task: Task): Promise&lt;any&gt;
  delete(id: number): Promise&lt;any&gt;
}
</pre><p>&nbsp;</p>
<h2>Interface Adaptors(インターフェイスアダプター)</h2>
<p>このレイヤーではドメインやユースケースに便利な形式から、永続フレームワーク(DB)に便利な形式にデータを変換します。</p>
<p>円の内側のコードは、DBについて何も知らず、DBがSQLであれば全てのSQLはこのレイヤーに限定する必要があります。</p>
<p>ここではPortの具象クラスであるGatewayとResourceを格納しています。そのため、GatewayではPortのインターフェイスを継承しています。</p>
<p>GatewayではconvertTaskメソッドでSQLから返ってきたEntityをDomainに変換する処理をしています。</p>
<p>ResourceのserializeTaskメソッドでTaskドメインからAPIのレスポンスとして返したい形に変換しています。</p>
<p>&nbsp;</p>
<p>・Gateway</p><pre class="crayon-plain-tag">import { TaskPort } from '../port/TaskPort'
import { TaskDriver } from '../driver/TaskDriver'
import {
  Task,
  TaskContent,
  TaskCreatedAt,
  TaskId,
  Tasks,
  TaskStatus,
  TaskUpdatedAt
} from '../domain/Task'

export class TaskGateway implements TaskPort {
  private taskDriver: TaskDriver

  constructor(taskDriver: TaskDriver) {
    this.taskDriver = taskDriver
  }

  private convertTask(result: any) {
    const task = new Task(
      new TaskId(result.id),
      new TaskContent(result.content),
      new TaskStatus(result.status),
      new TaskCreatedAt(result.created_at),
      new TaskUpdatedAt(result.updated_at)
    )

    return task
  }

  async find(id: number): Promise&lt;Task&gt; {
    const result = await this.taskDriver.find(id)

    return this.convertTask(result[0])
  }

  async findAll(): Promise&lt;Tasks&gt; {
    try {
      const results = await this.taskDriver.findAll()

      return results.map((result: Task) =&gt; {
        return this.convertTask(result)
      })
    } catch (e) {
      throw e
    }
  }

  async create(content: String): Promise&lt;any&gt; {
    try {
      const result = await this.taskDriver.create(content)

      return result
    } catch (e) {
      throw e
    }
  }

  async update(task: Task): Promise&lt;any&gt; {
    try {
      const result = await this.taskDriver.update(task)

      return result
    } catch (e) {
      throw e
    }
  }

  async delete(id: number): Promise&lt;any&gt; {
    try {
      const result = await this.taskDriver.delete(id)

      return result
    } catch (e) {
      throw e
    }
  }
}
</pre><p>&nbsp;</p>
<p>・Resource</p><pre class="crayon-plain-tag">import { TaskUseCase } from '../usecases/TaskUseCase'
import { Task } from '../domain/Task'

export class TaskResource {
  private taskUseCase: TaskUseCase

  constructor(taskUseCase: TaskUseCase) {
    this.taskUseCase = taskUseCase
  }

  async findTask(req: any, res: any) {
    const id = req.params.id
    const result = await this.taskUseCase.getById(id)

    return this.serialize(result)
  }

  async findAllTasks(req: any, res: any) {
    const result = await this.taskUseCase.getList()

    return this.serialize(result)
  }

  async createTask(req: any, res: any) {
    const { content } = req.body
    const result = await this.taskUseCase.create(content)
    return result
  }

  async updateTask(req: any, res: any) {
    const id = req.params.id
    const { status } = req.body
    const result = await this.taskUseCase.update(id, status)
    return result
  }

  async deleteTask(req: any, res: any) {
    const id = req.params.id
    const result = await this.taskUseCase.delete(id)
    return result
  }

  private serializeTask = (task: Task) =&gt; {
    return {
      id: task.id.value,
      content: task.content.value,
      status: task.status.value,
      createdAt: task.createdAt.value,
      updatedAt: task.updatedAt.value
    }
  }

  private serialize(data: any) {
    if (!data) {
      throw new Error('data is undefined or null')
    }
    if (Array.isArray(data)) {
      return data.map(task =&gt; this.serializeTask(task))
    }
    return this.serializeTask(data)
  }
}
</pre><p></p>
<h2>Frameworks &amp; Drivers(フレームワークとドライバ)</h2>
<p>最も外側の円は、フレームワークやツールで構成されています。例えばDBやWebフレームワークなどがこのレイヤーに格納されています。RouterやDriverもこのレイヤーに格納しています。</p>
<p>Tasksテーブルのカラムであるcreated_atとupdated_atはdefaultValueを「CURRENT_TIMESTAMP」、「CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP」にして自動的に登録されるようにしています。</p>
<p>statusもdefaultValueを1にすることでcreateではデフォルトで登録できるようにしています。</p>
<p>・Driver</p><pre class="crayon-plain-tag">import { Task } from '../domain/Task'
import { IDBConnection } from './db/IDBConnection'

export class TaskDriver {
  private connection: any

  constructor(connection: IDBConnection) {
    this.connection = connection
  }

  async find(id: number): Promise&lt;any&gt; {
    const result = await this.connection.execute(
      'select * from tasks where id = ?',
      id
    )

    return result
  }

  async findAll(): Promise&lt;any&gt; {
    const result = await this.connection.execute('select * from tasks')
    return result
  }

  async create(content: String): Promise&lt;any&gt; {
    const result = await this.connection.execute(
      'insert into tasks (content) values (?)',
      content
    )
    return result
  }

  async update(task: Task): Promise&lt;any&gt; {
    const result = this.connection.execute(
      'update tasks set status = ? where id = ?',
      [task.status.value, task.id.value]
    )
    return result
  }

  async delete(id: number): Promise&lt;any&gt; {
    const result = await this.connection.execute(
      'delete from tasks where id = ?',
      id
    )
    return result
  }
}
</pre><p>&nbsp;</p>
<p>Routerではexpressのrouterを使ってルーティング設定をしています。</p>
<p>またRouterで各層の依存関係を定義(DI)することで、それぞれレイヤーで利用可能な状態にします。</p>
<p>・Router</p><pre class="crayon-plain-tag">import express = require('express')
import { TaskUseCase } from '../usecases/TaskUseCase'
import { TaskGateway } from '../gateway/TaskGateway'
import { TaskResource } from '../rest/TaskResource'
import { MysqlConnection } from './db/MysqlConnection'
import { TaskDriver } from './TaskDriver'

const mysqlConnection = new MysqlConnection()
const taskDriver = new TaskDriver(mysqlConnection)
const taskGateway = new TaskGateway(taskDriver)
const taskUsecase = new TaskUseCase(taskGateway)
const taskResource = new TaskResource(taskUsecase)

const router = express.Router()

router.get('/tasks', async (req: express.Request, res: express.Response) =&gt; {
  const results = await taskResource.findAllTasks(req, res)
  res.send(results)
})

router.get(
  '/tasks/:id',
  async (req: express.Request, res: express.Response) =&gt; {
    const result = await taskResource.findTask(req, res)

    res.send(result)
  }
)

router.post('/tasks', async (req: express.Request, res: express.Response) =&gt; {
  const result = await taskResource.createTask(req, res)
  res.send(result)
})

router.patch(
  '/tasks/:id',
  async (req: express.Request, res: express.Response) =&gt; {
    const result = await taskResource.updateTask(req, res)
    res.send(result)
  }
)

router.delete(
  '/tasks/:id',
  async (req: express.Request, res: express.Response) =&gt; {
    const result = await taskResource.deleteTask(req, res)
    res.send(result)
  }
)

export default router
</pre><p>&nbsp;</p>
<p>Serverではexpressを使ってサーバー処理を書いています。routerのパスや待ち受けるポート番号などを設定しています。</p>
<p>また、CORSで「http://localhost:3000」のみアクセスできるようにしています。</p>
<p>・Server</p><pre class="crayon-plain-tag">import express from 'express'
import router from './router'
import bodyParser from 'body-parser'
import cors from 'cors'

const app = express()

app.use(bodyParser.urlencoded({ extended: false }))
app.use(bodyParser.json())

const allowedOrigins = ['http://localhost:3000']

const options: cors.CorsOptions = {
  origin: allowedOrigins
}
app.use(cors(options))
app.use('/api', router)

app.listen(5000, () =&gt; {
  console.log('listening on port 5000')
})

export default app
</pre><p>&nbsp;</p>
<p>・IDBConnection</p><pre class="crayon-plain-tag">export abstract class IDBConnection {
  abstract execute(query: string, params: any): any
}</pre><p>&nbsp;</p>
<p>・MysqlConnection</p><pre class="crayon-plain-tag">import mysql from 'mysql'
import dotenv from 'dotenv'
import util from 'util'
import { IDBConnection } from './IDBConnection'

export class MysqlConnection extends IDBConnection {
  private pool: any

  constructor() {
    super()
    dotenv.config()
    this.pool = mysql.createPool({
      connectionLimit: 5,
      host: process.env.DB_HOST_DEV,
      user: process.env.DB_USER_DEV,
      password: process.env.DB_PASSWORD_DEV,
      database: process.env.DB_NAME_DEV,
      timezone: 'utc'
    })

    this.pool.getConnection((error: any, connection: any) =&gt; {
      if (error) {
        if (error.code === 'PROTOCOL_CONNECTION_LOST') {
          console.error('Database connection was closed.')
        }
        if (error.code === 'ER_CON_COUNT_ERROR') {
          console.error('Database has too many connections.')
        }
        if (error.code === 'ECONNREFUSED') {
          console.error('Database connection was refused.')
        }
      }

      if (connection) connection.release()

      return
    })
    this.pool.query = util.promisify(this.pool.query)

    // pool event
    this.pool.on('connection', (connection: any) =&gt; {
      console.log('mysql connection create')
    })

    this.pool.on('release', (connection: any) =&gt; {
      console.log('Connection %d released', connection.threadId)
    })
  }

  execute(query: string, params: any = null) {
    if (params !== null) {
      return this.pool.query(query, params)
    } else {
      return this.pool.query(query)
    }
  }
}
</pre><p>&nbsp;</p>The post <a href="https://mintaku-blog.net/ts-clean-architecture/">【Typescript】Clean ArchitectureでToDo APIを実装する</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></content:encoded>
					
					<wfw:commentRss>https://mintaku-blog.net/ts-clean-architecture/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2050</post-id>	</item>
	</channel>
</rss>
