<?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>アーキテクチャ | みんたく</title>
	<atom:link href="https://mintaku-blog.net/category/develop/architecture/feed/" rel="self" type="application/rss+xml" />
	<link>https://mintaku-blog.net</link>
	<description>みんたくの技術ブログ</description>
	<lastBuildDate>Wed, 03 Apr 2024 19:45:27 +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>アーキテクチャ | みんたく</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>ユーザー情報更新のデータフローの理想状態を考える</title>
		<link>https://mintaku-blog.net/ideal-user-flow/</link>
					<comments>https://mintaku-blog.net/ideal-user-flow/#respond</comments>
		
		<dc:creator><![CDATA[みんたく]]></dc:creator>
		<pubDate>Thu, 09 Nov 2023 14:07:30 +0000</pubDate>
				<category><![CDATA[アーキテクチャ]]></category>
		<guid isPermaLink="false">https://mintaku-blog.net/?p=2419</guid>

					<description><![CDATA[<p>個人開発のユーザー情報更新の処理を見ていて、もっと良くできるのではないかと思い、Clean ArchitectureとDDDをベースに理想状態を考えてみまし …</p>
The post <a href="https://mintaku-blog.net/ideal-user-flow/">ユーザー情報更新のデータフローの理想状態を考える</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></description>
										<content:encoded><![CDATA[<p>個人開発のユーザー情報更新の処理を見ていて、もっと良くできるのではないかと思い、Clean ArchitectureとDDDをベースに理想状態を考えてみました。</p>
<h2>現状のユーザー情報更新の流れ</h2>
<p><img data-attachment-id="2426" data-permalink="https://mintaku-blog.net/ideal-user-flow/%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2023-11-09-14-46-37/" data-orig-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/11/6a50b30661438d1373cbd8bf6e2ab866.jpg?fit=1358%2C598&amp;ssl=1" data-orig-size="1358,598" 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="スクリーンショット 2023-11-09 14.46.37" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/11/6a50b30661438d1373cbd8bf6e2ab866.jpg?fit=300%2C132&amp;ssl=1" data-large-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/11/6a50b30661438d1373cbd8bf6e2ab866.jpg?fit=800%2C352&amp;ssl=1" loading="lazy" src="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/11/6a50b30661438d1373cbd8bf6e2ab866.jpg?resize=800%2C352&#038;ssl=1" alt="" width="800" height="352" class="aligncenter size-large wp-image-2426" srcset="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/11/6a50b30661438d1373cbd8bf6e2ab866.jpg?resize=1024%2C451&amp;ssl=1 1024w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/11/6a50b30661438d1373cbd8bf6e2ab866.jpg?resize=300%2C132&amp;ssl=1 300w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/11/6a50b30661438d1373cbd8bf6e2ab866.jpg?resize=768%2C338&amp;ssl=1 768w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/11/6a50b30661438d1373cbd8bf6e2ab866.jpg?w=1358&amp;ssl=1 1358w" sizes="(max-width: 800px) 100vw, 800px" data-recalc-dims="1" /></p>
<p>現状のユーザー情報更新までの流れはこんな感じになっています。</p>
<p>まずユーザー情報のアイコン画像をStorageに保存して、そのパスとユーザー情報をFirestoreに更新します。その後、更新したユーザー情報をもとに一部の情報ブラウザのLocalStorageにするような流れになっています。</p>
<p>データの流れは基本的に画面がから受け取った値を横流しにしている感じになっており、アプリケーションとしての役割や領域が曖昧になってしまっています。また、アーキテクチャとしても簡易的なClean Architectureになっており、責務がしっかりと分けきれていない感じがしました。</p>
<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>

<p>&nbsp;</p>
<h2>ユーザー情報更新のデータの流れや構成をこうしたい</h2>
<p><img data-attachment-id="2427" data-permalink="https://mintaku-blog.net/ideal-user-flow/%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2023-11-09-14-47-14/" data-orig-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/11/880f6c07011f39ef6f190710965651a6-scaled.jpg?fit=2560%2C925&amp;ssl=1" data-orig-size="2560,925" 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="スクリーンショット 2023-11-09 14.47.14" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/11/880f6c07011f39ef6f190710965651a6-scaled.jpg?fit=300%2C108&amp;ssl=1" data-large-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/11/880f6c07011f39ef6f190710965651a6-scaled.jpg?fit=800%2C289&amp;ssl=1" loading="lazy" src="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/11/880f6c07011f39ef6f190710965651a6.jpg?resize=800%2C289&#038;ssl=1" alt="" width="800" height="289" class="aligncenter size-large wp-image-2427" srcset="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/11/880f6c07011f39ef6f190710965651a6-scaled.jpg?resize=1024%2C370&amp;ssl=1 1024w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/11/880f6c07011f39ef6f190710965651a6-scaled.jpg?resize=300%2C108&amp;ssl=1 300w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/11/880f6c07011f39ef6f190710965651a6-scaled.jpg?resize=768%2C278&amp;ssl=1 768w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/11/880f6c07011f39ef6f190710965651a6-scaled.jpg?resize=1536%2C555&amp;ssl=1 1536w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/11/880f6c07011f39ef6f190710965651a6-scaled.jpg?resize=2048%2C740&amp;ssl=1 2048w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/11/880f6c07011f39ef6f190710965651a6-scaled.jpg?w=1600&amp;ssl=1 1600w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/11/880f6c07011f39ef6f190710965651a6-scaled.jpg?w=2400&amp;ssl=1 2400w" sizes="(max-width: 800px) 100vw, 800px" data-recalc-dims="1" /></p>
<p>ということで、自分の中で色々整理して、理想のアーキテクチャやデータフロー（データの型は簡易的にしています）を考えてみました。</p>
<p>まずは、ドメインやエンティティとして領域ごとに型を用意しました。アプリケーションのコアとなる部分ではドメインとして扱い、サードパーティーなどを操作するDriver層ではエンティティとして扱うようにしました。</p>
<p>アプリケーションと外部の境界のGateway/Presenterにてデータの変換を行うようにします。また、Contoller層を用意し、Usecaseにドメインの型で渡すようにします。</p>
<p>まだまだかもですが、これでだいぶ責務の切り分けはできてきたかと思います。きっちりやろうとするとコード量が増えますが、コアなユースケースなので安全かつ複雑性を排除して、keep cleanの精神でリファクタリングしていきたいです。</p>
<p>&nbsp;</p>
<h2>まとめ</h2>
<p>最近あんまりガッツリ開発に携わってないと、新鮮な目で設計とかリアーキ考えることができ、良い機会になりました。</p>
<p>あと考えていくうちに疑問点とか色々出てくるので、色んな人に聞いたり、議論しながらもっとブラッシュアップしてきたいと思いました。</p>
<p>他にもリファクタリングしたいところは多々あるので、コツコツやっていきたいと思います。</p>The post <a href="https://mintaku-blog.net/ideal-user-flow/">ユーザー情報更新のデータフローの理想状態を考える</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></content:encoded>
					
					<wfw:commentRss>https://mintaku-blog.net/ideal-user-flow/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2419</post-id>	</item>
		<item>
		<title>運営しているWebサービスをリアーキテクチャした話を整理する</title>
		<link>https://mintaku-blog.net/rearchitecture/</link>
					<comments>https://mintaku-blog.net/rearchitecture/#respond</comments>
		
		<dc:creator><![CDATA[みんたく]]></dc:creator>
		<pubDate>Wed, 05 Jul 2023 12:25:04 +0000</pubDate>
				<category><![CDATA[アーキテクチャ]]></category>
		<guid isPermaLink="false">https://mintaku-blog.net/?p=2387</guid>

					<description><![CDATA[<p>運営しているWebサービスが複雑化してきたので、リアーキテクチャやリファクタリングをしました。まだまだなところは多いですが、一旦整理したのでまとめておきます …</p>
The post <a href="https://mintaku-blog.net/rearchitecture/">運営しているWebサービスをリアーキテクチャした話を整理する</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></description>
										<content:encoded><![CDATA[<p>運営しているWebサービスが複雑化してきたので、リアーキテクチャやリファクタリングをしました。まだまだなところは多いですが、一旦整理したのでまとめておきます。</p>
<h2>背景</h2>
<p>自分が作ったWebサービスは、元々メディア的な属性が強かったのですが、認証機能やCRUDする機能など動的な機能が増えてきて、ロジック部分が多くなり複雑化してきました。</p>
<p>また、サーバーレスということもありフロントでのコード量が増え、なんとかアーキテクチャを見直さないと大変そうな感じがしていました。</p>
<p>そして一番の理由がテストが書きづらいかったところです。元々メディア的な運営の仕方でSSGしていたこともあり、ほぼ静的ページであったため、デプロイ前のgenerateがテスト代わりのようになっていました。</p>
<p>しかし、動的な機能が増えるとSSGでは検知できないため、テストを書く必要があり、いざ書こうと思うと非常に書きにく設計となっていました。</p>
<p>今後の運用性・拡張性を考えてもリアーキテクチャは急務であり、非常に優先度の高い重要なタスクとして取り掛かりました。</p>
<h2>色々考えたことを整理する</h2>
<p>・フレームワークに依存した作りをやめたい</p>
<p>・レイヤリングしたい</p>
<p>・DIしたい</p>
<p>・UTの導入</p>
<p>・ライブラリに依存したドメインを使わないようにしたい</p>
<p>・共通化したい</p>
<p>・型エラー直したい</p>
<p>ざっくりこんなことを考えており、1つ1つこなしていきました。感覚的な意識の部分としては、「依存性をなくして抽象的かつ中長期的に開発生産性の高いものにしたい」ということをぼんやり考えてました。</p>
<p>そんな中で、Nuxt3 + TypeScriptで開発しているのですが、ロジック部分などなるべくフレームワークに依存するような書き方はやめたいと考え、テストの書きにくさだったり、汎用性だったりを考えたときに、なるべく抽象性の高い書き方にしたく、素のTypeScriptで書くようにしました。</p>
<p>また、ドメインに関する情報の増加や複雑性もあり、DDD + Clean Architectureを導入することにしました。一部Firebaseライブラリのドメインを使っていましたが、そこへの依存度が高くなってしまい、Firebaseを辞めたときやバージョンアップで気付かぬうちに使えなくなってたというのをなくしたい気持ちがありました。</p>
<p>その上で、JestによるUTを導入し、安全かつ見通しの良い、開発していて楽しい開発体験を目指しました。</p>
<p>今回は、DDD + Clean Architectureを導入したリアーキテクチャのところを一部まとめておきたいと思います。</p>
<h2>DDD + Clean Architectureの導入してリアーキテクチャする</h2>
<p><img data-attachment-id="2392" data-permalink="https://mintaku-blog.net/rearchitecture/%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2023-07-03-16-24-27/" data-orig-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/06/dc4f8255a9399360fa44bb0d6c10bdf5.png?fit=232%2C580&amp;ssl=1" data-orig-size="232,580" 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="スクリーンショット 2023-07-03 16.24.27" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/06/dc4f8255a9399360fa44bb0d6c10bdf5.png?fit=120%2C300&amp;ssl=1" data-large-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/06/dc4f8255a9399360fa44bb0d6c10bdf5.png?fit=232%2C580&amp;ssl=1" loading="lazy" src="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/06/dc4f8255a9399360fa44bb0d6c10bdf5.png?resize=146%2C365&#038;ssl=1" alt="" width="146" height="365" class="aligncenter wp-image-2392" srcset="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/06/dc4f8255a9399360fa44bb0d6c10bdf5.png?w=232&amp;ssl=1 232w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/06/dc4f8255a9399360fa44bb0d6c10bdf5.png?resize=120%2C300&amp;ssl=1 120w" sizes="(max-width: 146px) 100vw, 146px" data-recalc-dims="1" /></p>
<p>構成としてはこんな感じで、srcの下にそれぞれレイヤリングして配置しています。modules.tsに依存関係を書いています。</p>
<p>依存関係としてはusecaseはgatewayとpresenterに依存しており、gatewaはdriverに依存しているといった形です。</p>
<p>本当はportなどのインターフェイスを作って依存性逆転して綺麗な形にしたいのですが、一旦このような形になっています。</p>
<p>usecaseではビジネスロジックを記述しており、garewayを通してデータの取得や登録などをしています。また、presenterを経由してstateの管理をしています。</p>
<p>gatewayではusecaseとdriverの橋渡しとして、driverからのデータをdomainに変換したりしています。データの取得・登録の実装部分はdriverで書いています。DBやAPIなど変更したとしても、driverを変えれば影響がないようにdriverからentityに詰めてgatewayに返すようにし、gatewayでdomainに詰め替えるようにしているところです。</p>
<p>ここまでレイヤリングすると、あとはusecaseを呼び出すだけで済むので、だいぶスッキリしました。</p>
<p>今後はTDDで進めたいと思っていますが、既存の部分に関してはガッとリアーキテクチャしてから、テスト書きました。テストはJestを使っており、jest-whenライブラリを活用しつつ書いています。</p>
<h2>今後について</h2>
<p>一旦基盤はできたので、今後は適宜リファクタリングしながら、保守性の高い設計に向けて改善を続けたいと思っています。</p>
<p>ドメインも全部なかったりまだ伸び代もあるので、常に改善する気持ちで開発し続けることが大事になりそうです。</p>
<p>あとはSSGベースでやってますが、SSRにしていきたい感あります。ちょっとコスト的にどれだけ高くなるのか心配ですが、チャレンジしたいと思っています。</p>
<p>あとE2Eも書きたいと思っています。Cypressでちょっとやりましたが、放置してしまってるので、どこかで本腰入れてやりたいと思っています。やはりテストで担保されていないと、いつ壊れるかわからないですし、そうした状況下で開発するのは結構しんどいと思います。</p>
<p>品質の担保かつ生産性の向上のためにも、よりより開発・設計を追求していきたいです。</p>
<div id="gtx-trans" style="position: absolute; left: 76px; top: 2643.65px;">
<div class="gtx-trans-icon"></div>
</div>The post <a href="https://mintaku-blog.net/rearchitecture/">運営しているWebサービスをリアーキテクチャした話を整理する</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></content:encoded>
					
					<wfw:commentRss>https://mintaku-blog.net/rearchitecture/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2387</post-id>	</item>
		<item>
		<title>改めて運営しているWebサービスのアーキテクチャを整理する</title>
		<link>https://mintaku-blog.net/architecture-2nd/</link>
					<comments>https://mintaku-blog.net/architecture-2nd/#respond</comments>
		
		<dc:creator><![CDATA[みんたく]]></dc:creator>
		<pubDate>Fri, 19 May 2023 11:35:02 +0000</pubDate>
				<category><![CDATA[Nuxt.js]]></category>
		<category><![CDATA[Firebase]]></category>
		<category><![CDATA[アーキテクチャ]]></category>
		<guid isPermaLink="false">https://mintaku-blog.net/?p=2370</guid>

					<description><![CDATA[<p>最近、NetlifyからFirebase Hostingに移行したり、認証機能を追加実装したりとアーキテクチャがちょっと変わってきたので、改めて整理したいと …</p>
The post <a href="https://mintaku-blog.net/architecture-2nd/">改めて運営しているWebサービスのアーキテクチャを整理する</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></description>
										<content:encoded><![CDATA[<p>最近、NetlifyからFirebase Hostingに移行したり、認証機能を追加実装したりとアーキテクチャがちょっと変わってきたので、改めて整理したいと思います。</p>
<h2>今のざっくりアーキテクチャ図</h2>
<p><img data-attachment-id="2373" data-permalink="https://mintaku-blog.net/architecture-2nd/%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2023-05-19-11-31-10/" data-orig-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/05/2936e99a19fcbf591587759c99c5011a.jpg?fit=1418%2C612&amp;ssl=1" data-orig-size="1418,612" 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="スクリーンショット 2023-05-19 11.31.10" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/05/2936e99a19fcbf591587759c99c5011a.jpg?fit=300%2C129&amp;ssl=1" data-large-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/05/2936e99a19fcbf591587759c99c5011a.jpg?fit=800%2C345&amp;ssl=1" loading="lazy" src="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/05/2936e99a19fcbf591587759c99c5011a.jpg?resize=800%2C345&#038;ssl=1" alt="" width="800" height="345" class="aligncenter size-large wp-image-2373" srcset="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/05/2936e99a19fcbf591587759c99c5011a.jpg?resize=1024%2C442&amp;ssl=1 1024w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/05/2936e99a19fcbf591587759c99c5011a.jpg?resize=300%2C129&amp;ssl=1 300w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/05/2936e99a19fcbf591587759c99c5011a.jpg?resize=768%2C331&amp;ssl=1 768w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/05/2936e99a19fcbf591587759c99c5011a.jpg?w=1418&amp;ssl=1 1418w" sizes="(max-width: 800px) 100vw, 800px" data-recalc-dims="1" /></p>
<p>こちらの記事で書いてありますが、元々Netlifyを使用していましたが、Firebase Hostingに移行しました。</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/2023/04/afbf9bcde7cb172c4c5e6bc2b6e660b9.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/2023/04/afbf9bcde7cb172c4c5e6bc2b6e660b9.png?w=560&amp;ssl=1 560w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/04/afbf9bcde7cb172c4c5e6bc2b6e660b9.png?resize=300%2C169&amp;ssl=1 300w" sizes="(max-width: 560px) 100vw, 560px" data-attachment-id="2363" data-permalink="https://mintaku-blog.net/netlify-to-firebase/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-9-5/" data-orig-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/04/afbf9bcde7cb172c4c5e6bc2b6e660b9.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のコピーのコピーのコピー (9)" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/04/afbf9bcde7cb172c4c5e6bc2b6e660b9.png?fit=300%2C169&amp;ssl=1" data-large-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/04/afbf9bcde7cb172c4c5e6bc2b6e660b9.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/netlify-to-firebase/">NetlifyからFirebase Hostingに移行した話</a>
			</p>
							<div class="ys-blog-card__dscr">
					満を辞して？NetlifyからFirebase Hostingに移行したので、メ&hellip;				</div>
										<div class="ys-blog-card__domain">mintaku-blog.net</div>
					</div>
	</div>
</div>

<p>基本SSGの構成は変わっておらず、後で説明しますが、一部SPA的なページが追加されました。</p>
<p>流れとしてはmainブランチにプッシュされるとGithub Actionsのパイプラインが動き出し、ビルドが始まります。</p>
<p>ビルド時はContenfulからデータを取得して、静的なページを生成していきます。ページの生成が完了すると、Firebase Hostingにデプロイします。</p>
<p>ページ数は今のところ大体3300ページくらいあり、ビルドしてデプロイするまで20分近くかかっており、長すぎるビルド時間が直近の課題となっています。</p>
<p>今考えているアプローチ方法は、ISGの導入や最近Nuxt3機能追加されたpayloadを使うなど、サーバー通信を減らしていきたいと思っています。</p>
<p>&nbsp;</p>
<h2>認証機能のところ</h2>
<p><img data-attachment-id="2374" data-permalink="https://mintaku-blog.net/architecture-2nd/%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2023-05-19-19-38-33/" data-orig-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/05/2801c7ada334840d3ce2e425ae897010.jpg?fit=1366%2C910&amp;ssl=1" data-orig-size="1366,910" 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="スクリーンショット 2023-05-19 19.38.33" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/05/2801c7ada334840d3ce2e425ae897010.jpg?fit=300%2C200&amp;ssl=1" data-large-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/05/2801c7ada334840d3ce2e425ae897010.jpg?fit=800%2C533&amp;ssl=1" loading="lazy" src="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/05/2801c7ada334840d3ce2e425ae897010.jpg?resize=800%2C533&#038;ssl=1" alt="" width="800" height="533" class="aligncenter size-large wp-image-2374" srcset="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/05/2801c7ada334840d3ce2e425ae897010.jpg?resize=1024%2C682&amp;ssl=1 1024w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/05/2801c7ada334840d3ce2e425ae897010.jpg?resize=300%2C200&amp;ssl=1 300w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/05/2801c7ada334840d3ce2e425ae897010.jpg?resize=768%2C512&amp;ssl=1 768w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/05/2801c7ada334840d3ce2e425ae897010.jpg?w=1366&amp;ssl=1 1366w" sizes="(max-width: 800px) 100vw, 800px" data-recalc-dims="1" /></p>
<p>今回新しく追加した認証機能はFirebase Authenticationを使いました。Firebaseに統合したいのもあって、ホスティングサービスもNetlifyからFirebase Hostingに移行した感じです。</p>
<p>1stリリースでは、認証のプロバイダはGoogleとTwitterのみにしています。後々、メールアドレスや他のSNS認証も追加する予定です。</p>
<p>ユーザー情報などの格納データはFirestoreを使っています。Firebase Authenticationとあわせて開発しやすかったです。</p>
<p>&nbsp;</p>
<h2>今後のアーキテクチャについて</h2>
<p>今後の開発において、ますます動的な機能が増えていくと思います。今はサーバーレスのSSGで開発していますが、後々はAPIを立てたり、SSRに変更するなど考えています。</p>
<p>新しい技術なども出てきたりすると思うので、臨機応変にその時その時考えて、アーキテクチャや使用技術を決めていきたいと思います。</p>
<p>また、負債を残さないためにもdependabotの導入によるライブラリのバージョンアップやこまめなリファクタリングなど、開発生産性を高く保ちながらより良いサービスを提供していきたいです。</p>The post <a href="https://mintaku-blog.net/architecture-2nd/">改めて運営しているWebサービスのアーキテクチャを整理する</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></content:encoded>
					
					<wfw:commentRss>https://mintaku-blog.net/architecture-2nd/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2370</post-id>	</item>
		<item>
		<title>Kotlin+Spring Bootで簡単なAPIをClean Architectureで実装してみた</title>
		<link>https://mintaku-blog.net/kotlin-spring-boot/</link>
					<comments>https://mintaku-blog.net/kotlin-spring-boot/#respond</comments>
		
		<dc:creator><![CDATA[みんたく]]></dc:creator>
		<pubDate>Sun, 03 Apr 2022 02:50:22 +0000</pubDate>
				<category><![CDATA[アーキテクチャ]]></category>
		<category><![CDATA[Kotlin]]></category>
		<guid isPermaLink="false">https://mintaku-blog.net/?p=2182</guid>

					<description><![CDATA[<p>Kotlin + Spring Bootを触ってみたので、Clean ArchitectureでちょっとしたAPIを作ってみました。 Kotlin: 1.6 …</p>
The post <a href="https://mintaku-blog.net/kotlin-spring-boot/">Kotlin+Spring Bootで簡単なAPIをClean Architectureで実装してみた</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></description>
										<content:encoded><![CDATA[<p>Kotlin + Spring Bootを触ってみたので、Clean ArchitectureでちょっとしたAPIを作ってみました。</p>
<ul>
<li>Kotlin: 1.6.10</li>
<li>SpringBoot: 2.4.4</li>
</ul>
<h2>Spring BootでのDI</h2>
<p>そもそもDIとはDependeny Injectionの略で依存性の注入という意味です。DIはオブジェクト間に生じる依存関係をオブジェクト内のコードに直接記述せず、外部から何らかの形で与えるようにする手法です。</p>
<h3>@Component</h3>
<p>クラスにComponentアノテーションを付与するとDIの対象であることを表します。<br />
そのクラスをDIコンテナが管理してくれるようになります。</p>
<p>他にも@Controllerや@Serviceがありますが、以下の使い分けをします。</p>
<ul>
<li>@Controller： MVCでコントローラー層のクラスに付与</li>
<li>@Service： MVCでサービス層のクラスに付与</li>
<li>@Repository： MVCでデータ層のクラスに付与</li>
<li>@Component： MVCに限らず、DIで利用したいクラスへ付与する</li>
</ul>
<h3>@Bean</h3>
<p>付与した関数で返却したインスタンスがDIコンテナに登録され、そのインスタンスをDIで使用できるようになります。</p>
<p>@Componentと@Beanの使い分けは以下の記事が参考になりました。</p>

<div class="ys-blog-card">
	<div class="ys-blog-card__container">
					<figure class="ys-blog-card__image">
				<img src="https://i0.wp.com/www.greptips.com/images/ogp/1318.png?w=800&#038;ssl=1" alt="" data-recalc-dims="1">			</figure>
				<div class="ys-blog-card__text">
			<p class="ys-blog-card__title">
				<a class="ys-blog-card__link" href="https://www.greptips.com/posts/1318/">Spring Bootの@Componentと@Beanの違い - grep Tips *</a>
			</p>
							<div class="ys-blog-card__dscr">
					まずは結論から SpringのDIコンテナに管理してもらいたいクラスが、自分で作&hellip;				</div>
										<div class="ys-blog-card__domain">www.greptips.com</div>
					</div>
	</div>
</div>

<h3>@Autowired</h3>
<p>注入されるフィールドに付けます。</p>
<p>DIコンテナはAutowiredアノテーションを付けたフィールドに対して、合致するオブジェクトを探してインジェクションします。</p>
<h4>lateinit var</h4>
<p>このフィールドへのインジェクションは変数の読み込みと同時に初期化されるのではなく、後からインジェクションされるためvar定義しておく必要があります。</p>
<p>&nbsp;</p>
<h2>Clean ArchitectureでTODOアプリを意識したAPIを実装してみる</h2>
<p>Clean ArchitectureでTODOアプリを意識したAPIを実装してみました。といってもDBも用意せずスタブみたいな感じにしています。メソッドもgetTasks()とgetTask()だけでちょっとしたAPIとなっています。</p>
<h3>Enterprise Business Rules</h3>
<p>Enterprise Business Rulesではアプリケーション全体の最重要ビジネスルールをカプセル化したドメインを格納しています。</p>
<p>今回はTODOアプリを意識しているので、タスクIDとタイトル、コンテンツを含むTaskドメインとListとで持つTasksドメインを作っています。</p>
<p>・Task.kt</p><pre class="crayon-plain-tag">package com.example.domain

data class TaskId(val value: Int)

data class Title(val value: String)

data class Content(val value: String)

data class Task(val title: Title, val content: Content)

data class Tasks(val list: List&lt;Task&gt;)</pre><p>&nbsp;</p>
<h3>Application Business Rules</h3>
<p>Application Business Rulesではアプリケーション固有のビジネスルールが含まれ、入出力するデータの流れを調整します。</p>
<p>TaskPortにインターフェイスを定義し、TaskGatewayで具象クラスを実装するようにしています。TaskUseCaseではTaskPortを呼び出すように実装しています。</p>
<p>・TaskPort.kt</p><pre class="crayon-plain-tag">package com.example.port

import com.example.domain.Task
import com.example.domain.TaskId
import com.example.domain.Tasks

interface TaskPort {
  fun getTasks(): Tasks
  fun getTask(taskId: TaskId): Task
}</pre><p>&nbsp;</p>
<p>・TaskUseCase.kt</p><pre class="crayon-plain-tag">package com.example.usecase

import com.example.domain.Task
import com.example.domain.TaskId
import com.example.port.TaskPort
import com.example.domain.Tasks
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component

@Component
class TaskUseCase() {
  @Autowired
  lateinit var taskPort: TaskPort

  fun getTasks(): Tasks {
    return taskPort.getTasks()
  }

  fun getTask(taskId: TaskId): Task {
    return taskPort.getTask(taskId)
  }

}</pre><p>&nbsp;</p>
<h3>Interface &amp; Adapter</h3>
<p>Interface &amp; Adapterではドメインやユースケースに便利な形式からDBに便利な形式にデータを変換します。</p>
<p>TaskGatewayはTaskPortで定義したinterfaceを実装しています。またTaskDriverで取得したJSONをドメインに変換する処理をしています。</p>
<p>TaskControllerでは逆にドメインをレスポンスの形に変換する処理をしています。</p>
<p>・TaskGateway.kt</p><pre class="crayon-plain-tag">package com.example.gateway

import com.example.domain.*
import com.example.driver.TaskDriver
import com.example.driver.TaskJson
import com.example.driver.TasksJson
import com.example.port.TaskPort
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component

@Component
class TaskGateway: TaskPort {
  @Autowired
  lateinit var taskDriver: TaskDriver

  override fun getTasks(): Tasks {
    val tasksJson: TasksJson = taskDriver.getTasks()
    return tasksJson.toTasks()
  }

  override fun getTask(taskId: TaskId): Task {
    val taskJson: TaskJson = taskDriver.getTask(taskId.value)
    return taskJson.toTask()
  }
}

fun TasksJson.toTasks(): Tasks {
  return this.list.map {
    Task(Title(it.title), Content(it.content))
  }.let(::Tasks)
}

fun TaskJson.toTask(): Task {
  return Task(Title(this.title), Content(this.content))
}</pre><p>&nbsp;</p>
<p>・TaskController.kt</p><pre class="crayon-plain-tag">package com.example.controller

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.bind.annotation.RestController
import com.example.usecase.TaskUseCase
import com.example.domain.Tasks
import com.example.domain.Task
import com.example.domain.TaskId
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable

@RestController
class TaskController {
  @Autowired
  lateinit var taskUseCase: TaskUseCase

  @GetMapping(value = ["/api/tasks"])
    fun getTasks(): ResponseEntity&lt;TasksJson&gt; {
    return ResponseEntity.ok(taskUseCase.getTasks().toJson())
  }

  @GetMapping(value = ["/api/tasks/{taskId}"])
  fun getTask(
    @PathVariable(value = "taskId", required = true) taskId: Int
  ): ResponseEntity&lt;TaskJson&gt; {
    return ResponseEntity.ok(taskUseCase.getTask(TaskId(taskId)).toJson())
  }
}

data class TaskJson(val title: String, val content: String)
data class TasksJson(val tasks: List&lt;TaskJson&gt;)

fun Tasks.toJson() = this.list.map { it.toJson() }.let(::TasksJson)
fun Task.toJson() = TaskJson(this.title.value, this.content.value)</pre><p>&nbsp;</p>
<h3>Framework &amp; Driver</h3>
<p>Framework &amp; Driverはフレームワークやツールで構成されています。</p>
<p>今回はスタブでJSON形式の値を返すようにしていますが、本来ならDB操作やAPIリクエストを行う想定です。</p>
<p>・TaskDriver.kt</p><pre class="crayon-plain-tag">package com.example.driver

import com.fasterxml.jackson.annotation.JsonCreator
import org.springframework.stereotype.Component

@Component
class TaskDriver() {
  fun getTasks(): TasksJson {
    return TasksJson(listOf(TaskJson("title", "content"), TaskJson("title2", "content2")))
  }

  fun getTask(taskId: Int): TaskJson {
    return TaskJson("title", "content")
  }
}

data class TasksJson @JsonCreator constructor(
  val list: List&lt;TaskJson&gt;
)

data class TaskJson @JsonCreator constructor(
  val title: String,
  val content: String
)</pre><p></p>The post <a href="https://mintaku-blog.net/kotlin-spring-boot/">Kotlin+Spring Bootで簡単なAPIをClean Architectureで実装してみた</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></content:encoded>
					
					<wfw:commentRss>https://mintaku-blog.net/kotlin-spring-boot/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2182</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[TypeScript]]></category>
		<category><![CDATA[アーキテクチャ]]></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>DDD実践入門を読んでモデリングやエンティティなどについて整理する</title>
		<link>https://mintaku-blog.net/ddd-entity-modeling/</link>
					<comments>https://mintaku-blog.net/ddd-entity-modeling/#respond</comments>
		
		<dc:creator><![CDATA[みんたく]]></dc:creator>
		<pubDate>Sun, 05 Dec 2021 06:41:01 +0000</pubDate>
				<category><![CDATA[アーキテクチャ]]></category>
		<guid isPermaLink="false">https://mintaku-blog.net/?p=2102</guid>

					<description><![CDATA[<p>DDD実践入門を読んでエンティティや値オブジェクト、モデリングについて整理してみました。 DDDとは 「顧客と開発者が業務を戦略的に理解し、共通の言葉を使い …</p>
The post <a href="https://mintaku-blog.net/ddd-entity-modeling/">DDD実践入門を読んでモデリングやエンティティなどについて整理する</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></description>
										<content:encoded><![CDATA[<p>DDD実践入門を読んでエンティティや値オブジェクト、モデリングについて整理してみました。</p>
<h2>DDDとは</h2>
<p>「顧客と開発者が業務を戦略的に理解し、共通の言葉を使いながらシステムを発展させる手法」です。</p>
<p>具体的には、チームの共通言語である「ユビキタス言語」を用いて「ドメインモデル」を構築し、それをコードとして実装します。また大規模で密結合なシステムにならないように、「ドメイン」と「境界づけられたコンテキスト」でシステムを分割し、「コアドメイン」という最重要領域に集中して開発を行います。</p>
<h2>DDDは高品質のソフトウェアを設計する手法</h2>
<p>ここでいう高品質とはバグがないということではなく、ビジネス的に成功していることを指しています。</p>
<p>「事業を理解し、チームの知識を1つにまとめる」ことを重視し、それを「ユビキタス言語」と呼ばれるチームの共通語でプログラムを実装します。</p>
<h2>DDDの3原則</h2>
<ol>
<li>コアドメインに集中すること</li>
<li>ドメインの実践者とソフトウェアの実践者による創造的な共同作業を通じて、モデルを探究すること</li>
<li>明示的な境界づけられたコンテキストの内部で、ユビキタス言語を語ること</li>
</ol>
<p>DDDで重要なことはビジネス的に成功しているソフトウェアを構築すること</p>
<h2>DDDの用語など</h2>
<p><img data-attachment-id="2105" data-permalink="https://mintaku-blog.net/ddd-entity-modeling/image4-6/" data-orig-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/11/image4.jpg?fit=908%2C650&amp;ssl=1" data-orig-size="908,650" 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="image4" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/11/image4.jpg?fit=300%2C215&amp;ssl=1" data-large-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/11/image4.jpg?fit=800%2C573&amp;ssl=1" loading="lazy" src="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/11/image4.jpg?resize=800%2C573&#038;ssl=1" alt="" width="800" height="573" class="aligncenter size-full wp-image-2105" srcset="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/11/image4.jpg?w=908&amp;ssl=1 908w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/11/image4.jpg?resize=300%2C215&amp;ssl=1 300w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/11/image4.jpg?resize=768%2C550&amp;ssl=1 768w" sizes="(max-width: 800px) 100vw, 800px" data-recalc-dims="1" /></p>
<h3>モデル</h3>
<p>抽象化によって不要な詳細を省いたもの</p>
<h3>ドメインモデル</h3>
<p>複雑な業務ドメインの中から、システムに必要な概念を適切に抽出したもの</p>
<h3>ドメイン</h3>
<p>チームが取り組んでいる事業全体</p>
<h3>コアドメイン</h3>
<p>事業的に最も重要で戦略的に不可欠な部分</p>
<h3>サブドメイン</h3>
<p>コアドメインではな補助的な部分</p>
<h3>境界づけられコンテキスト</h3>
<p>ドメインの課題を解決する部分</p>
<h2>エンティティと値オブジェクト</h2>
<p><img data-attachment-id="2106" data-permalink="https://mintaku-blog.net/ddd-entity-modeling/image3-9/" data-orig-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/11/image3-1.jpg?fit=888%2C558&amp;ssl=1" data-orig-size="888,558" 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="image3" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/11/image3-1.jpg?fit=300%2C189&amp;ssl=1" data-large-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/11/image3-1.jpg?fit=800%2C503&amp;ssl=1" loading="lazy" src="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/11/image3-1.jpg?resize=800%2C503&#038;ssl=1" alt="" width="800" height="503" class="aligncenter size-full wp-image-2106" srcset="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/11/image3-1.jpg?w=888&amp;ssl=1 888w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/11/image3-1.jpg?resize=300%2C189&amp;ssl=1 300w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/11/image3-1.jpg?resize=768%2C483&amp;ssl=1 768w" sizes="(max-width: 800px) 100vw, 800px" data-recalc-dims="1" /></p>
<h3>エンティティ</h3>
<ul>
<li>一意なものを表現する概念</li>
<li>一意であるため長期にわたって変化できるオブジェクト</li>
<li>一意に識別して変更を管理する必要がないものは値オブジェクト</li>
</ul>
<h3>値オブジェクト</h3>
<ul>
<li>一意に識別する必要がないオブジェクト</li>
<li>従来はシンプルな文字列型や数値型で実装してしまうような情報を「値オブジェクト」として実装することで、業務をコードとして明確に表現できるようになる<br />
→ primitive型と対比すると値自体に振る舞いを持たせることで凝集度を上げることができる</li>
</ul>
<h2>モデリングとエンティティ(エンティティ発見の流れ)</h2>
<h3>① ソフトウェア要件の理解</h3>
<p><img data-attachment-id="2107" data-permalink="https://mintaku-blog.net/ddd-entity-modeling/image2-12/" data-orig-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/11/image2-1.jpg?fit=872%2C568&amp;ssl=1" data-orig-size="872,568" 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/11/image2-1.jpg?fit=300%2C195&amp;ssl=1" data-large-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/11/image2-1.jpg?fit=800%2C521&amp;ssl=1" loading="lazy" src="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/11/image2-1.jpg?resize=800%2C521&#038;ssl=1" alt="" width="800" height="521" class="aligncenter size-full wp-image-2107" srcset="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/11/image2-1.jpg?w=872&amp;ssl=1 872w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/11/image2-1.jpg?resize=300%2C195&amp;ssl=1 300w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/11/image2-1.jpg?resize=768%2C500&amp;ssl=1 768w" sizes="(max-width: 800px) 100vw, 800px" data-recalc-dims="1" /></p>
<p>設計を始めるにあたり、まず業務内容から整理していきます。開発する対象業務についてドメインエキスパートから要望を聞き出していきます。</p>
<p>ここでは業務仕様を簡単なソフトウェア用件という形でユースケースやユーザーストーリという形で作成します。</p>
<h3>② モデリングを検討しエンティティを抽出</h3>
<p><img data-attachment-id="2108" data-permalink="https://mintaku-blog.net/ddd-entity-modeling/image1-15/" data-orig-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/11/image1-2.jpg?fit=874%2C624&amp;ssl=1" data-orig-size="874,624" 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/11/image1-2.jpg?fit=300%2C214&amp;ssl=1" data-large-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/11/image1-2.jpg?fit=800%2C571&amp;ssl=1" loading="lazy" src="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/11/image1-2.jpg?resize=800%2C571&#038;ssl=1" alt="" width="800" height="571" class="aligncenter size-full wp-image-2108" srcset="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/11/image1-2.jpg?w=874&amp;ssl=1 874w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/11/image1-2.jpg?resize=300%2C214&amp;ssl=1 300w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/11/image1-2.jpg?resize=768%2C548&amp;ssl=1 768w" sizes="(max-width: 800px) 100vw, 800px" data-recalc-dims="1" /></p>
<p>チームで共通のユビキタス言語を構築することを意識して、ドメインエキスパートと議論を行います。</p>
<p>エンティティの抽出においては、シナリオにおいて「変更」というキーワードがある箇所に注目します。主語の用語がエンティティとなる可能性が高くなります。</p>
<h3>③ エンティティを識別する属性と振る舞いを検討</h3>
<p><img data-attachment-id="2103" data-permalink="https://mintaku-blog.net/ddd-entity-modeling/image6-2/" data-orig-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/11/image6.jpg?fit=870%2C492&amp;ssl=1" data-orig-size="870,492" 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="image6" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/11/image6.jpg?fit=300%2C170&amp;ssl=1" data-large-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/11/image6.jpg?fit=800%2C452&amp;ssl=1" loading="lazy" src="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/11/image6.jpg?resize=800%2C452&#038;ssl=1" alt="" width="800" height="452" class="aligncenter size-full wp-image-2103" srcset="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/11/image6.jpg?w=870&amp;ssl=1 870w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/11/image6.jpg?resize=300%2C170&amp;ssl=1 300w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/11/image6.jpg?resize=768%2C434&amp;ssl=1 768w" sizes="(max-width: 800px) 100vw, 800px" data-recalc-dims="1" /></p>
<p>エンティティに注目してモデリングを行います。エンティティ設計の初期段階ではエンティティを一意に特定する属性と振る舞いにだけ注目します。</p>
<h3>④ 「一意な識別子」の設計</h3>
<p>エンティティを識別する一意な識別子を検討します。</p>
<p>一意な識別子を決定したら、一意な識別子(IDや主キー値)の生成方法と生成タイミングについて検討します(生成方法と生成タイミングは省略)。</p>
<p>一意な識別子を設定した後は、その識別子の値を変更しないようにします。もし、識別子が設定済みにもかかわらず、変更処理が呼び出された場合はエラーとなるようにしなければいけません。</p>
<h3>⑤ エンティティの振る舞い(メソッド)を検討</h3>
<p><img data-attachment-id="2104" data-permalink="https://mintaku-blog.net/ddd-entity-modeling/image5-3/" data-orig-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/11/image5.jpg?fit=880%2C554&amp;ssl=1" data-orig-size="880,554" 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="image5" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/11/image5.jpg?fit=300%2C189&amp;ssl=1" data-large-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/11/image5.jpg?fit=800%2C504&amp;ssl=1" loading="lazy" src="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/11/image5.jpg?resize=800%2C504&#038;ssl=1" alt="" width="800" height="504" class="aligncenter size-full wp-image-2104" srcset="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/11/image5.jpg?w=880&amp;ssl=1 880w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/11/image5.jpg?resize=300%2C189&amp;ssl=1 300w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/11/image5.jpg?resize=768%2C483&amp;ssl=1 768w" sizes="(max-width: 800px) 100vw, 800px" data-recalc-dims="1" /></p>
<p>エンティティが持つ振る舞いについて検討していきます。ソフトウェア要件に登場する同志に注目することで、メソッドの候補を洗い出していきます。</p>
<h3>⑥ エンティティの作成方法を検討</h3>
<p>これまで検討してきたモデルをコードとして実装していきます。</p>
<p>エンティティは一意である必要があるため、エンティティのコンストラクタには「識別子の生成に必要な情報」や「問い合わせを行うために必要な情報」を引数として渡します。</p>
<h3>⑦ エンティティのバリデーションを検討</h3>
<p>エンティティでは「属性単位」「オブジェクト全体」「複数オブジェクト時」という3つの粒度に分けてバリデーションを管理します。</p>
<h4>属性単位</h4>
<p>エンティティの属性／プロパティに値をセットする場合、「空ではないか」「5文字以上か」「30文字以下か」といった妥当性のチェックを行います。</p>
<h4>オブジェクト全体</h4>
<p>オブジェクト全体のチェックでは、実装クラスに対応した「バリデータ」クラスを作成し、適切なタイミングで呼び出すようにします。</p>
<h4>複数オブジェクト時</h4>
<p>複数オブジェクトの状態が妥当かチェックするケースでは、必要な数だけバリデータを用意します。</p>The post <a href="https://mintaku-blog.net/ddd-entity-modeling/">DDD実践入門を読んでモデリングやエンティティなどについて整理する</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></content:encoded>
					
					<wfw:commentRss>https://mintaku-blog.net/ddd-entity-modeling/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2102</post-id>	</item>
		<item>
		<title>Node.js + MySQLのアプリケーションをDocker上で動かせるようにする</title>
		<link>https://mintaku-blog.net/docker-nodejs/</link>
					<comments>https://mintaku-blog.net/docker-nodejs/#respond</comments>
		
		<dc:creator><![CDATA[みんたく]]></dc:creator>
		<pubDate>Thu, 04 Nov 2021 04:07:59 +0000</pubDate>
				<category><![CDATA[Node.js]]></category>
		<category><![CDATA[アーキテクチャ]]></category>
		<guid isPermaLink="false">https://mintaku-blog.net/?p=2066</guid>

					<description><![CDATA[<p>ローカルで動かしていたNode.js + MySQLのアプリケーションをDocker上で動かせるようにしました。またSequelizeを使ってマイグレーショ …</p>
The post <a href="https://mintaku-blog.net/docker-nodejs/">Node.js + MySQLのアプリケーションをDocker上で動かせるようにする</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></description>
										<content:encoded><![CDATA[<p>ローカルで動かしていたNode.js + MySQLのアプリケーションをDocker上で動かせるようにしました。またSequelizeを使ってマイグレーションを実行するところまでメモしています。</p>
<h2>docker-compose.yml、Dockerfileの作成</h2>
<p>・docker-compose.yml</p><pre class="crayon-plain-tag">version: '3.4'

services:
  db:
    image: mysql
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: sample_db
    ports:
      - '3306:3306'
    volumes:
      - data_volume:/var/lib/mysql
    command: --default-authentication-plugin=mysql_native_password

  api:
    image: api
    env_file: ./app.env
    build:
      context: .
      dockerfile: ./Dockerfile
    ports:
      - 5000:5000
    volumes:
      - .:/usr/src/app
    depends_on:
      - db

volumes:
  data_volume:
</pre><p>・Dockerfile</p><pre class="crayon-plain-tag">FROM node:16

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm install

COPY . .

EXPOSE 5000

CMD sleep 1; npm run dev
</pre><p>&nbsp;</p>
<p>docker-compose.ymlではMySQLの「db」とNode.jsの「api」の2つのサービスを定義しています。</p>
<h3>「db」サービスの定義</h3>
<p>公式からMySQL 8.0のバージョンのイメージを使用します。</p>
<p>environmentでMySQLに接続する際のパスワードと生成するDBを指定します。portsでポートフォワーディングの設定をしており、今回はホスト側のポート3306からコンテナ側のポート3306に繋がります。</p>
<p>volumesで「data_volume」というボリュームをコンテナの/var/lib/mysql/ディレクトリにマウントすることで、コンテナが削除されてもデータが保持されます。</p>
<p>また、MySQL8.0からはログイン認証方式のデフォルトがcaching_sha2_passwordになったことにより、接続時にエラーが発生するため、旧来の認証方法にするためのコマンドを追加しています。</p>
<h3>「api」サービスの定義</h3>
<p>apiというimageを指定しています。env_fileでapiコンテナからdbコンテナに接続する際の環境変数が定義されているapp.envファイルを読み込むようにしています。</p>
<p>buildでapiを起動するためのビルドを実行します。今回はDockerfileを指定し、そこでNode.jsのアプリケーションをDocker化しています。</p>
<p>参考：https://nodejs.org/ja/docs/guides/nodejs-docker-webapp/</p>
<h3>depends_onは起動順を担保しているだけで、稼働順を担保していない</h3>
<p>少しハマったのは、docker-compose upでコンテナの作成・起動を成功するが、APIからDBに接続する際にエラーになったことです。</p>
<p>depend_onでDBが起動が終わったタイミングで接続すると思っていたのですが、よくよく調べてみるとdepends_onは起動順を担保しているだけで、稼働順を担保していないことがわかりました。</p>
<p>つまり、DBから起動していたのですが、DBの起動が完了する前にAPIがDBに接続しようとしていたのです。</p>
<p>そのため、確実にDBが起動完了してからAPIが接続するようにするために「CMD sleep 1; npm run dev」で1秒するスリープさせるようにしました。色々調べてみると公式とかですとシェルを使ってみるとかかいてあったのですが、とりあえずこの方法でやっています。</p>
<p>&nbsp;</p>
<h2>Sequelizeでマイグレーションを実行する</h2>
<p>docker-compose upで問題なくコンテナ作成・起動・接続できたらSequelizeを導入していきます。</p>
<p>SequelizeはNode.jsのORMでマイグレーション機能があり、これを使ってマイグレーションしていきます。以下のコマンドでSequelizeとその依存パッケージをインストールと初期化を行います。</p><pre class="crayon-plain-tag">$ docker-compose run --rm api npm i mysql2 sequelize sequelize-cli
$ docker-compose run --rm api npx sequelize-cli init</pre><p>次に以下のコマンドでモデルとマイグレーションファイルを作成します。今回はToDoアプリを想定しており、tasksというテーブルをつくります。</p><pre class="crayon-plain-tag">$ docker-compose run --rm app npx sequelize-cli model:generate --name Task --attributes content:string,status:integer</pre><p>実行するとモデルとマイグレーションファイルが作成されます。マイグレーションファイルを少し修正しておきます。具体的にstatusにデフォルト値を追加したりcreated_atをCURRENT_TIMESTAMP、updated_atをCURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMPにしました。</p>
<p>・xxxxxxxx-create-task.js</p><pre class="crayon-plain-tag">'use strict'
module.exports = {
  up: async (queryInterface, Sequelize) =&gt; {
    await queryInterface.createTable('tasks', {
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.INTEGER
      },
      content: {
        type: Sequelize.STRING
      },
      status: {
        type: Sequelize.INTEGER,
        defaultValue: 1
      },
      created_at: {
        allowNull: false,
        type: Sequelize.DATE,
        defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
      },
      updated_at: {
        allowNull: false,
        type: Sequelize.DATE,
        defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')
      }
    })
  },
  down: async (queryInterface, Sequelize) =&gt; {
    await queryInterface.dropTable('tasks')
  }
}
</pre><p>先ほど起動したMySQLのコンテナの接続情報をconfigファイルに設定して、マイグレーションを実行していきます。正常に実行されれば完了です。</p><pre class="crayon-plain-tag">$ docker-compose run --rm app npx sequelize-cli db:migrate</pre><p>あとはseedersフォルダで初期データを投入することができます。シードファイルを作成して投入したいデータをセットし、以下のコマンドを実行することでシードできます。</p><pre class="crayon-plain-tag">$ docker-compose run --rm app npx sequelize-cli db:seed:all</pre><p></p>The post <a href="https://mintaku-blog.net/docker-nodejs/">Node.js + MySQLのアプリケーションをDocker上で動かせるようにする</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></content:encoded>
					
					<wfw:commentRss>https://mintaku-blog.net/docker-nodejs/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2066</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>
		<item>
		<title>SOLID原則について整理する</title>
		<link>https://mintaku-blog.net/solid-principle/</link>
					<comments>https://mintaku-blog.net/solid-principle/#respond</comments>
		
		<dc:creator><![CDATA[みんたく]]></dc:creator>
		<pubDate>Sat, 01 May 2021 10:49:30 +0000</pubDate>
				<category><![CDATA[アーキテクチャ]]></category>
		<category><![CDATA[まとめ系]]></category>
		<guid isPermaLink="false">https://mintaku-blog.net/?p=1956</guid>

					<description><![CDATA[<p>クリーンアーキテクチャを読んでSOLIDの原則についてまとめました。 SOLID原則 SOLID原則は、関数やデータ構造をどのようにクラスに組み込むのか、そ …</p>
The post <a href="https://mintaku-blog.net/solid-principle/">SOLID原則について整理する</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></description>
										<content:encoded><![CDATA[<p>クリーンアーキテクチャを読んでSOLIDの原則についてまとめました。</p>
<h2>SOLID原則</h2>
<p>SOLID原則は、関数やデータ構造をどのようにクラスに組み込むのか、そしてクラスの相互接続をどのようにするのかといったことを教えてくれます。SOLID原則の目的は以下のような声質を持つ中間レベルのソフトウェア構造を作ることです。</p>
<ul>
<li>変更に強いこと</li>
<li>理解しやすいこと</li>
<li>コンポーネントの基盤として多くのソフトウェアシステムで利用できること</li>
</ul>
<p>&nbsp;</p>
<h2>単一責任の原則(SRP: Single Responsibility Principle)</h2>
<p><img data-attachment-id="1967" data-permalink="https://mintaku-blog.net/solid-principle/%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2021-04-01-22-39-38/" data-orig-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/04/0263d5be9cd7f4d90f8707b9b45ca9bf.png?fit=1134%2C808&amp;ssl=1" data-orig-size="1134,808" 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="スクリーンショット 2021-04-01 22.39.38" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/04/0263d5be9cd7f4d90f8707b9b45ca9bf.png?fit=300%2C214&amp;ssl=1" data-large-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/04/0263d5be9cd7f4d90f8707b9b45ca9bf.png?fit=800%2C570&amp;ssl=1" loading="lazy" class="aligncenter size-large wp-image-1967" src="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/04/0263d5be9cd7f4d90f8707b9b45ca9bf.png?resize=800%2C570&#038;ssl=1" alt="" width="800" height="570" srcset="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/04/0263d5be9cd7f4d90f8707b9b45ca9bf.png?resize=1024%2C730&amp;ssl=1 1024w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/04/0263d5be9cd7f4d90f8707b9b45ca9bf.png?resize=300%2C214&amp;ssl=1 300w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/04/0263d5be9cd7f4d90f8707b9b45ca9bf.png?resize=768%2C547&amp;ssl=1 768w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/04/0263d5be9cd7f4d90f8707b9b45ca9bf.png?w=1134&amp;ssl=1 1134w" sizes="(max-width: 800px) 100vw, 800px" data-recalc-dims="1" /></p>
<p>本書では単一責任の原則に違反しているEmployeeクラスの例からその必要性と解決策を提示していました。</p>
<p>給与システムにおけるEmployeeクラスがあり、calculatePay()、reportHours()、save()の3つのメソッドがあります。</p>
<p>それぞれのメソッドは以下の責務を負っています。</p>
<ul>
<li>calculatePay()：経理部門が規定する。報告先はCFO</li>
<li>reportHours()：人事部門が規定して使用する。報告先はCOO</li>
<li>save()：データベースを管理者が規定する。報告先はCTO</li>
</ul>
<p>これらのメソッドをひとつのEmployeeクラスに入れると、開発者は全てのアクターを結合することになり、CFOのチームの何らかの操作が、COOチームの使うものに影響を及ぼしてしまうことになります。例えばCFOチームのみが算出方法に手を加える場合が発生した時に、CFOチーム以外の算出方法も変更されてしまい問題が起きます。</p>
<h3>解決策</h3>
<p><img data-attachment-id="1961" data-permalink="https://mintaku-blog.net/solid-principle/%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2021-04-01-18-24-13/" data-orig-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/04/fb5b93b6b2bfec269054dbd484295fa3.png?fit=794%2C432&amp;ssl=1" data-orig-size="794,432" 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="スクリーンショット 2021-04-01 18.24.13" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/04/fb5b93b6b2bfec269054dbd484295fa3.png?fit=300%2C163&amp;ssl=1" data-large-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/04/fb5b93b6b2bfec269054dbd484295fa3.png?fit=794%2C432&amp;ssl=1" loading="lazy" class="aligncenter size-full wp-image-1961" src="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/04/fb5b93b6b2bfec269054dbd484295fa3.png?resize=794%2C432&#038;ssl=1" alt="" width="794" height="432" srcset="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/04/fb5b93b6b2bfec269054dbd484295fa3.png?w=794&amp;ssl=1 794w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/04/fb5b93b6b2bfec269054dbd484295fa3.png?resize=300%2C163&amp;ssl=1 300w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/04/fb5b93b6b2bfec269054dbd484295fa3.png?resize=768%2C418&amp;ssl=1 768w" sizes="(max-width: 794px) 100vw, 794px" data-recalc-dims="1" /></p>
<p>一番わかりやすい解決法として、データを関数から切り離すやり方があります。この場合だと3つのクラスに分けそこからEmployeeDataクラスを使うようにします。EmployeeDataクラスはシンプルなデータ構造のみを保持し、メソッドは含みません。</p>
<p>この方法の弱点として開発者が3つのクラスをインスタンス化して、追跡しなければいけなくなることです。この弱点を解消するのが以下のFacadeパターンです。</p>
<p>&nbsp;</p>
<p><img data-attachment-id="1959" data-permalink="https://mintaku-blog.net/solid-principle/%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2021-04-01-18-23-59/" data-orig-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/04/0e328dede4ea74ed356e323e014c86f0.png?fit=1126%2C450&amp;ssl=1" data-orig-size="1126,450" 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="スクリーンショット 2021-04-01 18.23.59" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/04/0e328dede4ea74ed356e323e014c86f0.png?fit=300%2C120&amp;ssl=1" data-large-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/04/0e328dede4ea74ed356e323e014c86f0.png?fit=800%2C320&amp;ssl=1" loading="lazy" class="aligncenter size-large wp-image-1959" src="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/04/0e328dede4ea74ed356e323e014c86f0.png?resize=800%2C320&#038;ssl=1" alt="" width="800" height="320" srcset="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/04/0e328dede4ea74ed356e323e014c86f0.png?resize=1024%2C409&amp;ssl=1 1024w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/04/0e328dede4ea74ed356e323e014c86f0.png?resize=300%2C120&amp;ssl=1 300w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/04/0e328dede4ea74ed356e323e014c86f0.png?resize=768%2C307&amp;ssl=1 768w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/04/0e328dede4ea74ed356e323e014c86f0.png?w=1126&amp;ssl=1 1126w" sizes="(max-width: 800px) 100vw, 800px" data-recalc-dims="1" /></p>
<p>EmployeeFacadeクラスは実行したいメソッドを持つクラスのインスタンを生成して処理を委譲するだけのシンプルな構造です。これにより複数をそれぞれインスタンス化して追跡する必要がなくなりなります。</p>
<p>&nbsp;</p>
<h2>オープン・クローズドの原則(OCP: Open-Closed Principle)</h2>
<p>オープン・クローズドの原則は、ソフトウェアの振る舞いは既存の成果物を変更せず拡張できるようにすべきという考え方です。</p>
<p>変更の影響を受けずにシステムを拡張する目的を達成するために、システムをコンポーネントに分割して、コンポーネントの依存関係を階層構造にすることです。そして、上位レベルのコンポーネントが下位レベルのコンポーネントの変更の影響を受けないようにします。</p>
<p>&nbsp;</p>
<h2>リスコフの置換原則(LSP: Liskov Substitution Principle)</h2>
<p><img data-attachment-id="1957" data-permalink="https://mintaku-blog.net/solid-principle/%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2021-04-01-18-24-24/" data-orig-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/04/1c72c8eee112608f68a2dd92ef6d861c.png?fit=974%2C444&amp;ssl=1" data-orig-size="974,444" 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="スクリーンショット 2021-04-01 18.24.24" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/04/1c72c8eee112608f68a2dd92ef6d861c.png?fit=300%2C137&amp;ssl=1" data-large-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/04/1c72c8eee112608f68a2dd92ef6d861c.png?fit=800%2C365&amp;ssl=1" loading="lazy" class="aligncenter size-full wp-image-1957" src="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/04/1c72c8eee112608f68a2dd92ef6d861c.png?resize=800%2C365&#038;ssl=1" alt="" width="800" height="365" srcset="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/04/1c72c8eee112608f68a2dd92ef6d861c.png?w=974&amp;ssl=1 974w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/04/1c72c8eee112608f68a2dd92ef6d861c.png?resize=300%2C137&amp;ssl=1 300w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/04/1c72c8eee112608f68a2dd92ef6d861c.png?resize=768%2C350&amp;ssl=1 768w" sizes="(max-width: 800px) 100vw, 800px" data-recalc-dims="1" /></p>
<p>Licenseクラスを例にリスコフの置換原則をみてみます。</p>
<p>LicenseクラスにはcalcFee()メソッドがあり、Billingアプリケーションから呼ばれています。またLicenseの派生型であるPersonalLicenseとBusinessLicenseがあります。これらはそれぞれ異なるアルゴリズムを用いてライセンス料を計算します。</p>
<p>このBillingアプリケーションは2つの派生型に依存していないため、リスコフの置換原則を満たしているといえます。どちらの派生型もLicense型に置き換えることができます。</p>
<p>&nbsp;</p>
<h2>インターフェイス分類の原則(ISP: Interface Segregation Principle)</h2>
<p>インターフェイス分類の原則では必要としないお荷物を抱えたものに依存していると、予期せぬトラブルの元になることがあります。</p>
<p><img data-attachment-id="1960" data-permalink="https://mintaku-blog.net/solid-principle/%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2021-04-01-18-24-06/" data-orig-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/04/cfb6f8ef0afdf3b0cc37290db7985fab.png?fit=942%2C246&amp;ssl=1" data-orig-size="942,246" 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="スクリーンショット 2021-04-01 18.24.06" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/04/cfb6f8ef0afdf3b0cc37290db7985fab.png?fit=300%2C78&amp;ssl=1" data-large-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/04/cfb6f8ef0afdf3b0cc37290db7985fab.png?fit=800%2C209&amp;ssl=1" loading="lazy" class="aligncenter size-full wp-image-1960" src="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/04/cfb6f8ef0afdf3b0cc37290db7985fab.png?resize=800%2C209&#038;ssl=1" alt="" width="800" height="209" srcset="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/04/cfb6f8ef0afdf3b0cc37290db7985fab.png?w=942&amp;ssl=1 942w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/04/cfb6f8ef0afdf3b0cc37290db7985fab.png?resize=300%2C78&amp;ssl=1 300w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2021/04/cfb6f8ef0afdf3b0cc37290db7985fab.png?resize=768%2C201&amp;ssl=1 768w" sizes="(max-width: 800px) 100vw, 800px" data-recalc-dims="1" /></p>
<p>本書ではシステムSを担当するアーキテクトが、あるフレームワークFをシステムに導入した場合の例として紹介しています。このフレームワークFの作者は、フレームワークを特定のデータベースDのためだけに作っており、SはFに依存しており、さらにFはDに依存していることになります。</p>
<p>FはDの全ての機能を使っているわけはなく、その使っていない部分が変更されてもFは再度不必要なデプロイをすることになります。さらにDの一部の機能に障害が発生すると、それがFやSの障害の原因になってしまう可能性もあります。</p>
<p>&nbsp;</p>
<h2>依存関係逆転の原則(DIP: Dependency Inversion Principle)</h2>
<p>依存関係逆転の原則は、上位レベルの方針の実装コードは下位レベルの詳細の実装コードに依存すべきではなく、逆に詳細側が方針に依存すべきであるという原則です。</p>
<p>ただ依存関係逆転の原則を考える時にはOSやプラットフォーム周りは気にしないことが多いです。変化しないとみなして、こうした具象への依存は許容することにしています。</p>
<p>依存したくないのは、システム内の変化しやすい具象要素です。開発中のモジュールや頻繁に変更され続けているモジュールがその対象になります。</p>
<p>&nbsp;</p>
<p>参考：<a class="external external_icon" href="https://amzn.to/3rCpe8B" rel="noopener noreferrer nofollow">Clean Architecture 達人に学ぶソフトウェアの構造と設計</a></p>The post <a href="https://mintaku-blog.net/solid-principle/">SOLID原則について整理する</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></content:encoded>
					
					<wfw:commentRss>https://mintaku-blog.net/solid-principle/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1956</post-id>	</item>
	</channel>
</rss>
