├── .gitignore ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src ├── main └── kotlin │ └── com │ └── karrot │ └── example │ ├── const │ └── time.kt │ ├── entity │ ├── account │ │ └── User.kt │ ├── catalog │ │ └── Product.kt │ ├── order │ │ ├── Order.kt │ │ └── OrderItem.kt │ ├── shipment │ │ └── Delivery.kt │ └── store │ │ └── Store.kt │ ├── repository │ ├── account │ │ ├── UserAsyncRepository.kt │ │ ├── UserRepositoryBase.kt │ │ ├── UserRxRepository.kt │ │ └── UserSyncRepository.kt │ ├── catalog │ │ ├── ProductAsyncRepository.kt │ │ ├── ProductReactorRepository.kt │ │ ├── ProductRepositoryBase.kt │ │ └── ProductSyncRepository.kt │ ├── order │ │ ├── OrderAsyncRepository.kt │ │ ├── OrderFutureRepository.kt │ │ └── OrderSyncRepository.kt │ ├── shipment │ │ ├── AddressAsyncRepository.kt │ │ ├── AddressReactiveRepository.kt │ │ ├── AddressRepositoryBase.kt │ │ ├── AddressSyncRepository.kt │ │ └── LastItemSubscriber.kt │ └── store │ │ ├── StoreAsyncRepository.kt │ │ ├── StoreMutinyRepository.kt │ │ ├── StoreRepositoryBase.kt │ │ └── StoreSyncRepository.kt │ ├── usecase │ └── order │ │ ├── CreateOrderUseCaseBase.kt │ │ ├── async │ │ ├── CreateOrderAsyncStateMachine1UseCase.kt │ │ ├── CreateOrderAsyncStateMachine2UseCase.kt │ │ └── CreateOrderAsyncStateMachine3UseCase.kt │ │ ├── coroutine │ │ ├── CreateOrderAsyncCoroutineUseCase.kt │ │ ├── CreateOrderCoroutineUseCase.kt │ │ └── CreateOrderErrorCoroutineUseCase.kt │ │ ├── reactor │ │ ├── CreateOrderReactorSubscribeUseCase.kt │ │ └── CreateOrderReactorUseCase.kt │ │ └── sync │ │ ├── CreateOrderSyncStateMachineUseCase.kt │ │ └── CreateOrderSyncUseCase.kt │ ├── util │ └── coroutine.kt │ └── vo │ ├── Address.kt │ ├── Money.kt │ └── MoneyCurrency.kt └── test └── kotlin └── com └── karrot └── commerce └── usecase └── order ├── async ├── CreateOrderAsyncStateMachine1UseCaseTests.kt ├── CreateOrderAsyncStateMachine2UseCaseTests.kt └── CreateOrderAsyncStateMachine3UseCaseTests.kt ├── coroutine └── CreateOrderCoroutineUseCaseTests.kt ├── reactor ├── CreateOrderReactorSubscribeUseCaseTests.kt └── CreateOrderReactorUseCaseTests.kt └── sync ├── CreateOrderSyncStateMachineUseCaseTests.kt └── CreateOrderSyncUseCaseTests.kt /.gitignore: -------------------------------------------------------------------------------- 1 | ### Java template 2 | # Compiled class file 3 | *.class 4 | 5 | # Log file 6 | *.log 7 | 8 | # BlueJ files 9 | *.ctxt 10 | 11 | # Mobile Tools for Java (J2ME) 12 | .mtj.tmp/ 13 | 14 | # Package Files # 15 | *.jar 16 | *.war 17 | *.nar 18 | *.ear 19 | *.zip 20 | *.tar.gz 21 | *.rar 22 | 23 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 24 | hs_err_pid* 25 | 26 | ### JetBrains template 27 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 28 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 29 | 30 | # User-specific stuff 31 | .idea/**/workspace.xml 32 | .idea/**/tasks.xml 33 | .idea/**/usage.statistics.xml 34 | .idea/**/dictionaries 35 | .idea/**/shelf 36 | 37 | # Generated files 38 | .idea/**/contentModel.xml 39 | 40 | # Sensitive or high-churn files 41 | .idea/**/dataSources/ 42 | .idea/**/dataSources.ids 43 | .idea/**/dataSources.local.xml 44 | .idea/**/sqlDataSources.xml 45 | .idea/**/dynamic.xml 46 | .idea/**/uiDesigner.xml 47 | .idea/**/dbnavigator.xml 48 | 49 | # Gradle 50 | .idea/**/gradle.xml 51 | .idea/**/libraries 52 | 53 | # Gradle and Maven with auto-import 54 | # When using Gradle or Maven with auto-import, you should exclude module files, 55 | # since they will be recreated, and may cause churn. Uncomment if using 56 | # auto-import. 57 | # .idea/artifacts 58 | # .idea/compiler.xml 59 | # .idea/jarRepositories.xml 60 | # .idea/modules.xml 61 | # .idea/*.iml 62 | # .idea/modules 63 | # *.iml 64 | # *.ipr 65 | 66 | # CMake 67 | cmake-build-*/ 68 | 69 | # Mongo Explorer plugin 70 | .idea/**/mongoSettings.xml 71 | 72 | # File-based project format 73 | *.iws 74 | 75 | # IntelliJ 76 | out/ 77 | 78 | # mpeltonen/sbt-idea plugin 79 | .idea_modules/ 80 | 81 | # JIRA plugin 82 | atlassian-ide-plugin.xml 83 | 84 | # Cursive Clojure plugin 85 | .idea/replstate.xml 86 | 87 | # Crashlytics plugin (for Android Studio and IntelliJ) 88 | com_crashlytics_export_strings.xml 89 | crashlytics.properties 90 | crashlytics-build.properties 91 | fabric.properties 92 | 93 | # Editor-based Rest Client 94 | .idea/httpRequests 95 | 96 | # Android studio 3.1+ serialized cache file 97 | .idea/caches/build_file_checksums.ser 98 | 99 | ### Gradle template 100 | .gradle 101 | **/build/ 102 | !src/**/build/ 103 | 104 | # Ignore Gradle GUI config 105 | gradle-app.setting 106 | 107 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 108 | !gradle-wrapper.jar 109 | 110 | # Cache of project 111 | .gradletasknamecache 112 | 113 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 114 | # gradle/wrapper/gradle-wrapper.properties 115 | 116 | ### macOS template 117 | # General 118 | .DS_Store 119 | .AppleDouble 120 | .LSOverride 121 | 122 | # Icon must end with two \r 123 | Icon 124 | 125 | # Thumbnails 126 | ._* 127 | 128 | # Files that might appear in the root of a volume 129 | .DocumentRevisions-V100 130 | .fseventsd 131 | .Spotlight-V100 132 | .TemporaryItems 133 | .Trashes 134 | .VolumeIcon.icns 135 | .com.apple.timemachine.donotpresent 136 | 137 | # Directories potentially created on remote AFP share 138 | .AppleDB 139 | .AppleDesktop 140 | Network Trash Folder 141 | Temporary Items 142 | .apdisk 143 | 144 | .idea 145 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("jvm") version "1.6.10" 3 | } 4 | 5 | group = "com.karrot.commerce" 6 | version = "1.0-SNAPSHOT" 7 | 8 | repositories { 9 | mavenCentral() 10 | } 11 | 12 | dependencies { 13 | // kotlin 14 | implementation(kotlin("stdlib")) 15 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.5.2") 16 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.5.2") 17 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactive:1.5.2") 18 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk9:1.5.2") 19 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-rx3:1.5.2") 20 | implementation("io.smallrye.reactive:mutiny-kotlin:1.2.0") 21 | 22 | // reactor 23 | implementation("io.projectreactor:reactor-core:3.4.13") 24 | implementation("io.projectreactor.addons:reactor-adapter:3.4.5") 25 | 26 | // rxjava 27 | implementation("io.reactivex.rxjava3:rxjava:3.1.3") 28 | 29 | // mutiny 30 | implementation("io.smallrye.reactive:mutiny:1.2.0") 31 | implementation("io.smallrye.reactive:mutiny-reactor:1.2.0") 32 | 33 | // faker 34 | implementation("com.github.javafaker:javafaker:1.0.2") 35 | 36 | /// test 37 | // junit 38 | testImplementation("org.junit.jupiter:junit-jupiter:5.8.2") 39 | 40 | // mockk 41 | testImplementation("io.mockk:mockk:1.12.1") 42 | 43 | // common-lang 44 | implementation("org.apache.commons:commons-lang3:3.12.0") 45 | 46 | // coroutine 47 | testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.2") 48 | 49 | } 50 | 51 | tasks.withType { 52 | useJUnitPlatform() 53 | testLogging { 54 | events("passed", "skipped", "failed") 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaaon/kotlin-coroutines-examples/d94d2bed28f10cd07344602172093bf1c977c87b/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MSYS* | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "kotlin-coroutines-examples" 2 | 3 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karrot/example/const/time.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.example.const 2 | 3 | const val TIME_DELAY_MS = 100L 4 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karrot/example/entity/account/User.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.example.entity.account 2 | 3 | class User( 4 | val id: String, 5 | val name: String, 6 | ) { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karrot/example/entity/catalog/Product.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.example.entity.catalog 2 | 3 | import com.karrot.example.vo.Money 4 | 5 | class Product( 6 | val id: String, 7 | val name: String, 8 | val price: Money, 9 | val storeId: String, 10 | ) { 11 | 12 | override fun equals(other: Any?): Boolean { 13 | if (this === other) return true 14 | if (javaClass != other?.javaClass) return false 15 | 16 | other as Product 17 | 18 | if (id != other.id) return false 19 | if (name != other.name) return false 20 | if (price != other.price) return false 21 | if (storeId != other.storeId) return false 22 | 23 | return true 24 | } 25 | 26 | override fun hashCode(): Int { 27 | var result = id.hashCode() 28 | result = 31 * result + name.hashCode() 29 | result = 31 * result + price.hashCode() 30 | result = 31 * result + storeId.hashCode() 31 | return result 32 | } 33 | 34 | override fun toString(): String { 35 | return "Product(id='$id', name='$name', price=$price, storeId='$storeId')" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karrot/example/entity/order/Order.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.example.entity.order 2 | 3 | import com.karrot.example.entity.account.User 4 | import com.karrot.example.vo.Address 5 | 6 | class Order( 7 | val buyer: User, 8 | val items: List, 9 | val address: Address, 10 | ) { 11 | 12 | override fun equals(other: Any?): Boolean { 13 | if (this === other) return true 14 | if (javaClass != other?.javaClass) return false 15 | 16 | other as Order 17 | 18 | if (buyer != other.buyer) return false 19 | if (items != other.items) return false 20 | if (address != other.address) return false 21 | 22 | return true 23 | } 24 | 25 | override fun hashCode(): Int { 26 | var result = buyer.hashCode() 27 | result = 31 * result + items.hashCode() 28 | result = 31 * result + address.hashCode() 29 | return result 30 | } 31 | 32 | override fun toString(): String { 33 | return "Order(buyer=$buyer, items=$items, address=$address)" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karrot/example/entity/order/OrderItem.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.example.entity.order 2 | 3 | import com.karrot.example.entity.catalog.Product 4 | import com.karrot.example.entity.store.Store 5 | 6 | class OrderItem( 7 | val product: Product, 8 | val store: Store, 9 | ) { 10 | override fun equals(other: Any?): Boolean { 11 | if (this === other) return true 12 | if (javaClass != other?.javaClass) return false 13 | 14 | other as OrderItem 15 | 16 | if (product != other.product) return false 17 | 18 | return true 19 | } 20 | 21 | override fun hashCode(): Int { 22 | return product.hashCode() 23 | } 24 | 25 | override fun toString(): String { 26 | return "OrderItem(product=$product)" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karrot/example/entity/shipment/Delivery.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.example.entity.shipment 2 | 3 | class Delivery { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karrot/example/entity/store/Store.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.example.entity.store 2 | 3 | class Store( 4 | val id: String, 5 | val name: String, 6 | ) { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karrot/example/repository/account/UserAsyncRepository.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.example.repository.account 2 | 3 | import com.karrot.example.entity.account.User 4 | import io.reactivex.rxjava3.core.Maybe 5 | 6 | interface UserAsyncRepository { 7 | fun findUserByIdAsMaybe(userId: String): Maybe 8 | } 9 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karrot/example/repository/account/UserRepositoryBase.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.example.repository.account 2 | 3 | import com.github.javafaker.Faker 4 | import com.karrot.example.entity.account.User 5 | 6 | open class UserRepositoryBase { 7 | private val faker = Faker() 8 | 9 | internal fun prepareUser(userId: String): User { 10 | val name = faker.name().fullName() 11 | 12 | return User( 13 | id = userId, 14 | name = name, 15 | ) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karrot/example/repository/account/UserRxRepository.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.example.repository.account 2 | 3 | import com.karrot.example.const.TIME_DELAY_MS 4 | import com.karrot.example.entity.account.User 5 | import io.reactivex.rxjava3.core.Maybe 6 | import java.util.concurrent.TimeUnit 7 | 8 | class UserRxRepository : UserRepositoryBase(), UserAsyncRepository { 9 | override fun findUserByIdAsMaybe(userId: String): Maybe { 10 | val user = prepareUser(userId) 11 | return Maybe.just(user) 12 | .delay(TIME_DELAY_MS, TimeUnit.MILLISECONDS) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karrot/example/repository/account/UserSyncRepository.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.example.repository.account 2 | 3 | import com.karrot.example.const.TIME_DELAY_MS 4 | import com.karrot.example.entity.account.User 5 | 6 | class UserSyncRepository : UserRepositoryBase() { 7 | fun findUserByIdSync(userId: String): User { 8 | val user = prepareUser(userId) 9 | Thread.sleep(TIME_DELAY_MS) 10 | return user 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karrot/example/repository/catalog/ProductAsyncRepository.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.example.repository.catalog 2 | 3 | import com.karrot.example.entity.catalog.Product 4 | import reactor.core.publisher.Flux 5 | 6 | interface ProductAsyncRepository { 7 | fun findAllProductsByIdsAsFlux(productIds: List): Flux 8 | } 9 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karrot/example/repository/catalog/ProductReactorRepository.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.example.repository.catalog 2 | 3 | import com.karrot.example.const.TIME_DELAY_MS 4 | import com.karrot.example.entity.catalog.Product 5 | import reactor.core.publisher.Flux 6 | import java.time.Duration 7 | 8 | class ProductReactorRepository : ProductRepositoryBase(), ProductAsyncRepository { 9 | override fun findAllProductsByIdsAsFlux(productIds: List): Flux { 10 | val products = productIds.map { prepareProduct(it) } 11 | return Flux.fromIterable(products) 12 | .delayElements(Duration.ofMillis(TIME_DELAY_MS)) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karrot/example/repository/catalog/ProductRepositoryBase.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.example.repository.catalog 2 | 3 | import com.github.javafaker.Faker 4 | import com.karrot.example.entity.catalog.Product 5 | import com.karrot.example.vo.Money 6 | import com.karrot.example.vo.MoneyCurrency 7 | import java.util.* 8 | import kotlin.random.Random 9 | 10 | open class ProductRepositoryBase { 11 | private val faker = Faker() 12 | 13 | internal fun prepareProduct(productId: String): Product { 14 | val randomPriceAmount = Random.nextLong(1000, 1000000) 15 | val randomStoreId = UUID.randomUUID().toString() 16 | 17 | return Product( 18 | id = productId, 19 | storeId = randomStoreId, 20 | name = faker.commerce().productName(), 21 | price = Money(MoneyCurrency.WON, randomPriceAmount) 22 | ) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karrot/example/repository/catalog/ProductSyncRepository.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.example.repository.catalog 2 | 3 | import com.karrot.example.const.TIME_DELAY_MS 4 | import com.karrot.example.entity.catalog.Product 5 | 6 | class ProductSyncRepository : ProductRepositoryBase() { 7 | fun findAllProductsByIdsSync(productIds: List): List { 8 | Thread.sleep(TIME_DELAY_MS) 9 | return productIds.map { prepareProduct(it) } 10 | } 11 | 12 | fun findProductByIdSync(productId: String): Product { 13 | Thread.sleep(TIME_DELAY_MS/5) 14 | return prepareProduct(productId) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karrot/example/repository/order/OrderAsyncRepository.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.example.repository.order 2 | 3 | import com.karrot.example.entity.account.User 4 | import com.karrot.example.entity.catalog.Product 5 | import com.karrot.example.entity.order.Order 6 | import com.karrot.example.entity.store.Store 7 | import com.karrot.example.vo.Address 8 | import java.util.concurrent.CompletableFuture 9 | 10 | interface OrderAsyncRepository { 11 | fun createOrderAsFuture( 12 | buyer: User, 13 | products: List, 14 | stores: List, 15 | address: Address, 16 | ): CompletableFuture 17 | } 18 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karrot/example/repository/order/OrderFutureRepository.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.example.repository.order 2 | 3 | import com.karrot.example.const.TIME_DELAY_MS 4 | import com.karrot.example.entity.account.User 5 | import com.karrot.example.entity.catalog.Product 6 | import com.karrot.example.entity.order.Order 7 | import com.karrot.example.entity.order.OrderItem 8 | import com.karrot.example.entity.store.Store 9 | import com.karrot.example.vo.Address 10 | import java.util.concurrent.CompletableFuture 11 | import java.util.concurrent.TimeUnit 12 | 13 | class OrderFutureRepository : OrderAsyncRepository { 14 | override fun createOrderAsFuture( 15 | buyer: User, 16 | products: List, 17 | stores: List, 18 | address: Address, 19 | ): CompletableFuture { 20 | val orderItems = products.zip(stores).map { (product, store) -> 21 | OrderItem(product, store) 22 | } 23 | 24 | val createdOrder = Order( 25 | buyer = buyer, 26 | items = orderItems, 27 | address = address, 28 | ) 29 | 30 | val delayed = CompletableFuture.delayedExecutor(TIME_DELAY_MS, TimeUnit.MILLISECONDS) 31 | return CompletableFuture.supplyAsync({ createdOrder }, delayed) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karrot/example/repository/order/OrderSyncRepository.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.example.repository.order 2 | 3 | import com.karrot.example.const.TIME_DELAY_MS 4 | import com.karrot.example.entity.account.User 5 | import com.karrot.example.entity.catalog.Product 6 | import com.karrot.example.entity.order.Order 7 | import com.karrot.example.entity.order.OrderItem 8 | import com.karrot.example.entity.store.Store 9 | import com.karrot.example.vo.Address 10 | 11 | class OrderSyncRepository { 12 | fun createOrderSync( 13 | buyer: User, 14 | products: List, 15 | stores: List, 16 | address: Address, 17 | ): Order { 18 | val orderItems = products.zip(stores).map { (product, store) -> 19 | OrderItem(product, store) 20 | } 21 | 22 | val createdOrder = Order( 23 | buyer = buyer, 24 | items = orderItems, 25 | address = address, 26 | ) 27 | 28 | Thread.sleep(TIME_DELAY_MS) 29 | return createdOrder 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karrot/example/repository/shipment/AddressAsyncRepository.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.example.repository.shipment 2 | 3 | import com.karrot.example.entity.account.User 4 | import com.karrot.example.vo.Address 5 | import java.util.concurrent.Flow 6 | 7 | interface AddressAsyncRepository { 8 | fun findAddressByUserAsPublisher(user: User): Flow.Publisher
9 | } 10 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karrot/example/repository/shipment/AddressReactiveRepository.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.example.repository.shipment 2 | 3 | import com.karrot.example.const.TIME_DELAY_MS 4 | import com.karrot.example.entity.account.User 5 | import com.karrot.example.vo.Address 6 | import java.util.concurrent.Flow 7 | 8 | class AddressReactiveRepository : AddressRepositoryBase(), AddressAsyncRepository { 9 | override fun findAddressByUserAsPublisher(user: User): Flow.Publisher
{ 10 | val addressIterator = prepareAddresses().iterator() 11 | 12 | return Flow.Publisher
{ subscriber -> 13 | subscriber.onSubscribe(object : Flow.Subscription { 14 | override fun request(n: Long) { 15 | Thread.sleep(TIME_DELAY_MS) 16 | var cnt = n 17 | while (cnt-- > 0) { 18 | if (addressIterator.hasNext()) { 19 | subscriber.onNext(addressIterator.next()) 20 | } else { 21 | subscriber.onComplete() 22 | break 23 | } 24 | } 25 | } 26 | 27 | override fun cancel() { 28 | // do nothing 29 | } 30 | }) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karrot/example/repository/shipment/AddressRepositoryBase.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.example.repository.shipment 2 | 3 | import com.karrot.example.vo.Address 4 | 5 | open class AddressRepositoryBase { 6 | internal fun prepareAddresses(): List
{ 7 | return listOf( 8 | Address( 9 | roadNameAddress = "서울특별시 중구 세종대로 110", 10 | detailAddress = "1층", 11 | ), 12 | Address( 13 | roadNameAddress = "서울특별시 서초구 강남대로 465", 14 | detailAddress = "10층 B", 15 | ), 16 | Address( 17 | roadNameAddress = "서울특별시 구로구 디지털로30길 28", 18 | detailAddress = "609호", 19 | ) 20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karrot/example/repository/shipment/AddressSyncRepository.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.example.repository.shipment 2 | 3 | import com.karrot.example.const.TIME_DELAY_MS 4 | import com.karrot.example.entity.account.User 5 | import com.karrot.example.vo.Address 6 | 7 | class AddressSyncRepository : AddressRepositoryBase() { 8 | @Suppress 9 | fun findAddressByUserSync(@Suppress("UNUSED_PARAMETER") user: User): List
{ 10 | val address = prepareAddresses() 11 | Thread.sleep(TIME_DELAY_MS) 12 | return address 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karrot/example/repository/shipment/LastItemSubscriber.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.example.repository.shipment 2 | 3 | import java.util.concurrent.Flow 4 | 5 | class LastItemSubscriber( 6 | private val callback: (result: T) -> Unit 7 | ) : Flow.Subscriber { 8 | lateinit var s: Flow.Subscription 9 | var result: T? = null 10 | 11 | override fun onNext(t: T) { 12 | result = t 13 | this.s.request(1) 14 | } 15 | 16 | override fun onSubscribe(s: Flow.Subscription) { 17 | this.s = s 18 | this.s.request(1) 19 | } 20 | 21 | override fun onError(t: Throwable) { 22 | // do nothing 23 | } 24 | 25 | override fun onComplete() { 26 | checkNotNull(result) 27 | callback.invoke(result!!) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karrot/example/repository/store/StoreAsyncRepository.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.example.repository.store 2 | 3 | import com.karrot.example.entity.catalog.Product 4 | import com.karrot.example.entity.store.Store 5 | import io.smallrye.mutiny.Multi 6 | 7 | interface StoreAsyncRepository { 8 | fun findStoresByProductsAsMulti(products: List): Multi 9 | } 10 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karrot/example/repository/store/StoreMutinyRepository.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.example.repository.store 2 | 3 | import com.karrot.example.entity.catalog.Product 4 | import com.karrot.example.entity.store.Store 5 | import io.smallrye.mutiny.Multi 6 | 7 | class StoreMutinyRepository : StoreRepositoryBase(), StoreAsyncRepository { 8 | override fun findStoresByProductsAsMulti(products: List): Multi { 9 | return Multi.createFrom().iterable( 10 | products.map { prepareStore(it) } 11 | ) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karrot/example/repository/store/StoreRepositoryBase.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.example.repository.store 2 | 3 | import com.github.javafaker.Faker 4 | import com.karrot.example.entity.catalog.Product 5 | import com.karrot.example.entity.store.Store 6 | 7 | open class StoreRepositoryBase { 8 | private val faker = Faker() 9 | 10 | internal fun prepareStore(product: Product): Store { 11 | val randomStoreName = faker.company().name() 12 | 13 | return Store( 14 | id = product.storeId, 15 | name = randomStoreName, 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karrot/example/repository/store/StoreSyncRepository.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.example.repository.store 2 | 3 | import com.karrot.example.entity.catalog.Product 4 | import com.karrot.example.entity.store.Store 5 | 6 | class StoreSyncRepository : StoreRepositoryBase() { 7 | fun findStoresByProductsSync(products: List): List { 8 | return products.map { prepareStore(it) } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karrot/example/usecase/order/CreateOrderUseCaseBase.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.example.usecase.order 2 | 3 | import com.karrot.example.vo.Address 4 | 5 | open class CreateOrderUseCaseBase { 6 | fun checkValidRegion(address: Address) = true 7 | } 8 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karrot/example/usecase/order/async/CreateOrderAsyncStateMachine1UseCase.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.example.usecase.order.async 2 | 3 | import com.karrot.example.entity.account.User 4 | import com.karrot.example.entity.catalog.Product 5 | import com.karrot.example.entity.order.Order 6 | import com.karrot.example.entity.store.Store 7 | import com.karrot.example.repository.account.UserAsyncRepository 8 | import com.karrot.example.repository.catalog.ProductAsyncRepository 9 | import com.karrot.example.repository.order.OrderAsyncRepository 10 | import com.karrot.example.repository.shipment.AddressAsyncRepository 11 | import com.karrot.example.repository.shipment.LastItemSubscriber 12 | import com.karrot.example.repository.store.StoreAsyncRepository 13 | import com.karrot.example.usecase.order.CreateOrderUseCaseBase 14 | import com.karrot.example.vo.Address 15 | 16 | class CreateOrderAsyncStateMachine1UseCase( 17 | private val userRepository: UserAsyncRepository, 18 | private val addressRepository: AddressAsyncRepository, 19 | private val productRepository: ProductAsyncRepository, 20 | private val storeRepository: StoreAsyncRepository, 21 | private val orderRepository: OrderAsyncRepository, 22 | ) : CreateOrderUseCaseBase() { 23 | data class InputValues( 24 | val userId: String, 25 | val productIds: List, 26 | ) 27 | 28 | class SharedData { 29 | var label: Int = 0 30 | lateinit var result: Any 31 | lateinit var buyer: User 32 | lateinit var address: Address 33 | lateinit var products: List 34 | lateinit var stores: List 35 | lateinit var order: Order 36 | lateinit var resumeWith: (result: Any) -> Unit 37 | } 38 | 39 | fun execute( 40 | inputValues: InputValues, 41 | cb: (order: Order) -> Unit, 42 | sharedData: SharedData? = null, 43 | ) { 44 | val (userId, productIds) = inputValues 45 | 46 | val that = this 47 | val shared = sharedData ?: SharedData().apply { 48 | resumeWith = fun(result: Any) { 49 | this.result = result 50 | that.execute(inputValues, cb, this) 51 | } 52 | } 53 | 54 | when (shared.label) { 55 | 0 -> { 56 | shared.label = 1 57 | userRepository.findUserByIdAsMaybe(userId) 58 | .subscribe { user -> 59 | shared.resumeWith(user) 60 | } 61 | } 62 | 1 -> { 63 | shared.label = 2 64 | shared.buyer = shared.result as User 65 | addressRepository.findAddressByUserAsPublisher(shared.buyer) 66 | .subscribe(LastItemSubscriber { address -> 67 | shared.resumeWith(address) 68 | }) 69 | } 70 | 2 -> { 71 | shared.label = 3 72 | shared.address = shared.result as Address 73 | checkValidRegion(shared.address) 74 | productRepository.findAllProductsByIdsAsFlux(productIds) 75 | .collectList() 76 | .subscribe { products -> 77 | shared.resumeWith(products) 78 | } 79 | } 80 | 3 -> { 81 | shared.label = 4 82 | shared.products = shared.result as List 83 | check(shared.products.isNotEmpty()) 84 | storeRepository.findStoresByProductsAsMulti(shared.products) 85 | .collect().asList() 86 | .subscribe().with { stores -> 87 | shared.resumeWith(stores) 88 | } 89 | } 90 | 4 -> { 91 | shared.label = 5 92 | shared.stores = shared.result as List 93 | check(shared.stores.isNotEmpty()) 94 | orderRepository.createOrderAsFuture( 95 | shared.buyer, shared.products, shared.stores, shared.address 96 | ).whenComplete { order, _ -> 97 | shared.resumeWith(order) 98 | } 99 | } 100 | 5 -> { 101 | shared.order = shared.result as Order 102 | cb(shared.order) 103 | } 104 | else -> throw IllegalAccessException() 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karrot/example/usecase/order/async/CreateOrderAsyncStateMachine2UseCase.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.example.usecase.order.async 2 | 3 | import com.karrot.example.entity.account.User 4 | import com.karrot.example.entity.catalog.Product 5 | import com.karrot.example.entity.order.Order 6 | import com.karrot.example.entity.store.Store 7 | import com.karrot.example.repository.account.UserAsyncRepository 8 | import com.karrot.example.repository.catalog.ProductAsyncRepository 9 | import com.karrot.example.repository.order.OrderAsyncRepository 10 | import com.karrot.example.repository.shipment.AddressAsyncRepository 11 | import com.karrot.example.repository.shipment.LastItemSubscriber 12 | import com.karrot.example.repository.store.StoreAsyncRepository 13 | import com.karrot.example.usecase.order.CreateOrderUseCaseBase 14 | import com.karrot.example.vo.Address 15 | import kotlin.coroutines.Continuation 16 | import kotlin.coroutines.CoroutineContext 17 | 18 | class CreateOrderAsyncStateMachine2UseCase( 19 | private val userRepository: UserAsyncRepository, 20 | private val addressRepository: AddressAsyncRepository, 21 | private val productRepository: ProductAsyncRepository, 22 | private val storeRepository: StoreAsyncRepository, 23 | private val orderRepository: OrderAsyncRepository, 24 | ) : CreateOrderUseCaseBase() { 25 | data class InputValues( 26 | val userId: String, 27 | val productIds: List, 28 | ) 29 | 30 | class SharedDataContinuation( 31 | val completion: Continuation, 32 | ) : Continuation { 33 | var label: Int = 0 34 | lateinit var result: Any 35 | lateinit var buyer: User 36 | lateinit var address: Address 37 | lateinit var products: List 38 | lateinit var stores: List 39 | lateinit var order: Order 40 | lateinit var resume: () -> Unit 41 | 42 | override val context: CoroutineContext = completion.context 43 | override fun resumeWith(result: Result) { 44 | this.result = result 45 | this.resume() 46 | } 47 | } 48 | 49 | fun execute(inputValues: InputValues, completion: Continuation) { 50 | val (userId, productIds) = inputValues 51 | 52 | val that = this 53 | val cont = completion as? SharedDataContinuation 54 | ?: SharedDataContinuation(completion).apply { 55 | resume = fun() { 56 | // recursive self 57 | that.execute(inputValues, this) 58 | } 59 | } 60 | 61 | when (cont.label) { 62 | 0 -> { 63 | cont.label = 1 64 | userRepository.findUserByIdAsMaybe(userId) 65 | .subscribe { user -> 66 | cont.resumeWith(Result.success(user)) 67 | } 68 | } 69 | 1 -> { 70 | cont.label = 2 71 | cont.buyer = (cont.result as Result).getOrThrow() 72 | addressRepository.findAddressByUserAsPublisher(cont.buyer) 73 | .subscribe(LastItemSubscriber { address -> 74 | cont.resumeWith(Result.success(address)) 75 | }) 76 | } 77 | 2 -> { 78 | cont.label = 3 79 | cont.address = (cont.result as Result
).getOrThrow() 80 | checkValidRegion(cont.address) 81 | productRepository.findAllProductsByIdsAsFlux(productIds) 82 | .collectList() 83 | .subscribe { products -> 84 | cont.resumeWith(Result.success(products)) 85 | } 86 | } 87 | 3 -> { 88 | cont.label = 4 89 | cont.products = (cont.result as Result>).getOrThrow() 90 | check(cont.products.isNotEmpty()) 91 | storeRepository.findStoresByProductsAsMulti(cont.products) 92 | .collect().asList() 93 | .subscribe().with { stores -> 94 | cont.resumeWith(Result.success(stores)) 95 | } 96 | } 97 | 4 -> { 98 | cont.label = 5 99 | cont.stores = (cont.result as Result>).getOrThrow() 100 | check(cont.stores.isNotEmpty()) 101 | orderRepository.createOrderAsFuture( 102 | cont.buyer, cont.products, cont.stores, cont.address 103 | ).whenComplete { order, _ -> 104 | cont.resumeWith(Result.success(order)) 105 | } 106 | } 107 | 5 -> { 108 | cont.order = (cont.result as Result).getOrThrow() 109 | cont.completion.resumeWith(Result.success(cont.order)) 110 | } 111 | else -> throw IllegalAccessException() 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karrot/example/usecase/order/async/CreateOrderAsyncStateMachine3UseCase.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.example.usecase.order.async 2 | 3 | import com.karrot.example.entity.account.User 4 | import com.karrot.example.entity.catalog.Product 5 | import com.karrot.example.entity.order.Order 6 | import com.karrot.example.entity.store.Store 7 | import com.karrot.example.repository.account.UserAsyncRepository 8 | import com.karrot.example.repository.catalog.ProductAsyncRepository 9 | import com.karrot.example.repository.order.OrderAsyncRepository 10 | import com.karrot.example.repository.shipment.AddressAsyncRepository 11 | import com.karrot.example.repository.store.StoreAsyncRepository 12 | import com.karrot.example.usecase.order.CreateOrderUseCaseBase 13 | import com.karrot.example.util.awaitLast 14 | import com.karrot.example.util.awaitSingle 15 | import com.karrot.example.util.toList 16 | import com.karrot.example.vo.Address 17 | import kotlin.coroutines.Continuation 18 | import kotlin.coroutines.CoroutineContext 19 | 20 | class CreateOrderAsyncStateMachine3UseCase( 21 | private val userRepository: UserAsyncRepository, 22 | private val addressRepository: AddressAsyncRepository, 23 | private val productRepository: ProductAsyncRepository, 24 | private val storeRepository: StoreAsyncRepository, 25 | private val orderRepository: OrderAsyncRepository, 26 | ) : CreateOrderUseCaseBase() { 27 | data class InputValues( 28 | val userId: String, 29 | val productIds: List, 30 | ) 31 | 32 | class SharedDataContinuation( 33 | private val continuation: Continuation, 34 | ) : Continuation { 35 | var label: Int = 0 36 | lateinit var result: Any 37 | lateinit var buyer: User 38 | lateinit var address: Address 39 | lateinit var products: List 40 | lateinit var stores: List 41 | lateinit var order: Order 42 | lateinit var resume: () -> Unit 43 | 44 | override val context: CoroutineContext = continuation.context 45 | 46 | override fun resumeWith(result: Result) { 47 | this.result = result 48 | this.resume() 49 | } 50 | 51 | fun complete(result: Result) { 52 | this.continuation.resumeWith(result) 53 | } 54 | } 55 | 56 | fun execute(inputValues: InputValues, continuation: Continuation) { 57 | val (userId, productIds) = inputValues 58 | 59 | val that = this 60 | val cont = continuation as? SharedDataContinuation 61 | ?: SharedDataContinuation(continuation).apply { 62 | resume = fun() { 63 | that.execute(inputValues, this) 64 | } 65 | } 66 | 67 | when (cont.label) { 68 | 0 -> { 69 | cont.label = 1 70 | userRepository.findUserByIdAsMaybe(userId).awaitSingle(cont) 71 | } 72 | 1 -> { 73 | cont.label = 2 74 | cont.buyer = (cont.result as Result).getOrThrow() 75 | addressRepository.findAddressByUserAsPublisher(cont.buyer).awaitLast(cont) 76 | } 77 | 2 -> { 78 | cont.label = 3 79 | cont.address = (cont.result as Result
).getOrThrow() 80 | checkValidRegion(cont.address) 81 | productRepository.findAllProductsByIdsAsFlux(productIds).toList(cont) 82 | } 83 | 3 -> { 84 | cont.label = 4 85 | cont.products = (cont.result as Result>).getOrThrow() 86 | check(cont.products.isNotEmpty()) 87 | storeRepository.findStoresByProductsAsMulti(cont.products).toList(cont) 88 | } 89 | 4 -> { 90 | cont.label = 5 91 | cont.stores = (cont.result as Result>).getOrThrow() 92 | check(cont.stores.isNotEmpty()) 93 | orderRepository.createOrderAsFuture( 94 | cont.buyer, cont.products, cont.stores, cont.address 95 | ).awaitSingle(cont) 96 | } 97 | 5 -> { 98 | cont.order = (cont.result as Result).getOrThrow() 99 | cont.complete(Result.success(cont.order)) 100 | } 101 | else -> throw IllegalAccessException() 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karrot/example/usecase/order/coroutine/CreateOrderAsyncCoroutineUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.example.usecase.order.coroutine 2 | 3 | import com.karrot.example.entity.order.Order 4 | import com.karrot.example.repository.account.UserAsyncRepository 5 | import com.karrot.example.repository.catalog.ProductAsyncRepository 6 | import com.karrot.example.repository.order.OrderAsyncRepository 7 | import com.karrot.example.repository.shipment.AddressAsyncRepository 8 | import com.karrot.example.repository.store.StoreAsyncRepository 9 | import com.karrot.example.usecase.order.CreateOrderUseCaseBase 10 | import kotlinx.coroutines.CoroutineScope 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.async 13 | import kotlinx.coroutines.flow.toList 14 | import kotlinx.coroutines.future.await 15 | import kotlinx.coroutines.jdk9.awaitLast 16 | import kotlinx.coroutines.reactive.asFlow 17 | import kotlinx.coroutines.rx3.awaitSingle 18 | 19 | class CreateOrderAsyncCoroutineUseCase( 20 | private val userRepository: UserAsyncRepository, 21 | private val addressRepository: AddressAsyncRepository, 22 | private val productRepository: ProductAsyncRepository, 23 | private val storeRepository: StoreAsyncRepository, 24 | private val orderRepository: OrderAsyncRepository, 25 | ) : CreateOrderUseCaseBase() { 26 | data class InputValues( 27 | val userId: String, 28 | val productIds: List, 29 | ) 30 | 31 | suspend fun execute(inputValues: InputValues): Order { 32 | val (userId, productIds) = inputValues 33 | 34 | // 1. 구매자 조회 35 | val buyer = userRepository.findUserByIdAsMaybe(userId).awaitSingle() 36 | 37 | // 2. 주소 조회 및 유효성 체크 38 | val addressDeferred = CoroutineScope(Dispatchers.IO).async { 39 | addressRepository.findAddressByUserAsPublisher(buyer) 40 | .awaitLast() 41 | } 42 | 43 | // 3. 상품들 조회 44 | val products = productRepository.findAllProductsByIdsAsFlux(productIds).asFlow().toList() 45 | check(products.isNotEmpty()) 46 | 47 | // 4. 스토어 조회 48 | val storesDeferred = CoroutineScope(Dispatchers.IO).async { 49 | storeRepository.findStoresByProductsAsMulti(products).asFlow().toList() 50 | } 51 | 52 | val address = addressDeferred.await() 53 | val stores = storesDeferred.await() 54 | 55 | checkValidRegion(address) 56 | check(stores.isNotEmpty()) 57 | 58 | // 5. 주문 생성 59 | val order = orderRepository.createOrderAsFuture(buyer, products, stores, address).await() 60 | 61 | return order 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karrot/example/usecase/order/coroutine/CreateOrderCoroutineUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.example.usecase.order.coroutine 2 | 3 | import com.karrot.example.entity.order.Order 4 | import com.karrot.example.repository.account.UserAsyncRepository 5 | import com.karrot.example.repository.catalog.ProductAsyncRepository 6 | import com.karrot.example.repository.order.OrderAsyncRepository 7 | import com.karrot.example.repository.shipment.AddressAsyncRepository 8 | import com.karrot.example.repository.store.StoreAsyncRepository 9 | import com.karrot.example.usecase.order.CreateOrderUseCaseBase 10 | import kotlinx.coroutines.flow.toList 11 | import kotlinx.coroutines.future.await 12 | import kotlinx.coroutines.jdk9.awaitLast 13 | import kotlinx.coroutines.reactive.asFlow 14 | import kotlinx.coroutines.rx3.awaitSingle 15 | 16 | class CreateOrderCoroutineUseCase( 17 | private val userRepository: UserAsyncRepository, 18 | private val addressRepository: AddressAsyncRepository, 19 | private val productRepository: ProductAsyncRepository, 20 | private val storeRepository: StoreAsyncRepository, 21 | private val orderRepository: OrderAsyncRepository, 22 | ) : CreateOrderUseCaseBase() { 23 | data class InputValues( 24 | val userId: String, 25 | val productIds: List, 26 | ) 27 | 28 | suspend fun execute(inputValues: InputValues): Order { 29 | val (userId, productIds) = inputValues 30 | 31 | // 1. 구매자 조회 32 | val buyer = userRepository.findUserByIdAsMaybe(userId).awaitSingle() 33 | 34 | // 2. 주소 조회 및 유효성 체크 35 | val address = addressRepository.findAddressByUserAsPublisher(buyer) 36 | .awaitLast() 37 | checkValidRegion(address) 38 | 39 | // 3. 상품들 조회 40 | val products = productRepository.findAllProductsByIdsAsFlux(productIds).asFlow().toList() 41 | check(products.isNotEmpty()) 42 | 43 | // 4. 스토어 조회 44 | val stores = storeRepository.findStoresByProductsAsMulti(products).asFlow().toList() 45 | check(stores.isNotEmpty()) 46 | 47 | // 5. 주문 생성 48 | val order = orderRepository.createOrderAsFuture(buyer, products, stores, address).await() 49 | 50 | return order 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karrot/example/usecase/order/coroutine/CreateOrderErrorCoroutineUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.example.usecase.order.coroutine 2 | 3 | import com.karrot.example.entity.order.Order 4 | import com.karrot.example.repository.account.UserAsyncRepository 5 | import com.karrot.example.repository.catalog.ProductAsyncRepository 6 | import com.karrot.example.repository.order.OrderAsyncRepository 7 | import com.karrot.example.repository.shipment.AddressAsyncRepository 8 | import com.karrot.example.repository.store.StoreAsyncRepository 9 | import com.karrot.example.usecase.order.CreateOrderUseCaseBase 10 | import kotlinx.coroutines.flow.toList 11 | import kotlinx.coroutines.future.await 12 | import kotlinx.coroutines.jdk9.awaitLast 13 | import kotlinx.coroutines.reactive.asFlow 14 | import kotlinx.coroutines.rx3.awaitSingle 15 | 16 | class CreateOrderErrorCoroutineUseCase( 17 | private val userRepository: UserAsyncRepository, 18 | private val addressRepository: AddressAsyncRepository, 19 | private val productRepository: ProductAsyncRepository, 20 | private val storeRepository: StoreAsyncRepository, 21 | private val orderRepository: OrderAsyncRepository, 22 | ) : CreateOrderUseCaseBase() { 23 | data class InputValues( 24 | val userId: String, 25 | val productIds: List, 26 | ) 27 | 28 | suspend fun execute(inputValues: InputValues): Order { 29 | val (userId, productIds) = inputValues 30 | 31 | // 1. 구매자 조회 32 | val buyer = try { 33 | userRepository.findUserByIdAsMaybe(userId).awaitSingle() 34 | } catch (e: Exception) { 35 | throw NoSuchElementException("no such user") 36 | } 37 | 38 | // 2. 주소 조회 및 유효성 체크 39 | val address = addressRepository.findAddressByUserAsPublisher(buyer) 40 | .awaitLast() 41 | checkValidRegion(address) 42 | 43 | // 3. 상품들 조회 44 | val products = productRepository.findAllProductsByIdsAsFlux(productIds).asFlow().toList() 45 | check(products.isNotEmpty()) 46 | 47 | // 4. 스토어 조회 48 | val stores = storeRepository.findStoresByProductsAsMulti(products).asFlow().toList() 49 | check(stores.isNotEmpty()) 50 | 51 | // 5. 주문 생성 52 | val order = orderRepository.createOrderAsFuture(buyer, products, stores, address).await() 53 | 54 | return order 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karrot/example/usecase/order/reactor/CreateOrderReactorSubscribeUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.example.usecase.order.reactor 2 | 3 | import com.karrot.example.entity.order.Order 4 | import com.karrot.example.repository.account.UserRxRepository 5 | import com.karrot.example.repository.catalog.ProductReactorRepository 6 | import com.karrot.example.repository.order.OrderFutureRepository 7 | import com.karrot.example.repository.shipment.AddressReactiveRepository 8 | import com.karrot.example.repository.shipment.LastItemSubscriber 9 | import com.karrot.example.repository.store.StoreMutinyRepository 10 | import com.karrot.example.usecase.order.CreateOrderUseCaseBase 11 | import reactor.core.publisher.Mono 12 | 13 | class CreateOrderReactorSubscribeUseCase( 14 | private val userRepository: UserRxRepository, 15 | private val addressRepository: AddressReactiveRepository, 16 | private val productRepository: ProductReactorRepository, 17 | private val storeRepository: StoreMutinyRepository, 18 | private val orderRepository: OrderFutureRepository, 19 | ) : CreateOrderUseCaseBase() { 20 | data class InputValues( 21 | val userId: String, 22 | val productIds: List, 23 | ) 24 | 25 | fun execute(inputValues: InputValues): Mono { 26 | val (userId, productIds) = inputValues 27 | 28 | return Mono.create { emitter -> 29 | userRepository.findUserByIdAsMaybe(userId) 30 | .subscribe { buyer -> 31 | addressRepository.findAddressByUserAsPublisher(buyer) 32 | .subscribe(LastItemSubscriber { address -> 33 | checkValidRegion(address) 34 | productRepository.findAllProductsByIdsAsFlux(productIds) 35 | .collectList() 36 | .subscribe { products -> 37 | check(products.isNotEmpty()) 38 | storeRepository.findStoresByProductsAsMulti(products) 39 | .collect().asList() 40 | .subscribe().with { stores -> 41 | check(stores.isNotEmpty()) 42 | orderRepository.createOrderAsFuture( 43 | buyer, products, stores, address 44 | ).whenComplete { order, _ -> 45 | emitter.success(order) 46 | } 47 | } 48 | } 49 | }) 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karrot/example/usecase/order/reactor/CreateOrderReactorUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.example.usecase.order.reactor 2 | 3 | import com.karrot.example.entity.order.Order 4 | import com.karrot.example.repository.account.UserRxRepository 5 | import com.karrot.example.repository.catalog.ProductReactorRepository 6 | import com.karrot.example.repository.order.OrderFutureRepository 7 | import com.karrot.example.repository.shipment.AddressReactiveRepository 8 | import com.karrot.example.repository.store.StoreMutinyRepository 9 | import com.karrot.example.usecase.order.CreateOrderUseCaseBase 10 | import reactor.adapter.JdkFlowAdapter 11 | import reactor.adapter.rxjava.RxJava3Adapter 12 | import reactor.core.publisher.Flux 13 | import reactor.core.publisher.Mono 14 | 15 | class CreateOrderReactorUseCase( 16 | private val userRepository: UserRxRepository, 17 | private val addressRepository: AddressReactiveRepository, 18 | private val productRepository: ProductReactorRepository, 19 | private val storeRepository: StoreMutinyRepository, 20 | private val orderRepository: OrderFutureRepository, 21 | ) : CreateOrderUseCaseBase() { 22 | data class InputValues( 23 | val userId: String, 24 | val productIds: List, 25 | ) 26 | 27 | fun execute(inputValues: InputValues): Mono { 28 | val (userId, productIds) = inputValues 29 | 30 | return RxJava3Adapter.maybeToMono(userRepository.findUserByIdAsMaybe(userId)) 31 | .flatMap { buyer -> 32 | JdkFlowAdapter.flowPublisherToFlux( 33 | addressRepository.findAddressByUserAsPublisher(buyer)) 34 | .last() 35 | .flatMap { address -> 36 | checkValidRegion(address) 37 | productRepository.findAllProductsByIdsAsFlux(productIds) 38 | .collectList() 39 | .flatMap { products -> 40 | check(products.isNotEmpty()) 41 | Flux.from(storeRepository.findStoresByProductsAsMulti(products)) 42 | .collectList() 43 | .flatMap { stores -> 44 | check(stores.isNotEmpty()) 45 | Mono.fromFuture( 46 | orderRepository.createOrderAsFuture( 47 | buyer, products, stores, address 48 | ) 49 | ) 50 | } 51 | } 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karrot/example/usecase/order/sync/CreateOrderSyncStateMachineUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.example.usecase.order.sync 2 | 3 | import com.karrot.example.entity.account.User 4 | import com.karrot.example.entity.catalog.Product 5 | import com.karrot.example.entity.order.Order 6 | import com.karrot.example.entity.store.Store 7 | import com.karrot.example.repository.account.UserSyncRepository 8 | import com.karrot.example.repository.catalog.ProductSyncRepository 9 | import com.karrot.example.repository.order.OrderSyncRepository 10 | import com.karrot.example.repository.shipment.AddressSyncRepository 11 | import com.karrot.example.repository.store.StoreSyncRepository 12 | import com.karrot.example.usecase.order.CreateOrderUseCaseBase 13 | import com.karrot.example.vo.Address 14 | 15 | class CreateOrderSyncStateMachineUseCase( 16 | private val userRepository: UserSyncRepository, 17 | private val addressRepository: AddressSyncRepository, 18 | private val productRepository: ProductSyncRepository, 19 | private val storeRepository: StoreSyncRepository, 20 | private val orderRepository: OrderSyncRepository, 21 | ) : CreateOrderUseCaseBase() { 22 | data class InputValues( 23 | val userId: String, 24 | val productIds: List, 25 | ) 26 | 27 | class SharedData { 28 | var label: Int = 0 29 | lateinit var result: Any 30 | lateinit var buyer: User 31 | lateinit var address: Address 32 | lateinit var products: List 33 | lateinit var stores: List 34 | lateinit var order: Order 35 | lateinit var resumeWith: (result: Any) -> Order 36 | } 37 | 38 | fun execute( 39 | inputValues: InputValues, 40 | sharedData: SharedData? = null, 41 | ): Order { 42 | val (userId, productIds) = inputValues 43 | 44 | val that = this 45 | val shared = sharedData ?: SharedData().apply { 46 | this.resumeWith = fun (result: Any): Order { 47 | this.result = result 48 | return that.execute(inputValues, this) 49 | } 50 | } 51 | 52 | return when (shared.label) { 53 | 0 -> { 54 | shared.label = 1 55 | userRepository.findUserByIdSync(userId) 56 | .let { user -> 57 | shared.resumeWith(user) 58 | } 59 | } 60 | 1 -> { 61 | shared.label = 2 62 | shared.buyer = shared.result as User 63 | addressRepository.findAddressByUserSync(shared.buyer).last() 64 | .let { address -> 65 | shared.resumeWith(address) 66 | } 67 | } 68 | 2 -> { 69 | shared.label = 3 70 | shared.address = shared.result as Address 71 | checkValidRegion(shared.address) 72 | productRepository.findAllProductsByIdsSync(productIds) 73 | .let { products -> 74 | shared.resumeWith(products) 75 | } 76 | } 77 | 3 -> { 78 | shared.label = 4 79 | shared.products = shared.result as List 80 | check(shared.products.isNotEmpty()) 81 | storeRepository.findStoresByProductsSync(shared.products) 82 | .let { stores -> 83 | shared.resumeWith(stores) 84 | } 85 | } 86 | 4 -> { 87 | shared.label = 5 88 | shared.stores = shared.result as List 89 | check(shared.stores.isNotEmpty()) 90 | orderRepository.createOrderSync( 91 | shared.buyer, shared.products, shared.stores, shared.address 92 | ).let { order -> 93 | shared.resumeWith(order) 94 | } 95 | } 96 | 5 -> { 97 | shared.order = shared.result as Order 98 | shared.order 99 | } 100 | else -> throw IllegalAccessException() 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karrot/example/usecase/order/sync/CreateOrderSyncUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.example.usecase.order.sync 2 | 3 | import com.karrot.example.entity.order.Order 4 | import com.karrot.example.repository.account.UserSyncRepository 5 | import com.karrot.example.repository.catalog.ProductSyncRepository 6 | import com.karrot.example.repository.order.OrderSyncRepository 7 | import com.karrot.example.repository.shipment.AddressSyncRepository 8 | import com.karrot.example.repository.store.StoreSyncRepository 9 | import com.karrot.example.usecase.order.CreateOrderUseCaseBase 10 | 11 | class CreateOrderSyncUseCase( 12 | private val userRepository: UserSyncRepository, 13 | private val addressRepository: AddressSyncRepository, 14 | private val productRepository: ProductSyncRepository, 15 | private val storeRepository: StoreSyncRepository, 16 | private val orderRepository: OrderSyncRepository, 17 | ) : CreateOrderUseCaseBase() { 18 | data class InputValues( 19 | val userId: String, 20 | val productIds: List, 21 | ) 22 | 23 | fun execute(inputValues: InputValues): Order { 24 | val (userId, productIds) = inputValues 25 | 26 | // 1. 구매자 조회 27 | val buyer = userRepository.findUserByIdSync(userId) 28 | 29 | // 2. 주소 조회 및 유효성 체크 30 | val address = addressRepository.findAddressByUserSync(buyer).last() 31 | checkValidRegion(address) 32 | 33 | // 3. 상품들 조회 34 | val products = productRepository.findAllProductsByIdsSync(productIds) 35 | check(products.isNotEmpty()) 36 | 37 | // 4. 스토어 조회 38 | val stores = storeRepository.findStoresByProductsSync(products) 39 | check(stores.isNotEmpty()) 40 | 41 | // 5. 주문 생성 42 | val order = orderRepository.createOrderSync(buyer, products, stores, address) 43 | 44 | return order 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karrot/example/util/coroutine.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.example.util 2 | 3 | import com.karrot.example.repository.shipment.LastItemSubscriber 4 | import io.reactivex.rxjava3.core.Maybe 5 | import io.smallrye.mutiny.Multi 6 | import reactor.core.publisher.Flux 7 | import java.util.concurrent.CompletionStage 8 | import java.util.concurrent.Flow 9 | import kotlin.coroutines.Continuation 10 | 11 | fun Maybe.awaitSingle(cont: Continuation) { 12 | this.subscribe { user -> 13 | cont.resumeWith(Result.success(user)) 14 | } 15 | } 16 | 17 | fun Flow.Publisher.awaitLast(cont: Continuation) { 18 | this.subscribe(LastItemSubscriber { address -> 19 | cont.resumeWith(Result.success(address)) 20 | }) 21 | } 22 | 23 | fun Flux.toList(cont: Continuation) { 24 | this.collectList() 25 | .subscribe { products -> 26 | cont.resumeWith(Result.success(products)) 27 | } 28 | } 29 | 30 | fun Multi.toList(cont: Continuation) { 31 | this.collect() 32 | .asList() 33 | .subscribeAsCompletionStage() 34 | .whenComplete { stores, _ -> 35 | cont.resumeWith(Result.success(stores)) 36 | } 37 | } 38 | 39 | fun CompletionStage.awaitSingle(cont: Continuation) { 40 | this.whenComplete { order, _ -> 41 | cont.resumeWith(Result.success(order)) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karrot/example/vo/Address.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.example.vo 2 | 3 | data class Address( 4 | val detailAddress: String, 5 | val roadNameAddress: String, 6 | ) 7 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karrot/example/vo/Money.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.example.vo 2 | 3 | data class Money( 4 | val currency: MoneyCurrency, 5 | val amount: Long, 6 | ) 7 | -------------------------------------------------------------------------------- /src/main/kotlin/com/karrot/example/vo/MoneyCurrency.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.example.vo 2 | 3 | enum class MoneyCurrency { 4 | WON 5 | } 6 | -------------------------------------------------------------------------------- /src/test/kotlin/com/karrot/commerce/usecase/order/async/CreateOrderAsyncStateMachine1UseCaseTests.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.commerce.usecase.order.async 2 | 3 | import com.karrot.example.repository.account.UserRxRepository 4 | import com.karrot.example.repository.catalog.ProductReactorRepository 5 | import com.karrot.example.repository.order.OrderFutureRepository 6 | import com.karrot.example.repository.shipment.AddressReactiveRepository 7 | import com.karrot.example.repository.store.StoreMutinyRepository 8 | import com.karrot.example.usecase.order.async.CreateOrderAsyncStateMachine1UseCase 9 | import io.mockk.impl.annotations.InjectMockKs 10 | import io.mockk.impl.annotations.SpyK 11 | import io.mockk.junit5.MockKExtension 12 | import org.apache.commons.lang3.time.StopWatch 13 | import org.junit.jupiter.api.Test 14 | import org.junit.jupiter.api.extension.ExtendWith 15 | import java.util.concurrent.CountDownLatch 16 | import java.util.concurrent.TimeUnit 17 | 18 | @ExtendWith(MockKExtension::class) 19 | class CreateOrderAsyncStateMachine1UseCaseTests { 20 | @InjectMockKs 21 | private lateinit var createOrderUseCase: CreateOrderAsyncStateMachine1UseCase 22 | 23 | @SpyK 24 | private var spyUserRepository: UserRxRepository = UserRxRepository() 25 | 26 | @SpyK 27 | private var spyProductRepository: ProductReactorRepository = ProductReactorRepository() 28 | 29 | @SpyK 30 | private var spyStoreRepository: StoreMutinyRepository = StoreMutinyRepository() 31 | 32 | @SpyK 33 | private var spyOrderRepository: OrderFutureRepository = OrderFutureRepository() 34 | 35 | @SpyK 36 | private var spyAddressRepository: AddressReactiveRepository = AddressReactiveRepository() 37 | 38 | @Test 39 | fun `should return a createdOrder in async with state machine`() { 40 | // given 41 | val userId = "user1" 42 | val productIds = listOf("product1", "product2", "product3") 43 | 44 | // when 45 | val watch = StopWatch().also { it.start() } 46 | val lock = CountDownLatch(1) 47 | 48 | val inputValues = CreateOrderAsyncStateMachine1UseCase.InputValues(userId, productIds) 49 | createOrderUseCase.execute(inputValues, { createdOrder -> 50 | watch.stop() 51 | lock.countDown() 52 | 53 | println("Time Elapsed: ${watch.time}ms") 54 | println(createdOrder) 55 | }) 56 | 57 | // then 58 | lock.await(3000, TimeUnit.MILLISECONDS) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/test/kotlin/com/karrot/commerce/usecase/order/async/CreateOrderAsyncStateMachine2UseCaseTests.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.commerce.usecase.order.async 2 | 3 | import com.karrot.example.repository.account.UserRxRepository 4 | import com.karrot.example.repository.catalog.ProductReactorRepository 5 | import com.karrot.example.repository.order.OrderFutureRepository 6 | import com.karrot.example.repository.shipment.AddressReactiveRepository 7 | import com.karrot.example.repository.store.StoreMutinyRepository 8 | import com.karrot.example.usecase.order.async.CreateOrderAsyncStateMachine2UseCase 9 | import io.mockk.impl.annotations.InjectMockKs 10 | import io.mockk.impl.annotations.SpyK 11 | import io.mockk.junit5.MockKExtension 12 | import org.apache.commons.lang3.time.StopWatch 13 | import org.junit.jupiter.api.Test 14 | import org.junit.jupiter.api.extension.ExtendWith 15 | import java.util.concurrent.CountDownLatch 16 | import java.util.concurrent.TimeUnit 17 | import kotlin.coroutines.Continuation 18 | import kotlin.coroutines.EmptyCoroutineContext 19 | 20 | @ExtendWith(MockKExtension::class) 21 | class CreateOrderAsyncStateMachine2UseCaseTests { 22 | @InjectMockKs 23 | private lateinit var createOrderUseCase: CreateOrderAsyncStateMachine2UseCase 24 | 25 | @SpyK 26 | private var spyUserRepository: UserRxRepository = UserRxRepository() 27 | 28 | @SpyK 29 | private var spyProductRepository: ProductReactorRepository = ProductReactorRepository() 30 | 31 | @SpyK 32 | private var spyStoreRepository: StoreMutinyRepository = StoreMutinyRepository() 33 | 34 | @SpyK 35 | private var spyOrderRepository: OrderFutureRepository = OrderFutureRepository() 36 | 37 | @SpyK 38 | private var spyAddressRepository: AddressReactiveRepository = AddressReactiveRepository() 39 | 40 | @Test 41 | fun `should return a createdOrder in async with state machine`() { 42 | // given 43 | val userId = "user1" 44 | val productIds = listOf("product1", "product2", "product3") 45 | 46 | // when 47 | val watch = StopWatch().also { it.start() } 48 | val lock = CountDownLatch(1) 49 | val testContinuation = object : Continuation { 50 | override val context = EmptyCoroutineContext 51 | override fun resumeWith(result: Result) { 52 | watch.stop() 53 | lock.countDown() 54 | 55 | println("Time Elapsed: ${watch.time}ms") 56 | println(result.getOrThrow()) 57 | } 58 | } 59 | 60 | val inputValues = CreateOrderAsyncStateMachine2UseCase.InputValues(userId, productIds) 61 | 62 | createOrderUseCase.execute(inputValues, testContinuation) 63 | 64 | // then 65 | lock.await(3000, TimeUnit.MILLISECONDS) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/test/kotlin/com/karrot/commerce/usecase/order/async/CreateOrderAsyncStateMachine3UseCaseTests.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.commerce.usecase.order.async 2 | 3 | import com.karrot.example.repository.account.UserRxRepository 4 | import com.karrot.example.repository.catalog.ProductReactorRepository 5 | import com.karrot.example.repository.order.OrderFutureRepository 6 | import com.karrot.example.repository.shipment.AddressReactiveRepository 7 | import com.karrot.example.repository.store.StoreMutinyRepository 8 | import com.karrot.example.usecase.order.async.CreateOrderAsyncStateMachine3UseCase 9 | import io.mockk.impl.annotations.InjectMockKs 10 | import io.mockk.impl.annotations.SpyK 11 | import io.mockk.junit5.MockKExtension 12 | import org.apache.commons.lang3.time.StopWatch 13 | import org.junit.jupiter.api.Test 14 | import org.junit.jupiter.api.extension.ExtendWith 15 | import java.util.concurrent.CountDownLatch 16 | import java.util.concurrent.TimeUnit 17 | import kotlin.coroutines.Continuation 18 | import kotlin.coroutines.EmptyCoroutineContext 19 | 20 | @ExtendWith(MockKExtension::class) 21 | class CreateOrderAsyncStateMachine3UseCaseTests { 22 | @InjectMockKs 23 | private lateinit var createOrderUseCase: CreateOrderAsyncStateMachine3UseCase 24 | 25 | @SpyK 26 | private var spyUserRepository: UserRxRepository = UserRxRepository() 27 | 28 | @SpyK 29 | private var spyProductRepository: ProductReactorRepository = ProductReactorRepository() 30 | 31 | @SpyK 32 | private var spyStoreRepository: StoreMutinyRepository = StoreMutinyRepository() 33 | 34 | @SpyK 35 | private var spyOrderRepository: OrderFutureRepository = OrderFutureRepository() 36 | 37 | @SpyK 38 | private var spyAddressRepository: AddressReactiveRepository = AddressReactiveRepository() 39 | 40 | @Test 41 | fun `should return a createdOrder in async with state machine`() { 42 | // given 43 | val userId = "user1" 44 | val productIds = listOf("product1", "product2", "product3") 45 | 46 | // when 47 | val watch = StopWatch().also { it.start() } 48 | val lock = CountDownLatch(1) 49 | val testContinuation = object : Continuation { 50 | override val context = EmptyCoroutineContext 51 | override fun resumeWith(result: Result) { 52 | watch.stop() 53 | lock.countDown() 54 | 55 | println("Time Elapsed: ${watch.time}ms") 56 | println(result.getOrThrow()) 57 | } 58 | } 59 | 60 | val inputValues = CreateOrderAsyncStateMachine3UseCase.InputValues(userId, productIds) 61 | createOrderUseCase.execute(inputValues, testContinuation) 62 | 63 | // then 64 | lock.await(3000, TimeUnit.MILLISECONDS) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/test/kotlin/com/karrot/commerce/usecase/order/coroutine/CreateOrderCoroutineUseCaseTests.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.commerce.usecase.order.coroutine 2 | 3 | import com.karrot.example.repository.account.UserRxRepository 4 | import com.karrot.example.repository.catalog.ProductReactorRepository 5 | import com.karrot.example.repository.order.OrderFutureRepository 6 | import com.karrot.example.repository.shipment.AddressReactiveRepository 7 | import com.karrot.example.repository.store.StoreMutinyRepository 8 | import com.karrot.example.usecase.order.coroutine.CreateOrderCoroutineUseCase 9 | import io.mockk.impl.annotations.InjectMockKs 10 | import io.mockk.impl.annotations.SpyK 11 | import io.mockk.junit5.MockKExtension 12 | import kotlinx.coroutines.runBlocking 13 | import org.apache.commons.lang3.time.StopWatch 14 | import org.junit.jupiter.api.Test 15 | import org.junit.jupiter.api.extension.ExtendWith 16 | 17 | @ExtendWith(MockKExtension::class) 18 | class CreateOrderCoroutineUseCaseTests { 19 | @InjectMockKs 20 | private lateinit var createOrderUseCase: CreateOrderCoroutineUseCase 21 | 22 | @SpyK 23 | private var spyUserRepository: UserRxRepository = UserRxRepository() 24 | 25 | @SpyK 26 | private var spyProductRepository: ProductReactorRepository = ProductReactorRepository() 27 | 28 | @SpyK 29 | private var spyStoreRepository: StoreMutinyRepository = StoreMutinyRepository() 30 | 31 | @SpyK 32 | private var spyOrderRepository: OrderFutureRepository = OrderFutureRepository() 33 | 34 | @SpyK 35 | private var spyAddressRepository: AddressReactiveRepository = AddressReactiveRepository() 36 | 37 | @Test 38 | fun `should return a createdOrder in coroutine`() = runBlocking { 39 | // given 40 | val userId = "user1" 41 | val productIds = listOf("product1", "product2", "product3") 42 | 43 | // when 44 | val watch = StopWatch().also { it.start() } 45 | 46 | val inputValues = CreateOrderCoroutineUseCase.InputValues(userId, productIds) 47 | val createdOrder = createOrderUseCase.execute(inputValues) 48 | 49 | watch.stop() 50 | println("Time Elapsed: ${watch.time}ms") 51 | 52 | // then 53 | println(createdOrder) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/test/kotlin/com/karrot/commerce/usecase/order/reactor/CreateOrderReactorSubscribeUseCaseTests.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.commerce.usecase.order.reactor 2 | 3 | import com.karrot.example.repository.account.UserRxRepository 4 | import com.karrot.example.repository.catalog.ProductReactorRepository 5 | import com.karrot.example.repository.order.OrderFutureRepository 6 | import com.karrot.example.repository.shipment.AddressReactiveRepository 7 | import com.karrot.example.repository.store.StoreMutinyRepository 8 | import com.karrot.example.usecase.order.reactor.CreateOrderReactorSubscribeUseCase 9 | import io.mockk.impl.annotations.InjectMockKs 10 | import io.mockk.impl.annotations.SpyK 11 | import io.mockk.junit5.MockKExtension 12 | import org.apache.commons.lang3.time.StopWatch 13 | import org.junit.jupiter.api.Test 14 | import org.junit.jupiter.api.extension.ExtendWith 15 | 16 | @ExtendWith(MockKExtension::class) 17 | class CreateOrderReactorSubscribeUseCaseTests { 18 | @InjectMockKs 19 | private lateinit var createOrderUseCase: CreateOrderReactorSubscribeUseCase 20 | 21 | @SpyK 22 | private var spyUserRepository: UserRxRepository = UserRxRepository() 23 | 24 | @SpyK 25 | private var spyProductRepository: ProductReactorRepository = ProductReactorRepository() 26 | 27 | @SpyK 28 | private var spyStoreRepository: StoreMutinyRepository = StoreMutinyRepository() 29 | 30 | @SpyK 31 | private var spyOrderRepository: OrderFutureRepository = OrderFutureRepository() 32 | 33 | @SpyK 34 | private var spyAddressRepository: AddressReactiveRepository = AddressReactiveRepository() 35 | 36 | @Test 37 | fun `should return a createdOrder in async`() { 38 | // given 39 | val userId = "user1" 40 | val productIds = listOf("product1", "product2", "product3") 41 | 42 | // when 43 | val watch = StopWatch().also { it.start() } 44 | 45 | val inputValues = CreateOrderReactorSubscribeUseCase.InputValues(userId, productIds) 46 | val createdOrder = createOrderUseCase.execute(inputValues).block() 47 | 48 | watch.stop() 49 | println("Time Elapsed: ${watch.time}ms") 50 | 51 | // then 52 | println(createdOrder) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/test/kotlin/com/karrot/commerce/usecase/order/reactor/CreateOrderReactorUseCaseTests.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.commerce.usecase.order.reactor 2 | 3 | import com.karrot.example.repository.account.UserRxRepository 4 | import com.karrot.example.repository.catalog.ProductReactorRepository 5 | import com.karrot.example.repository.order.OrderFutureRepository 6 | import com.karrot.example.repository.shipment.AddressReactiveRepository 7 | import com.karrot.example.repository.store.StoreMutinyRepository 8 | import com.karrot.example.usecase.order.reactor.CreateOrderReactorUseCase 9 | import io.mockk.impl.annotations.InjectMockKs 10 | import io.mockk.impl.annotations.SpyK 11 | import io.mockk.junit5.MockKExtension 12 | import org.apache.commons.lang3.time.StopWatch 13 | import org.junit.jupiter.api.Test 14 | import org.junit.jupiter.api.extension.ExtendWith 15 | 16 | @ExtendWith(MockKExtension::class) 17 | class CreateOrderReactorUseCaseTests { 18 | @InjectMockKs 19 | private lateinit var createOrderUseCase: CreateOrderReactorUseCase 20 | 21 | @SpyK 22 | private var spyUserRepository: UserRxRepository = UserRxRepository() 23 | 24 | @SpyK 25 | private var spyProductRepository: ProductReactorRepository = ProductReactorRepository() 26 | 27 | @SpyK 28 | private var spyStoreRepository: StoreMutinyRepository = StoreMutinyRepository() 29 | 30 | @SpyK 31 | private var spyOrderRepository: OrderFutureRepository = OrderFutureRepository() 32 | 33 | @SpyK 34 | private var spyAddressRepository: AddressReactiveRepository = AddressReactiveRepository() 35 | 36 | @Test 37 | fun `should return a createdOrder in async`() { 38 | // given 39 | val userId = "user1" 40 | val productIds = listOf("product1", "product2", "product3") 41 | 42 | // when 43 | val watch = StopWatch().also { it.start() } 44 | 45 | val inputValues = CreateOrderReactorUseCase.InputValues(userId, productIds) 46 | val createdOrder = createOrderUseCase.execute(inputValues).block() 47 | 48 | watch.stop() 49 | println("Time Elapsed: ${watch.time}ms") 50 | 51 | // then 52 | println(createdOrder) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/test/kotlin/com/karrot/commerce/usecase/order/sync/CreateOrderSyncStateMachineUseCaseTests.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.commerce.usecase.order.sync 2 | 3 | import com.karrot.example.repository.account.UserSyncRepository 4 | import com.karrot.example.repository.catalog.ProductSyncRepository 5 | import com.karrot.example.repository.order.OrderSyncRepository 6 | import com.karrot.example.repository.shipment.AddressSyncRepository 7 | import com.karrot.example.repository.store.StoreSyncRepository 8 | import com.karrot.example.usecase.order.sync.CreateOrderSyncStateMachineUseCase 9 | import io.mockk.impl.annotations.InjectMockKs 10 | import io.mockk.impl.annotations.SpyK 11 | import io.mockk.junit5.MockKExtension 12 | import org.apache.commons.lang3.time.StopWatch 13 | import org.junit.jupiter.api.Test 14 | import org.junit.jupiter.api.extension.ExtendWith 15 | 16 | @ExtendWith(MockKExtension::class) 17 | class CreateOrderSyncStateMachineUseCaseTests { 18 | @InjectMockKs 19 | private lateinit var createOrderUseCase: CreateOrderSyncStateMachineUseCase 20 | 21 | @SpyK 22 | private var spyUserRepository: UserSyncRepository = UserSyncRepository() 23 | 24 | @SpyK 25 | private var spyProductRepository: ProductSyncRepository = ProductSyncRepository() 26 | 27 | @SpyK 28 | private var spyStoreRepository: StoreSyncRepository = StoreSyncRepository() 29 | 30 | @SpyK 31 | private var spyOrderRepository: OrderSyncRepository = OrderSyncRepository() 32 | 33 | @SpyK 34 | private var spyAddressRepository: AddressSyncRepository = AddressSyncRepository() 35 | 36 | @Test 37 | fun `should return a createdOrder in sync with state machine`() { 38 | // given 39 | val userId = "user1" 40 | val productIds = listOf("product1", "product2", "product3") 41 | 42 | // when 43 | val watch = StopWatch().also { it.start() } 44 | 45 | val inputValues = CreateOrderSyncStateMachineUseCase.InputValues(userId, productIds) 46 | val createdOrder = createOrderUseCase.execute(inputValues) 47 | 48 | watch.stop() 49 | println("Time Elapsed: ${watch.time}ms") 50 | 51 | // then 52 | println(createdOrder) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/test/kotlin/com/karrot/commerce/usecase/order/sync/CreateOrderSyncUseCaseTests.kt: -------------------------------------------------------------------------------- 1 | package com.karrot.commerce.usecase.order.sync 2 | 3 | import com.karrot.example.repository.account.UserSyncRepository 4 | import com.karrot.example.repository.catalog.ProductSyncRepository 5 | import com.karrot.example.repository.order.OrderSyncRepository 6 | import com.karrot.example.repository.shipment.AddressSyncRepository 7 | import com.karrot.example.repository.store.StoreSyncRepository 8 | import com.karrot.example.usecase.order.sync.CreateOrderSyncUseCase 9 | import io.mockk.impl.annotations.InjectMockKs 10 | import io.mockk.impl.annotations.SpyK 11 | import io.mockk.junit5.MockKExtension 12 | import org.apache.commons.lang3.time.StopWatch 13 | import org.junit.jupiter.api.Test 14 | import org.junit.jupiter.api.extension.ExtendWith 15 | 16 | @ExtendWith(MockKExtension::class) 17 | class CreateOrderSyncUseCaseTests { 18 | @InjectMockKs 19 | private lateinit var createOrderUseCase: CreateOrderSyncUseCase 20 | 21 | @SpyK 22 | private var spyUserRepository: UserSyncRepository = UserSyncRepository() 23 | 24 | @SpyK 25 | private var spyProductRepository: ProductSyncRepository = ProductSyncRepository() 26 | 27 | @SpyK 28 | private var spyStoreRepository: StoreSyncRepository = StoreSyncRepository() 29 | 30 | @SpyK 31 | private var spyOrderRepository: OrderSyncRepository = OrderSyncRepository() 32 | 33 | @SpyK 34 | private var spyAddressRepository: AddressSyncRepository = AddressSyncRepository() 35 | 36 | @Test 37 | fun `should return a createdOrder in sync`() { 38 | // given 39 | val userId = "user1" 40 | val productIds = listOf("product1", "product2", "product3") 41 | 42 | // when 43 | val watch = StopWatch().also { it.start() } 44 | 45 | val inputValues = CreateOrderSyncUseCase.InputValues(userId, productIds) 46 | val createdOrder = createOrderUseCase.execute(inputValues) 47 | 48 | watch.stop() 49 | println("Time Elapsed: ${watch.time}ms") 50 | 51 | // then 52 | println(createdOrder) 53 | } 54 | } 55 | --------------------------------------------------------------------------------