3 เหตุผลไม่ใช้ Clean Architecture

จากมุมมอง Android Developer

Travis P
8 min readOct 19, 2023
Photo by Wilhelm Gunkel on Unsplash

หลายท่านคงทราบว่าผมไม่ปลื้ม Clean Architecture มานานมาก ตั้งแต่ 2017 ก็เริ่มออกมาต่อต้าน ตั้งคำถาม เปิด discussion เปิดวอร์ บางเหตุผลก็ฟังขึ้น บางเหตุผลฟังไม่ขึ้น แต่สุดท้ายก็ไม่เป็นผลครับ Clean แม่งเป็น mainstream จริงๆ

เมื่อต่อต้านไม่ได้ก็เข้าร่วม ถูกต้องครับ จากความเก็บกดเปิดวอร์แพ้ ทำให้ผมดำดิ่งศึกษา Clean Architecture อย่างจริงจัง หวังว่าจะหาจุดบกพร่องมาแย้งให้ได้ มันมีดีอะไรนักหนาวะ เขียนโค้ดซ้ำๆ แล้วยังต้องเขียน test ครอบโค้ดซ้ำๆ พวกนั้นอีก ซ้ำจนมีคนทำ code generator มาช่วย gen ทีได้มาทุก layer แถม test ด้วย โอ้มายก้อด จะถึกไปไหน

ผิดคาดว่ะครับ ปี 2023 ผมเลื่อมใสศรัทธาใน Clean Architecture เข้าใจอย่างถ่องแท้ คิดว่ามันมีประโยชน์ พวกเราควรน้อมนำคำสอนลุงบ๊อบใช้มันต่อไป

แต่แต่แต่แต่!!!! แต่เราตีความมันผิดครับ แก่นเรื่อง boundary และ dependency pointing inward น่ะถูกต้องเลย แต่เราเอามาใช้ผิด scope เท่านั้นเอง

โอเค เข้าเรื่อง มาดู 3 เหตุผลกันเลย

Photo by Matthew Osborn on Unsplash

1. Mobile Apps เป็นแค่ Presentation Layer

หมดยุคแล้วครับที่เราต้องมาเถียงกันว่าต้องมี domain layer ใน mobile apps ไหม คำตอบคือ ไม่มี domain layer ครับ ไม่มี data layer ด้วย เสียใจด้วยแต่ mobile apps เป็นแค่ presentation layer เท่านั้นแหละ

เมื่อไม่นานมานี้ผมเห็นโพสนึง 400 กว่าแชร์ รวบรวม Clean Architcutre template ของแต่ละ platform/framework

คุณว่ามัน make sense มั้ยอะ frontend web เอย mobile app เอย backend เอย มี Clean กันหมด

เรามาวิเคราะห์ Clean บน mobile ที่ทำกันอยู่อธิบายแต่ละ layer ว่าไง สมเหตุสมผลหรือเปล่า

  1. Presentation Layer = MVC/MVP/MVVM/MVX อะไรก็ว่าไป หรือเป็นพวก Redux/mobx/bloc ก็นับ layer นี้ยังไงก็สมเหตุสมผล คุณมี UI อยู่อะ
  2. Domain Layer = UseCase ที่ส่วนใหญ่ทำหน้าที่แค่ call data layer มีบางอัน chain หลาย API call ดูมีประโยชน์ขึ้นบ้าง บางอันเราก็พยายามยัดเยียด logic เข้าไปเช่น date/currency format logic หรือ validation logic ซึ่งจริงๆคุณทำเป็น util/helper pure function ได้ layer นี้ดูไร้ประโยชน์ที่สุด
  3. Data Layer = Local/Remote Repository/DataSource ที่ส่วนใหญ่ก็เป็น Remote Repository/DataSource API call น่ะแหละ มีเก็บ local ใน SharedPreferences/UserDefault บ้าง ก็สมเหตุสมผลอยู่นะ แต่ผมว่าการที่เราต้องแยก remote/local โดยไม่ได้มี requirement 100% offline support มัน overkill มากๆ คุณแค่เขียนให้มันเทสง่ายก็พอ

ถ้าเรามองให้กว้างขึ้น มองว่าทุกส่วนประกอบในระบบเราประกอบเป็น Clean ล่ะ

  1. Presentation Layer = mobile app, web frontend, web back office ทุกอย่างที่มี UI ต่อกับ API จาก backend ทั้งหมด
  2. Domain Layer = ส่วน business logic ใน backend
  3. Data Layer = ส่วนต่อ database/external service ใน backend

เรามาค่อยๆดูกันว่าอย่างนี้มัน Clean จริงหรือเปล่า

  • คุณมี flexibility ที่จะเปลี่ยน presentation layer เมื่อไรก็ได้ เพราะคุณใช้ http API call คุยระหว่าง presentation layer กับ domain layer
  • domain layer ไม่รู้จัก presentation layer ขอแค่ยิง API เป็นก็พอ
  • คุณมี flexibility ที่จะเปลี่ยน database เมื่อไรก็ได้ยกตัวอย่างเช่น Spring มี Repository ที่อนุญาติให้คุณสลับ DataSource ได้ตามใจชอบเพียงแค่เปลี่ยน config เนื่องจากผมไม่ได้โปรด้านนี้ พูดไรไม่ได้มาก แต่เข้าใจว่า backend framework หรือ database abstraction library จะสามารถทำพวกนี้ได้อย่างง่ายดาย
  • domain layer ไม่รู้จัก data layer ถ้าคุณทำ dependency injection ถูกต้อง
  • presentation layer เทสง่ายโคตร คุณแค่ mock API call แล้วเช็ค UI แสดงถูกไหม
  • domain layer เทสง่าย คุณ mock data layer หรือสลับเป็น test/in-memory db แล้วเช็ค API response ถูกไหม
  • data layer อาจจะไม่ต้องเทสเพราะคุณใช้ lib/framework แต่ถ้าคุณมี custom query เพื่อทำ optimization ก็ขึ้นกับ lib ที่คุณใช้ว่ามันเทสยังไง

ขอหมายเหตุไว้ตรงนี้ไว้นิดนึงว่า

  • domain/data layer ที่อยู่ใน backend นั้นขึ้นอยู่กับ framework/lib ที่ใช้ว่ามันช่วย abstract pure domain logic ออกจาก framework/lib ขนาดไหน
  • backend framework บางอันมีวิธี integrate domain/business logic ของคุณเข้ากับ framework ได้โดย business logic ของคุณไม่รู้จัก framework เลย ในเคสนี้คุณสามารถเทส domain layer ได้แบบ plain class, plain function เลย
  • การพึ่งพา framework ไม่ได้ทำให้ใครตาย คุณไม่จำเป็นต้องแยก business logic ออกจาก framework 100% เพราะ framework มันเกิดขึ้นมาอำนวยความสะดวก คุณเลือกเอาว่าคุณรับได้กับความไม่ clean นิดหน่อยแต่ยังเทสได้ หรือจะถึกแยก 100% “เผื่อ”ว่าวันนึงคุณจะย้ายออกจาก framework นั้นๆ
  • จริงๆ Clean ไม่ได้จำเป็นต้องแบ่งเป็น 3 layer แต่ส่วนใหญ่ใน mobile เค้าฮิตกันแบบนั้น
  • จริงๆคำอธิบายชุดนี้ไม่ได้เฉพาะเจาะจงกับ Clean แต่มันเป็นคำอธิบายในมุมของ layered architecture โดยทั่วไป ซึ่ง Clean เป็นหนึ่งในนั้น

อ้าว แล้วอย่างนี้ 3 layer ใน mobile ที่เราทำกันมาจริงๆแล้วคืออะไรล่ะ?

  • MVC/MVP/MVVM/MVX ถูกต้องอยู่แล้ว เป็นส่วนหนึ่งใน presentation layer
  • UseCase ไม่มีจริง
  • date/currency format คือการแปลงข้อมูลที่ user อ่านยากอ่านไม่เข้าใจเป็นข้อมูลที่ user เข้าใจ ก็เป็นส่วนหนึ่งใน presentation layer
  • validation logic ฝั่ง app ผมว่า grey area เข้าข่ายเป็น domain/business logic แต่ถ้ามองว่าเป็นการ format/validate raw input ของ user ก็จัดเป็น presentation logic ได้
  • ถึงแม้ app จะมี domain logic อยู่บ้าง แต่คุณก็ไม่จำเป็นต้องแต่งตั้งมันเป็น UseCase ไม่ต้องไปแยก module คุณแค่เขียนเป็น pure function โง่ๆก็ได้ ยัง test ได้ ไม่มีใครตาย
  • remote repository/data source ไม่ใช่ data layer แต่เป็นเพียงการสื่อสารไปยัง domain layer ที่อยู่ฝั่ง backend เท่านั้น
  • ถ้าคุณเถียงว่า repository/data source มันยังไงก็ data layer ชัดๆ ผมท้าให้คุณ login app คุณด้วย user A แล้วไป query user profile ของ user B มา ถ้าคุณทำไม่ได้แปลว่ามันผ่าน domain/business logic มาแล้ว ถ้าทำได้คือ backend คุณบ้งอะ
  • local repository/data source ซึ่งส่วนใหญ่มีไว้ทำ 3 อย่างคือ 1) ใช้เป็น cache 2) ตั้งใจเก็บข้อมูลให้รอด app kill 3)ใช้ฝากข้อมูลไปหน้าท้ายๆเพราะคุณขี้เกียจส่งผ่านหลายๆหน้า พวกนี้ผมว่ามันคือ state management ครับเป็นการจัดการหรือแชร์ข้อมูลตาม screen flow ต่างๆในแอพ เป็นส่วนหนึ่งของ presentation layer แน่นอน เพราะส่วนใหญ่ API คุณ stateless นะครับ ไม่ได้รู้เรื่องด้วยหรอกว่า getUserProfile โดนยิงมาจากหน้าไหน
  • data mapper ระหว่าง layer ถ้าคุณจะทำ คุณทำมันในฐานะ domain layer -> presentation layer เพื่อ format หรือเพื่อตัดส่วนไม่ใช้ออกก็พอฟังขึ้น แต่ผมขอถามสวนกลับไปว่า การแปลง data ให้อยู่ในรูปที่แสดงผลได้ มันคือหน้าที่โดยตรงของ presentation layer หรือเปล่า คุณจะไปทำตรงไหนก็ได้ ViewModel ก็ได้ จะ format ใน View ก่อนยัดใส่ UI ก็ยังได้ ตราบเท่าที่คุณมีวิธีเทส
  • การต่อ external sdk เช่น facebook/line login หรือ real time chat sdk จัดเป็น data layer ได้ จัดเป็นขอบของระบบคุณ ถ้าจะทำ data mapper บน mobile ตรงนี้พอได้ แต่ผมมั่นใจว่า backend คุณก็มี mapping เช่นกัน
  • ในเมื่อเรารู้แล้วว่าการยิง API ไม่ใช่ data layer ผมขอแนะนำให้เลิกเรียกการยิง API ว่า Repository เพราะ Repository มันคือ abstraction ของ raw data store ที่คุณจะ insert/query มันยังไงก็ได้ จะเรียกว่า MyApi หรือ MyService หรืออื่นๆก็ตามสะดวก
  • อาจมีคำอธิบายหรือเหตุผลอื่นๆอีก แต่ตอนนี้นึกไม่ออก อาจจะมาอัพเดตภายหลัง

ขอหมายเหตุเด่นๆไว้ตรงนี้ข้อนึงครับ ถ้าแอพคุณต่อ firebase database ตรงๆ นั่นแหละมีครบ 3 layer เลย อยากแบ่ง module อยากทำ UseCase อยากห่อ Repository เอาเลยครับ โอกาสมาถึงแล้ว

เหตุผลข้อ 1 สรุปสั้นๆคือ Clean Architecture ควรนำมาใช้กับภาพรวมระบบทั้งหมด โดย presentation layer สามารถประกอบด้วย Android/iOS/web/desktop app หรืออื่นๆ ส่วน backend เป็นผู้รับผิดชอบ domain และ data layer

Photo by James Francis on Unsplash

2. Data Mapper ไม่ได้มีประโยชน์อย่างที่โฆษณา

ถ้าเหตุผลข้อ 1 ยังไม่หนักแน่นพอ อยากชวนมาวิเคราะห์ Mapper กันครับ

ทุกสำนักจะมีเหมือนกันคือ DataToDomainMapper เพื่อ map API response/entity มาเป็น domain model ซึ่งจากเหตุผลข้อ 1 จริงๆแล้ว API call เป็นขอบของ presentation layer ต่อกับ domain layer ที่ backend ถูกไหมครับ ฉะนั้นการ map API response มันควรจะเป็น DomainBackendToPresentationAppMapper ตามหลัก Clean ที่เหมาะสม

บางสำนักขยันมี DomainToPresentationMapper ด้วย ส่วนใหญ่มักเป็น 1:1 mapping บางทีมีการ format, แปลง type หรือ assign default value ให้ null ซึ่งหน้าที่ส่วนนี้เป็นหน้าที่โดยตรงของ presentation layer อย่าง app นี่แหละ คุณจะทำใน class ไหนก็ได้ คุณจะเหนื่อยเพิ่มกระจายบางส่วนไปทำใน Mapper ทำไมในเมื่อไปทำใน ViewModel หรือ View ก็ได้ มันมีวีธีเทสอยู่แล้ว

Mapper ช่วยให้ dev หลายคนทำงาน 2–3 layer พร้อมกันได้จริงหรือ

คำตอบก็จริงแหละถ้าคุณมี dev 2 คน คนแรกทำ View/ViewModel อีกคนเขียน UseCase/Repository/DataSource/Service/API call

คำถามคือ workflow/process การทำงานของคุณมันเป็นแบบนั้นหรือเปล่า ถ้าทีมคุณแบ่งงานให้ dev 1 คนรับผิดชอบ 1 feature ไปเลย ก็ไม่มีประโยชน์เลยนะ

Mapper ช่วยให้แก้โค้ดง่ายเมื่อมี API เปลี่ยนจริงหรือ

สมมติคุณทำ ApiResponseToModelMapper เพื่อจำกัด change ให้อยู่แค่ Mapper ไม่กระทบไปถึง ViewModel/UI ลองคิดตามนี้ครับ

  • Domain Model ใน app คุณสร้างตามอะไร
  • ถ้าสร้าง Model ตาม json response ที่ backend ส่งให้ก็จบแล้วครับ คุณ map ให้ตายถ้า json เปลี่ยนคุณก็เปลี่ยนตามอยู่ดี
  • ถ้าคุณสร้างตาม Figma ยังมีเหตุผล ถือเป็น DomainBackendToPresentationAppMapper ได้
  • แต่ช้าก่อน ถ้า app คุณต่อกับ Backend For Frontend (BFF) แปลว่า API response ที่คุณได้ ถูกออกแบบมาให้แอพโดยเฉพาะ ถ้าจะมี change เพิ่มลด property แปลว่ามันต้องมี requirement change กระทบถึงฝั่งแอพอยู่แล้ว เคสนี้คุณจะ map ยังไงมันก็ต้องแก้ทะลุไปถึงอย่างต่ำ ViewModel แน่นอน
  • ถ้าคุณต่อกับ legacy backend ที่พ่น json อะไรก็ไม่รู้ออกมาเต็มไปหมด แล้วอยาก map ให้เหลือแค่ของที่ใช้ อันนี้ทำเลยครับ เห็นใจจริงๆ แต่ก็มีวิธีอื่นเช่น custom json serializer เพื่อ map json ใหญ่ๆเข้า model เท่าที่ใช้โดยตรง ซึ่ง performance อาจดีกว่าด้วยเพราะไม่ต้อง deserialize json ทั้งก้อน แล้วมา map อีกที
  • ถ้าคุณอยาก aggregate ยุบ/รวม หลายๆ API response มาเพื่อให้ View ใช้สะดวก อันนี้มีเหตุผลครับ แต่ก็มีทางเลือกอื่นเช่น สร้าง wrapper class มาครอบแล้วมี getter ดึงข้อมูลง่ายๆ
  • นอกจากนี้ การใช้ API response ตรงๆทำให้ debug ง่ายขึ้น คุณเคยป่ะล่ะต้องไล่ผ่าน ViewModel Mapper UseCase Mapper กว่าจะรู้ว่า json มันผิดตรงไหน บางทีการที่คุณไปใส่ default value ใน mapper ทำให้เกิด bug เองด้วยซ้ำ
  • การใช้ API response ใน View ไม่ได้ทำให้ใครตาย กรณี API breaking change เราจะโดน compiler เตือน ไม่ลืมแก้แน่นอน หากคุณมี mapper คุณต้องเช็คเพิ่มหรือแก้เพิ่มอีก
Photo by CATHY PHAM on Unsplash

3. UseCase ไม่ใช่วิธีแก้ ViewModel บวม

การแก้ ViewModel บวม เป็นหนึ่งในเหตุผลหลักสนับสนุนการทำ UseCase class เลย นอกจากนี้ยังสามารถ reuse ได้ สลับ local/remote repo/data source ได้ เทสง่ายด้วย คุณว่ามันจริงขนาดไหนมาตั้งใจวิเคราะห์กันทีละข้อเลย

  • การย้ายโค้ดใน ViewModel ไปอยู่ใน UseCase มันย้อนแย้งหลัก Clean ที่คุณเชื่ออย่างสิ้นเชิง คุณบอกว่า ViewModel เป็น presentation layer แล้วจะ refactor ไปอยู่ใน UseCase domain layer ได้ไง
  • reuse ได้ = ถูกต้อง แต่ไม่ใช่วิธีเดียว function โง่ๆก็ reuse ได้ป่าว
  • สลับ local/remote repo/data source ได้ = ถูกต้อง แต่คุณก็สามารถส่ง repo interface เข้าไปใน function ก็สลับได้เหมือนกันป่าว
  • เทส UseCase ได้ง่าย = ถูกต้อง แต่ function ก็เทสง่ายเช่นกัน
  • เทส ViewModel ได้ง่าย = จริง อันนี้ function สู้ไม่ได้
  • ลด ViewModel บวม = ไม่จริง เพราะ UseCase คุณมันก็แค่ chain API call อาจมีทำ transform/validate logic หรือ if-else บ้าง แต่สุดท้ายคุณก็เหลือ view-binding logic ที่ต้องทำใน ViewModel อยู่ดี
  • UseCase ที่เขียนแล้วพอมีประโยน์ก็มีเพียงไม่กี่อันในแอพหรอก สมมติคุณยิง 50 API มี UseCase ที่ chain API มี logic จริงจัง 10 อันก็หรูแล้ว ที่เหลือก็แค่เรียก repository

UseCase ก็ไม่เวิร์ค แยก function ก็ไม่ตอบโจทย์ แล้วจะให้ทำไง?

แก้ ViewModel บวมด้วย Kotlin Delegation

อันนี้คนละเรื่องกับ delegated property นะครับ ที่จะพูดถึงคือ delegation pattern สามารถไปทบทวน talk ที่ผมเคยขี้โม้ให้ฟังได้

สมมติคุณมี requirement login flow ตามนี้

  1. validate ค่า email และ password
  2. ถ้าไม่สำเร็จให้โชว์ error snackbar
  3. ถ้าสำเร็จยิง login API
  4. ถ้าไม่สำเร็จโชว์ error snackbar
  5. ถ้าสำเร็จยิง getUserProfile API ต่อ
  6. ถ้าไม่สำเร็จโชว์ error snackbar

login flow นี้ยิงจาก 2 หน้า LoginScreen และ CartScreen แต่ละหน้ามี ViewModel ของตัวเอง implementation ก็ประมาณนี้

class LoginVewModel(
private val authService: AuthService,
private val userProfileService: UserProfileService,
) {
val userProfileStateFlow: MutableStateFlow<UserProfile?> = MutableStateFlow(null)
val errorStateFlow: MutableStateFlow<String?> = MutableStateFlow(null)
fun login(email: String, password: String) {
viewModelScope.launch {
if (!validateEmail(email)) {
errorStateFlow.value = "invalid email"
return@launch
}
val loginResult = authService.login(email, password)
if (loginResult.isFailure) {
errorStateFlow.value = loginResult.exceptionOrNull()!!.message ?: "login fail"
return@launch
}
val profileResult = userProfileService.getUserProfile()
if (profileResult.isFailure) {
errorStateFlow.value = profileResult.exceptionOrNull()!!.message ?: "get profile fail"
} else {
userProfileStateFlow.value = profileResult.getOrThrow()
}
}
}
}

ถ้าคุณฝืนทำ UseCase คุณจะได้ 3 UseCase ที่ได้แค่ห่อ validateEmail() authService.login() และ userProfileService.getUserProfile() โดยไม่ได้ทำให้ fun login() สั้นลงเลย แถมต้อง inject UseCase เพิ่มวุ่นวายอีก

ด้วยความที่มันต้อง show error snackbar เห็นได้ชัดว่า UseCase class ช่วยไรไม่ได้เลย คุณต้องเขียนซ้ำในทั้ง 2 ViewModel อยู่ดี

แต่เราสามารถใช้ delegation ช่วยแบบนี้ได้

interface LoginActionBase {
val authService: AuthService
val userProfileService: UserProfileService
val userProfileStateFlow: MutableStateFlow<UserProfile?>
val errorStateFlow: MutableStateFlow<String?>
val coroutineScope: CoroutineScope
}

interface LoginAction {
fun LoginActionBase.executeLoginFlow(email: String, password: String)
}

class LoginActionDelegate : LoginAction {
override fun LoginActionBase.executeLoginFlow(email: String, password: String) {
coroutineScope.launch {
if (!validateEmail(email)) {
errorStateFlow.value = "invalid email"
return@launch
}
val loginResult = authService.login(email, password)
if (loginResult.isFailure) {
errorStateFlow.value = loginResult.exceptionOrNull()!!.message ?: "login fail"
return@launch
}
val profileResult = userProfileService.getUserProfile()
if (profileResult.isFailure) {
errorStateFlow.value = profileResult.exceptionOrNull()!!.message ?: "get profile fail"
} else {
userProfileStateFlow.value = profileResult.getOrThrow()
}
}
}
}
  • เริ่มจากประกาศ interface LoginActionBase เพื่อรวบรวมทุกอย่างที่ใช้ในการทำ login flow
  • ประกาศ interface LoginAction มี fun LoginActionBase.executeLoginFlow()
  • implement class LoginActionDelegate : LoginAction แล้วย้าย logic ทั้งหมดมาไว้ใน fun LoginActionBase.executeLoginFlow()
  • เพียงเท่านี้เราก็สามารถห่อ logic login flow ทั้ง API call ทั้ง if-else ทั้ง show error มาเก็บไว้ใน reusable class ละ
class LoginScreenViewModel(
override val authService: AuthService,
override val userProfileService: UserProfileService,
) : LoginActionBase, LoginAction by LoginActionDelegate() {
override val userProfileStateFlow: MutableStateFlow<UserProfile?> = MutableStateFlow(null)
override val errorStateFlow: MutableStateFlow<String?> = MutableStateFlow(null)
override val coroutineScope: CoroutineScope get() = viewModelScope

fun login(email: String, password: String) {
executeLoginFlow(email, password)
}
}

class CartScreenViewModel(
override val authService: AuthService,
override val userProfileService: UserProfileService,
) : LoginActionBase, LoginAction by LoginActionDelegate() {
override val userProfileStateFlow: MutableStateFlow<UserProfile?> = MutableStateFlow(null)
override val errorStateFlow: MutableStateFlow<String?> = MutableStateFlow(null)
override val coroutineScope: CoroutineScope get() = viewModelScope

fun login(email: String, password: String) {
executeLoginFlow(email, password)
}
}

วิธีใช้งานไม่ยากเลย เพียงแค่

  • ให้ LoginScreenViewModel implement LoginActionBase และ inject/ประกาศ property ให้ครบถ้วน
  • ให้ LoginScreenViewModel implement LoginAction แต่ไม่ implement เอง โดยส่งต่อไปให้ by LoginActionDelegate() เป็นคนทำแทน
  • ใน LoginScreenViewModel สามารถเรียกใช้ executeLoginFlow() ได้อย่างง่ายดาย โดยใช้ implementation ของ LoginActionDelegate
  • หาก ViewModel ใดๆอยากใช้ด้วย ก็แค่ทำเหมือนกัน

ขอแสดงความเสียใจกับชาว iOS/Swift ที่หลงมาอ่านด้วยครับ ผมไม่รู้ท่า delegation มันทำยังไงใน Swift

สำหรับใครที่อ่านถึงตรงนี้แล้วยังไม่ซื้อ ยังคิดว่าใน app มันต้องมี 3 layer แน่ๆ อย่าเก็บไว้ครับ กรี๊ดออกมาครับ กรี๊ดออกมาเลย

หยอกๆ ถ้าคุณคิดถึง UseCase ขนาดนั้นผมอนุญาติให้คุณใช้ท่า delegation ได้ แล้ว rename มันให้หมด กลับสู่ UseCase ที่คุ้นเคย

LoginActionBase >>> LoginUseCaseInputOutput/LoginUseCaseDependencies
LoginAction >>> LoginUseCase
LoginActionDelegate >>> LoginUseCaseImpl

หมายเหตุ ถ้าคุณเขียน Flutter สามารถใช้ท่าประมาณนี้ได้ด้วย mixin

แก้ ViewModel บวมด้วย Redux หรือ state management อื่นๆ

MVC/MVP/MVVM ไม่ใช่ pattern เดียวที่ไว้ใช้จัดการ UI กับส่วนอื่นๆของแอพ พวกเราชาว mobile ไม่ค่อยคุ้นกับคำว่า state management หรอก ยกเว้นคุณศึกษา JetpackCompose มาบ้าง แต่หากคุณเป็นสาย web น่าจะคุ้นเคยเป็นอย่างดี

เนื่องจากทั้ง web frontend และ mobile app มันอยู่ใน presentation layer มีหน้าที่จัดการ UI เหมือนกัน เทคนิคพวกนี้มันเอามาประยุกต์ข้ามกันได้ครับ ผมขอยกตัวอย่าง Redux ที่เข้าใจว่าตอนนี้ outdated แล้วมั้ง ผมไม่ได้อัพเดตความรู้ฝั่งเว็บเลยช่วงนี้

คร่าวๆคือ Redux จะมีสิ่งที่เรียกว่า reducer ครับมันคือ function โง่ๆน่ะแหละ ไอเดียคือ newState = reduce(currentState, action) เปรียบเทียบกับท่า ViewModel ก็ประมาณ

  • currentState คือ data ใน StateFlow ในViewModel
  • action คือการที่ UI เรียก function บน ViewModel
  • reduce คือ logic ว่าถ้า action นี้ถูกกระทำใน currentState แบบนี้ จะผลิต newState ออกมายังไง
  • newState ก็คือ dataใหม่ ใน StateFlow ในViewModel หลักจากเกิด action แล้ว

ผมถือโอกาสนี้ encourage พวกคุณศึกษาหาความรู้ต่างสายกันด้วยครับ เผื่อได้ไอเดียใหม่ๆมาแก้ปัญหา

ส่งท้าย

สรุปสั้นตามนี้ครับ

  1. Clean Architecture มีประโยชน์ในการออกแบบทั้งระบบ ถ้าคุณพยายามเอามาใช้ผิด scope ก็จะเกิดความไม่สมเหตุสมผลมากมาย
  2. อย่าทำ Mapper โดยไม่คิด ให้คุณพิจารณาก่อนว่าโปรเจคคุณทำเพื่อประโยชน์อะไร คุ้มกับการ maintenance หรือไม่
  3. การแก้ ViewModel บวมทำได้หลายวิธี ลองใช้ความรู้และจินตนาการออกแบบท่าให้เหมาะกับโปรเจคของคุณ

ขอทักทายผู้อ่านหน่อย ผมรู้มีบางท่านตามเฟสผมอยู่เวลาผมบ่นอะไรก็ตามมา like ขอบคุณที่ยังไม่ลืมกันครับ หวังว่าโพสนี้จะแรงสะใจชดเชยที่หายไป 4 ปี แล้วเจอกันใหม่เมื่อผมว่างหรือผมหัวร้อนจนทนไม่ได้ครับ

ปล. ใครอยากให้ไปช่วย consult project ทัก inbox มาได้ครับ ช่วงนี้เงินบาทขาดแคลน 🙇‍♂️

--

--