# Ktor勉強会 #004 ## 前回(#003)の内容 - HackMD - https://hackmd.io/4_9hMrPUSre3xRxY0cK9pQ?view - Gist(HackMDの内容と同じ) - https://gist.github.com/yoshixmk/8720d48ad19bbf2d5675cde0937ecb92 - repository - https://github.com/yoshixmk/ktor-sample ## 参加者 - うえき - やました - あっきー ## 開催日 2020/05/17(日) 13:00ごろ ## 宿題 * Heroku アカウントつくっておく * [x] うえき * [x] やました * [x] あっきー * Heroku CLIを Terminal で打てるようにしておく * [x] うえき * [x] やました * [x] あっきー ## Agenda ざっくり書くとこんな感じ - heroku - クリーンアーキテクチャ(Users) - クリーンアーキテクチャ化(既存のMemos) + Test(既存のMemosに対して) ### heroku - やましたさんのパート - [資料](https://ktor.io/servers/deploy/hosting/heroku.html) ```bash # heroku-cli/6.15.36 (darwin-x64) node-v9.9.0より大きなら大丈夫かな $ heroku --version heroku/7.41.1 darwin-x64 node-v12.16.2 # 資料の内容を書いてね $ touch app.json # 資料の内容を書いてね $ touch system.properties ``` #### みんなの予想 * `app.json` を作る * `Procfile` をつくる * `system.properties` を作る * `git push master heroku` * 動く!!! ### 実録 * `heroku create ktor-sample-<unique key>` * ブラウザ立ち上がるのでログイン * 名前をktor-sample-xxxとして作成。自由にunique key考えて。 * `git remote` * `heroku` が追加されているはず * prerequirement * `app.json` つくる * `Procfile` つくる * `system.proeprties` つくる * 以下のファイルがあること * `build.gradle` * `settings.gralde` * `gradlew` * `gradle/wrapper/gradle-wrapper.jar` * `gradle/wrapper/gradle-wrapper.properties` * `build.gradle` に `stage` taskを追加する * `git push heroku master` * pushできる、でも動くか不明 * やましたはbuildがしっぱしたけど、他のひとなら通るかも、やってみてください * herokuのログを見る: `heroku logs -t` * herokuの再起動(pushしない時): `heroku restart` #### `app.json` ```=json { "name": "demo-comprendre", "description": "demo-comprendre", "image": "heroku/java", "addons": [ "heroku-postgresql" ] } ``` ##### `build.gralde` ##### やました `stage` を追加 ```=gradle task stage(dependsOn: ['build', 'clean']) build.mustRunAfter clean ``` ##### かわかみ コマンドライン上で、以下のコマンドを実行する事によって、 `gradle task` の `stage` を設定しなくてもよくなった ``` heroku config:set GRADLE_TASK="shadowJar" ``` ##### `Procfile` `jar` の名前は変更してください。 ```=Procfile web: java -jar build/libs/demo-comprendre-0.0.1-SNAPSHOT.jar ``` ##### `system.properties` ``` java.runtime.version=1.8 ``` ##### やましたはこれでうごいた `https://pacific-cove-85277.herokuapp.com/` **ただしまだDBはうまくいかない** ##### うえき `application.conf` にて `port = ${PORT}` を設定してる場合、環境変数で80をいれる。 ![](https://i.imgur.com/IxaGU54.png) ##### DB接続までやる場合 下記をやれば動く(はず) - application.confで定義した環境変数設定を追加する - flyway migrationをする ### [クリーンアーキテクチャ](https://blog.tai2.net/the_clean_architecture.html) ![](https://i.imgur.com/b20JpBJ.png) [Hexagonal Architecture](https://blog.tai2.net/hexagonal_architexture.html) (別名 Ports and Adapters)、[Onion Architecture](http://jeffreypalermo.com/blog/the-onion-architecture-part-1/)、[Screaming Architecture](http://blog.8thlight.com/uncle-bob/2011/09/30/Screaming-Architecture.html)などのアーキテクチャを参考に作られた、綺麗なアーキテクチャ。アーキテクチャでは、一番バズってると思う 原点にサンプルコードがないので、「やってみた系」の記事が目立つ。 - [参考1: いつもの、社内エンジニアが書いたブログ](https://rinoguchi.hatenablog.com/entry/2020/04/24/100000#DI%E4%BE%9D%E5%AD%98%E6%80%A7%E3%81%AE%E6%B3%A8%E5%85%A5%E3%81%AE%E5%B0%8E%E5%85%A5) - [参考2: Qiita repository by kaonash](https://qiita.com/kaonash/items/41f605c10bbff413eabe#infrastructure) - [参考2: Github repository by kaonash](https://github.com/kaonash/ktor-clean-architecture-sample) **参考にした内容も、構成の良さそうなところだけ真似ていくことに。** **鵜呑みにせず納得できなければ、自分の書き方に変えてたりするので、間違ってたら、ごめんなさい** 1. package作る 1. interfaceのやりとり作る 1. class実装 1. DI導入 1. Memosの内容をそれっぽく移動(各自) #### package作る サンプルの、複数形ではなかったところは複数形に。 interface実装では、ixxxという感じの名前にした (図はkaonashさんのもの) ![](https://i.imgur.com/FZAYw8C.png) ```bash= # Ktorの仕組みはこの層だけ $ mkdir -p src/infrastructures/routes $ touch src/infrastructures/routes/.gitkeep # Exposedの仕組みはこの層だけ $ mkdir -p src/databases/repository $ touch src/databases/repository/.gitkeep # serviceを使う。responseデータへ詰め替え。 $ mkdir -p src/interfaces/controllers $ mkdir -p src/interfaces/repository $ touch src/interfaces/controllers/.gitkeep $ touch src/interfaces/repository/.gitkeep # use-cases -> usecases ハイフンあんまり見ないので。 # dtoがこの層にあったが、domainsに配置 $ mkdir -p src/usecases/service $ touch src/usecases/service/.gitkeep # common-libはdomainsに変えました $ mkdir -p src/domains/users $ touch src/domains/users/.gitkeep # https://github.com/nrslib/CleanArchitecture/blob/master/CleanArchitectureSample/Domain/Domain/Users/IUserRepository.cs $ mkdir -p src/domains/irepository $ touch src/domains/irepository/.gitkeep ``` ドメインを追加します - domains.users.User ```kotlin= data class User(val id: Long, val familyName: String, val givenName: String) { val fullName = "$familyName $givenName" } data class Users(val users: List<User>) ``` #### interfaceのやりとり作る 先ほど作ったドメインクラスを使って、interfaceで仮実装していきます ドメインを返す、repositoryのinterface - domains.irepository.IUserRepository ```kotlin= :yoshixmk.domains.irepository package domains.irepository import domains.users.User interface IUserRepository { fun findById(userId: Long): User? } ``` ![](https://i.imgur.com/3JwfguM.png) [出典](https://terasolunaorg.github.io/guideline/public_review/ImplementationAtEachLayer/DomainLayer.html#id10) - usecases.service.UserService - ファイル名は、IUserServiceでも良いが、あとでここにUserService同居させたいため ```kotlin= UserService interface IUserService { fun findById(userId: Long): User } ``` - interfaces.controllers.UserController - usecasesと同様の理由で、ファイル名にはIをつけてない - IN(request data): リクエストデータの形式を決定 - OUT(response data): レスポンスデータの形式を決定 ```kotlin= IUserController package interfaces.controllers interface IUserController { fun getUser(userId: UserId): UserResponse } // IN data class UserId(var id: Long) // OUT data class UserResponse(var userId: Long, var familyName: String, var givenName: String) ``` #### class実装 - databases.repository.UserRepository - domainを返す仮実装です。 ```kotlin= class UserRepository : IUserRepository { override fun findById(userId: Long): User? { // TODO DBからのデータ形式を、domainsに詰め替え。Service層にinject return User(1, "Test", "Taro") } } ``` - usecases.service.UserServiceに実装 - IUserServiceを具象化 ```kotlin= ・・・ class UserService( private val userRepository: IUserRepository ) : IUserService { override fun findById(userId: Long): User { return userRepository.findById(userId) ?: throw IllegalStateException("No User Found for Given Id") } } ・・・ ``` - interfaces.controllers.UserController - IUserControllerを具象化 ```kotlin= UserController ・・・ class UserController(private val userService: IUserService) : IUserController { override fun getUser(userId: UserId): UserResponse { return userService.findById(userId.id).toResponse() } } private fun User.toResponse() = UserResponse(id, familyName, givenName) ``` - ルーティングの実装 - UserRoutes.kt ```kotlin= :Routing.users fun Routing.users() { // TODO injectを用いる val userController: UserController = UserController(UserService(UserRepository())) route("v1/users") { route("{id}") { get { val userId = call.parameters["id"]?.toLongOrNull() ?: return@get call.respond( HttpStatusCode.BadRequest, "Invalid parameter: [${call.parameters["id"]}]" ) call.respond(userController.getUser(UserId(userId))) } } } } ``` - Routes.kt ```kotlin= fun Routing.routes() { // ユーザ機能 users() } ``` - ここまでうまくできていれば、モックで作成したテストユーザの内容が見れます - http://localhost:8080/v1/users/1 ![](https://i.imgur.com/8idL4QE.png) #### DI導入 次に、変更容易性を保つための仕組みとしてDI(Dependency Injection)を取り入れます。 これは依存関係(Dependency)をオブジェクト内のコードに直接記述せず、外部から注入(Inject)することで、組み替え可能にする、デザインパターンです - DIには、[Koin](https://github.com/InsertKoinIO/koin)を使用する - [参考: いつもの(再掲)](https://rinoguchi.hatenablog.com/entry/2020/04/24/100000#DI%E4%BE%9D%E5%AD%98%E6%80%A7%E3%81%AE%E6%B3%A8%E5%85%A5%E3%81%AE%E5%B0%8E%E5%85%A5) - gradle.properties ```groovy= ・・・ koin_version=2.1.5 ``` - build.gradle ```groovy= buildscript { ・・・ dependencies { classpath "org.koin:koin-gradle-plugin:$koin_version" } } apply plugin: 'koin' dependencies { implementation "org.koin:koin-ktor:$koin_version" } ``` - 前述で、UserRoutesに書いてた、これは消してね! ```kotlin= ・・・ val userController: UserController = UserController(UserService(UserRepository())) ・・・ ``` - KoinModules.kt - 今後大きくなるので、最初から外だしで定義 ```kotlin= :KoinModules.kt val koinModules = module(createdAtStart = true) { singleBy<IUserController, UserController>() singleBy<IUserService, UserService>() singleBy<IUserRepository, UserRepository>() } ``` - Application.kt - 追記 ```kotlin= install(org.koin.ktor.ext.Koin) { // あとでtestKoinModules作ります modules(koinModules) } ``` - UserRoutes.kt: IUserControllerへInject - ここは明示的にinjectを書きます ```kotlin= import org.koin.ktor.ext.inject fun Routing.users() { val userController: IUserController by inject() ・・・ } ``` - UserController.kt: IUserServiceへInject - Controller以降はコンストラクタに受け取るだけで良い ```kotlin= interface IUserController { fun getUser(userId: UserId): UserResponse } class UserController(private val userService: IUserService) : IUserController { override fun getUser(userId: UserId): UserResponse { return userService.findById(userId.id).toResponse() } } - UserService.kt: IUserRepositoryへinject - ここもコンストラクタに受け取るだけで良い ```kotlin= interface IUserService { fun findById(userId: Long): User } class UserService(private val userRepository: IUserRepository) : IUserService { } ``` - ここまで修正して、再度テストユーザの内容が見れることを確認します - http://localhost:8080/v1/users/1 ![](https://i.imgur.com/8idL4QE.png) - ここまでの修正内容 https://github.com/yoshixmk/ktor-sample/pull/3 #### Memosの内容に、クリーンアーキテクチャを 各自で、Usersを参考に書き換えをやりましょう - interfaceの名前は、下記のようにする。 - フレームワークやエンジニアによって、やり方に差異がある気がしてます。[Spring framework の CrudRepository](https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/repository/CrudRepository.html)などは、save()としてcreateもしくはupdateを隠蔽しています。 - 個人的には、上記の`CrudRepository`のようなことをすると、エンドポイントで分割されてたCRUDがくっついたり離れたりするので、余計なコードが増えてしまうので、意図的にRepositoryをUseCase層寄りに自由度を高めます - 逆の視点からすると、同じ理由で、クエリの発行も、ソートまでかけた方がコストが安いなどあるため、UseCase層でソート処理などはしないことにします - MemoRepository.kt ```kotlin= IMemoRepository.kt interface IMemoRepository { fun findById(id: Int): Memo? fun findAll(): List<Memo> fun findAllSortedById(): List<Memo> fun create(memo: Memo): Memo fun update(memo: Memo): Memo? fun deleteById(id: Int): Long } ``` - MemoService.kt ```kotlin= interface IMemoService { fun findById(userId: Int): Memo? fun findAllSortedById(): List<Memo> fun create(subject: String): Memo fun update(id: Int, subject: String): Memo? fun deleteById(id: Int): Long } ``` - IMemoController.kt ```kotlin= interface IMemoController { fun getMemo(memoId: MemoId): Memo fun getMemos(): List<Memo> fun postMemo(memo: MemoContent): Int fun putMemo(memo: Memo): Memo fun deleteMemo(memoId: MemoId): Unit } // IN data class MemoId(val id: Int) // IN data class MemoContent(val subject: String) // IN or OUT data class Memo(val memo_id: Int, val subject: String) ``` - ここまでのPR - https://github.com/yoshixmk/ktor-sample/pull/4/files - 厳密には、少し足りないです。 == Memos == ## 次回の開催予定 - 2020/05/23(土) 13:00 - 6(React) : 4(Ktor) ### 次回、「テスト part2」 - 今日できなかったとこ ### 次回、「Locations」 - Routingにプラスする感じでできそうな気がしたのでやってみる ### Front作る - React with TypeScript - create-react-apps - react-dev-server - サンプル作る? - ktor講習会のmemoをブラウザからいじれるようなかんじの物をつくる ### 本日の成果 * うえき * やました * [20200517](https://github.com/jamashita/demo-comprendre/releases/tag/20200517) * あっきー