發現 Android Clean Architecture 的優勢
已發表: 2022-03-11你更喜歡什麼:為架構糟糕但運行良好的應用程序添加新功能,或者修復架構良好但有漏洞的 Android 應用程序中的錯誤? 就個人而言,我肯定會選擇第二個選項。 考慮到每個類中所有內容的所有依賴關係,添加一個新功能,即使是一個簡單的功能,在應用程序中也會變得非常費力。 我記得我的一個 Android 項目,項目經理讓我添加一個小功能——比如下載數據並將其顯示在新屏幕上。 這是我的一位找到新工作的同事編寫的應用程序。 該功能不應超過半個工作日。 我非常樂觀……
在對應用程序如何工作、有哪些模塊以及它們如何相互通信進行了 7 個小時的調查後,我對該功能進行了一些試用實現。 那是地獄。 數據模型的微小變化迫使登錄屏幕發生重大變化。 添加網絡請求需要更改幾乎所有屏幕和GodOnlyKnowsWhatThisClassDoes
類的實現。 將數據保存到數據庫或整個應用程序崩潰時,按鈕顏色更改會導致奇怪的行為。 第二天中途,我告訴我的項目經理,“我們有兩種方法來實現這個功能。 首先,我可以多花三天時間,最後會以非常骯髒的方式實現它,並且每個下一個功能或錯誤修復的實現時間都會成倍增長。 或者,我可以重寫應用程序。 這將需要我兩三週的時間,但我們會為未來的應用程序更改節省時間。”幸運的是,他同意了第二種選擇。 如果我曾經懷疑為什麼應用程序中的良好軟件架構(即使是非常小的一個)很重要,這個應用程序完全消除了它們。 但是我們應該使用哪種 Android 架構模式來避免此類問題呢?
在本文中,我想向您展示一個 Android 應用程序中的干淨架構示例。 然而,這種模式的主要思想可以適應每個平台和語言。 好的架構應該獨立於平台、語言、數據庫系統、輸入或輸出等細節。
示例應用
我們將創建一個簡單的 Android 應用程序來註冊我們的位置,並具有以下功能:
- 用戶可以創建一個具有名稱的帳戶。
- 用戶可以編輯帳戶名稱。
- 用戶可以刪除該帳戶。
- 用戶可以選擇活動帳戶。
- 用戶可以保存位置。
- 用戶可以看到用戶的位置列表。
- 用戶可以看到用戶列表。
清潔架構
這些層是乾淨架構的主要核心。 在我們的應用程序中,我們將使用三層:表示層、域和模型。 每一層都應該分開,不需要知道其他層。 它應該存在於自己的世界中,並且最多共享一個小接口進行通信。
層職責:
- Domain:包含我們應用的業務規則。 它應該提供反映我們應用程序功能的用例。
- 展示:向用戶展示數據,並收集必要的數據,如用戶名。 這是一種輸入/輸出。
- 模型:為我們的應用程序提供數據。 負責從外部獲取數據,並保存到數據庫、雲服務器等。
哪些層應該知道其他層? 獲得答案的最簡單方法是考慮變化。 讓我們以表示層為例——我們將向用戶展示一些東西。 如果我們在表示中改變一些東西,我們是否也應該在模型層中做出改變? 想像一下,我們有一個“用戶”屏幕,其中包含用戶的姓名和最後的位置。 如果我們想呈現用戶的最後兩個位置而不是只顯示一個,我們的模型應該不會受到影響。 所以,我們有第一個原則:表示層不知道模型層。
而且,相反——模型層應該知道表示層嗎? 再說一次——不,因為如果我們將數據源從數據庫更改為網絡,它不應該改變 UI 中的任何內容(如果您考慮在這裡添加一個加載器——是的,但我們也可以有一個 UI 加載器使用數據庫時)。 所以這兩層是完全分開的。 偉大的!
領域層呢? 它是最重要的,因為它包含所有主要的業務邏輯。 這是我們想要在將數據傳遞給模型層或將其呈現給用戶之前處理數據的地方。 它應該獨立於任何其他層——它對數據庫、網絡或用戶界面一無所知。 由於這是核心,其他層將只與這一層通信。 為什麼我們要讓這個完全獨立? 與 UI 設計或數據庫或網絡存儲中的某些內容相比,業務規則的更改可能更少。 我們將通過一些提供的接口與該層進行通信。 它不使用任何具體的模型或 UI 實現。 這些都是細節,記住——細節會改變。 一個好的架構並不局限於細節。
理論到此為止。 讓我們開始編碼吧! 本文圍繞代碼展開,因此——為了更好地理解——您應該從 GitHub 下載代碼並檢查其中的內容。 創建了三個 Git 標籤 —architecture_v1、architecture_v2 和 architecture_v3,它們對應於文章的各個部分。
應用技術
在應用程序中,我使用 Kotlin 和 Dagger 2 進行依賴注入。 這裡既不需要 Kotlin 也不需要 Dagger 2,但它使事情變得容易得多。 你可能會對我不使用 RxJava(也不是 RxKotlin)感到驚訝,但我發現它在這裡沒有用,而且我不喜歡使用任何庫,只是因為它在頂部並且有人說它是必須的。 正如我所說的——語言和庫是細節,所以你可以使用你想要的。 還使用了一些 Android 單元測試庫:JUnit、Robolectric 和 Mockito。
領域
在我們的 Android 應用架構設計中,最重要的層是領域層。 讓我們從它開始。 這就是我們的業務邏輯和與其他層通信的接口所在的地方。 主要核心是UseCase
,它反映了用戶可以使用我們的應用程序做什麼。 讓我們為它們準備一個抽象:
abstract class UseCase<out Type, in Params> { private var job: Deferred<OneOf<Failure, Type>>? = null abstract suspend fun run(params: Params): OneOf<Failure, Type> fun execute(params: Params, onResult: (OneOf<Failure, Type>) -> Unit) { job?.cancel() job = async(CommonPool) { run(params) } launch(UI) { val result = job!!.await() onResult(result) } } open fun cancel() { job?.cancel() } open class NoParams }
我決定在這裡使用 Kotlin 的協程。 每個UseCase
都必須實現一個 run 方法來提供數據。 該方法在後台線程調用,收到結果後,在UI線程上傳遞。 返回的類型是OneOf<F, T>
——我們可以用數據返回錯誤或成功:
sealed class OneOf<out E, out S> { data class Error<out E>(val error: E) : OneOf<E, Nothing>() data class Success<out S>(val data: S) : OneOf<Nothing, S>() val isSuccess get() = this is Success<S> val isError get() = this is Error<E> fun <E> error(error: E) = Error(error) fun <S> success(data: S) = Success(data) fun oneOf(onError: (E) -> Any, onSuccess: (S) -> Any): Any = when (this) { is Error -> onError(error) is Success -> onSuccess(data) } }
領域層需要自己的實體,所以下一步是定義它們。 我們現在有兩個實體: User
和UserLocation
:
data class User(var id: Int? = null, val name: String, var isActive: Boolean = false) data class UserLocation(var id: Int? = null, val latitude: Double, val longitude: Double, val time: Long, val userId: Int)
現在我們知道要返回什麼數據,我們必須聲明數據提供者的接口。 這些將是IUsersRepository
和ILocationsRepository
。 它們必須在模型層中實現:
interface IUsersRepository { fun setActiveUser(userId: Int): OneOf<Failure, User> fun getActiveUser(): OneOf<Failure, User?> fun createUser(user: User): OneOf<Failure, User> fun removeUser(userId: Int): OneOf<Failure, User?> fun editUser(user: User): OneOf<Failure, User> fun users(): OneOf<Failure, List<User>> } interface ILocationsRepository { fun locations(userId: Int): OneOf<Failure, List<UserLocation>> fun addLocation(location: UserLocation): OneOf<Failure, UserLocation> }
這組操作應該足以為應用程序提供必要的數據。 在這個階段,我們不決定如何存儲數據——這是一個我們希望獨立的細節。 目前,我們的領域層甚至不知道它在 Android 上。 我們將嘗試保持這種狀態(有點。我稍後會解釋)。
最後一步(或幾乎最後一步)是為我們的UseCase
定義實現,這將由演示數據使用。 它們都非常簡單(就像我們的應用程序和數據一樣簡單)——它們的操作僅限於從存儲庫中調用適當的方法,例如:
class GetLocations @Inject constructor(private val repository: ILocationsRepository) : UseCase<List<UserLocation>, UserIdParams>() { override suspend fun run(params: UserIdParams): OneOf<Failure, List<UserLocation>> = repository.locations(params.userId) }
Repository
抽象使我們的UseCases
非常容易測試——我們不必關心網絡或數據庫。 它可以以任何方式模擬,因此我們的單元測試將測試實際用例,而不是其他不相關的類。 這將使我們的單元測試簡單快速:
@RunWith(MockitoJUnitRunner::class) class GetLocationsTests { private lateinit var getLocations: GetLocations private val locations = listOf(UserLocation(1, 1.0, 1.0, 1L, 1)) @Mock private lateinit var locationsRepository: ILocationsRepository @Before fun setUp() { getLocations = GetLocations(locationsRepository) } @Test fun `should call getLocations locations`() { runBlocking { getLocations.run(UserIdParams(1)) } verify(locationsRepository, times(1)).locations(1) } @Test fun `should return locations obtained from locationsRepository`() { given { locationsRepository.locations(1) }.willReturn(OneOf.Success(locations)) val returnedLocations = runBlocking { getLocations.run(UserIdParams(1)) } returnedLocations shouldEqual OneOf.Success(locations) } }
目前,領域層已經完成。
模型
作為一名 Android 開發人員,您可能會選擇 Room,這是用於存儲數據的新 Android 庫。 但是讓我們想像一下,項目經理問你是否可以推遲關於數據庫的決定,因為管理層正試圖在 Room、Realm 和一些新的、超快速的存儲庫之間做出決定。 我們需要一些數據來開始使用 UI,所以我們暫時將其保存在內存中:
class MemoryLocationsRepository @Inject constructor(): ILocationsRepository { private val locations = mutableListOf<UserLocation>() override fun locations(userId: Int): OneOf<Failure, List<UserLocation>> = OneOf.Success(locations.filter { it.userId == userId }) override fun addLocation(location: UserLocation): OneOf<Failure, UserLocation> { val addedLocation = location.copy(id = locations.size + 1) locations.add(addedLocation) return OneOf.Success(addedLocation) } }
介紹
兩年前,我寫了一篇關於 MVP 作為 Android 非常好的應用程序結構的文章。 當 Google 發布了偉大的架構組件,這使得 Android 應用程序開髮變得更加容易時,MVP 不再需要並且可以被 MVVM 取代; 然而,這種模式的一些想法仍然非常有用——比如關於啞視圖的想法。 他們應該只關心顯示數據。 為此,我們將使用 ViewModel 和 LiveData。
我們應用程序的設計非常簡單——一個帶有底部導航的活動,其中兩個菜單條目顯示locations
片段或users
片段。 在這些視圖中,我們使用 ViewModels,而後者又使用來自領域層的UseCase
,從而保持通信整潔和簡單。 例如,這裡是LocationsViewModel
:
class LocationsViewModel @Inject constructor(private val getLocations: GetLocations, private val saveLocation: SaveLocation) : BaseViewModel() { var locations = MutableLiveData<List<UserLocation>>() fun loadLocations(userId: Int) { getLocations.execute(UserIdParams(userId)) { it.oneOf(::handleError, ::handleLocationsChange) } } fun saveLocation(location: UserLocation, onSaved: (UserLocation) -> Unit) { saveLocation.execute(UserLocationParams(location)) { it.oneOf(::handleError) { location -> handleLocationSave(location, onSaved) } } } private fun handleLocationSave(location: UserLocation, onSaved: (UserLocation) -> Unit) { val currentLocations = locations.value?.toMutableList() ?: mutableListOf() currentLocations.add(location) this.locations.value = currentLocations onSaved(location) } private fun handleLocationsChange(locations: List<UserLocation>) { this.locations.value = locations } }
對那些不熟悉 ViewModel 的人稍微解釋一下——我們的數據存儲在位置變量中。 當我們從getLocations
用例中獲取數據時,它們被傳遞給LiveData
值。 此更改將通知觀察者,以便他們可以做出反應並更新他們的數據。 我們為片段中的數據添加一個觀察者:
class LocationsFragment : BaseFragment() { ... private fun initLocationsViewModel() { locationsViewModel = ViewModelProviders.of(activity!!, viewModelFactory)[LocationsViewModel::class.java] locationsViewModel.locations.observe(this, Observer<List<UserLocation>> { showLocations(it ?: emptyList()) }) locationsViewModel.error.observe(this, Observer<Failure> { handleError(it) }) } private fun showLocations(locations: List<UserLocation>) { locationsAdapter.locations = locations } private fun handleError(error: Failure?) { toast(R.string.user_fetch_error).show() } }
在每次位置更改時,我們只需將新數據傳遞給分配給回收器視圖的適配器——這就是在回收器視圖中顯示數據的正常 Android 流程的去向。

因為我們在視圖中使用 ViewModel,所以它們的行為也很容易測試——我們可以只模擬 ViewModel,而不關心數據源、網絡或其他因素:
@RunWith(RobolectricTestRunner::class) @Config(application = TestRegistryRobolectricApplication::class) class LocationsFragmentTests { private var usersViewModel = mock(UsersViewModel::class.java) private var locationsViewModel = mock(LocationsViewModel::class.java) lateinit var fragment: LocationsFragment @Before fun setUp() { UsersViewModelMock.intializeMock(usersViewModel) LocationsViewModelMock.intializeMock(locationsViewModel) fragment = LocationsFragment() fragment.viewModelFactory = ViewModelUtils.createFactoryForViewModels(usersViewModel, locationsViewModel) startFragment(fragment) } @Test fun `should getActiveUser on start`() { Mockito.verify(usersViewModel).getActiveUser() } @Test fun `should load locations from active user`() { usersViewModel.activeUserId.value = 1 Mockito.verify(locationsViewModel).loadLocations(1) } @Test fun `should display locations`() { val date = Date(1362919080000)//10-03-2013 13:38 locationsViewModel.locations.value = listOf(UserLocation(1, 1.0, 2.0, date.time, 1)) val recyclerView = fragment.find<RecyclerView>(R.id.locationsRecyclerView) recyclerView.measure(100, 100) recyclerView.layout(0,0, 100, 100) val adapter = recyclerView.adapter as LocationsListAdapter adapter.itemCount `should be` 1 val viewHolder = recyclerView.findViewHolderForAdapterPosition(0) as LocationsListAdapter.LocationViewHolder viewHolder.latitude.text `should equal` "Lat: 1.0" viewHolder.longitude.text `should equal` "Lng: 2.0" viewHolder.locationDate.text `should equal` "10-03-2013 13:38" } }
您可能會注意到表示層也被分成具有清晰邊界的較小層。 像activities
、 fragments
、 ViewHolders
等視圖只負責顯示數據。 他們只知道 ViewModel 層——並且只使用它來獲取或發送用戶和位置。 它是一個與域通信的 ViewModel。 視圖的 ViewModel 實現與域的 UseCases 相同。 套用一句話,乾淨的架構就像一個洋蔥——它有層,層也可以有層。
依賴注入
我們已經為我們的架構創建了所有類,但還有一件事要做——我們需要將所有東西連接在一起的東西。 表示層、域層和模型層保持乾淨,但我們需要一個模塊,它是骯髒的,並且將了解所有事物的一切——通過這種知識,它將能夠連接我們的層。 實現它的最好方法是使用一種常見的設計模式(SOLID 中定義的干淨代碼原則之一)——依賴注入,它為我們創建適當的對象並將它們注入到所需的依賴中。 我在這裡使用了 Dagger 2(在項目中間,我將版本更改為 2.16,它的樣板更少),但您可以使用任何您喜歡的機制。 最近玩了一下Koin庫,覺得也值得一試。 我想在這裡使用它,但是在測試時模擬 ViewModel 時遇到了很多問題。 我希望我能找到一種方法來快速解決它們——如果是這樣,我可以在使用 Koin 和 Dagger 2 時展示這個應用程序的不同之處。
您可以在 GitHub 上使用標籤 architecture_v1 檢查此階段的應用程序。
變化
我們完成了圖層,測試了應用程序——一切正常! 除了一件事——我們仍然需要知道我們的 PM 想要使用什麼數據庫。 假設他們來找你說管理層同意使用 Room,但他們仍然希望將來有可能使用最新的超高速庫,所以你需要記住潛在的變化。 此外,其中一位利益相關者詢問數據是否可以存儲在雲中,並想知道這種更改的成本。 因此,現在是檢查我們的架構是否良好以及我們是否可以在不更改表示或域層的情況下更改數據存儲系統的時候了。
變化 1
使用 Room 的第一件事是為數據庫定義實體。 我們已經有了一些: User
和UserLocation
。 我們所要做的就是添加像@Entity
和@PrimaryKey
這樣的註解,然後我們就可以在我們的模型層和數據庫中使用它。 偉大的! 這是打破我們想要保留的所有架構規則的絕佳方式。 實際上,域實體不能通過這種方式轉換為數據庫實體。 想像一下,我們還想從網絡下載數據。 我們可以使用更多的類來處理網絡響應——轉換我們的簡單實體以使它們與數據庫和網絡一起工作。 這是通向未來災難的最短路徑(並且哭著說:“到底是誰寫了這段代碼?”)。 對於我們使用的每種數據存儲類型,我們都需要單獨的實體類。 它不會花費太多,所以讓我們正確定義 Room 實體:
@Entity data class UserEntity( @PrimaryKey(autoGenerate = true) var id: Long?, @ColumnInfo(name = "name") var name: String, @ColumnInfo(name = "isActive") var isActive: Boolean = false ) @Entity(foreignKeys = [ ForeignKey(entity = UserEntity::class, parentColumns = [ "id" ], childColumns = [ "userId" ], onDelete = CASCADE) ]) data class UserLocationEntity( @PrimaryKey(autoGenerate = true) var id: Long?, @ColumnInfo(name = "latitude") var latitude: Double, @ColumnInfo(name = "longitude") var longitude: Double, @ColumnInfo(name = "time") var time: Long, @ColumnInfo(name = "userId") var userId: Long )
如您所見,它們與域實體幾乎相同,因此很容易合併它們。 這只是一個意外——數據越複雜,相似度就會越小。
接下來,我們必須實現UserDAO
和UserLocationsDAO
,我們的AppDatabase
,最後是IUsersRepository
和ILocationsRepository
的實現。 這裡有一個小問題—— ILocationsRepository
應該返回一個UserLocation
,但它從數據庫接收一個UserLocationEntity
。 與User
相關的類也是如此。 相反,當數據庫需要UserLocationEntity
UserLocation
為了解決這個問題,我們需要域和數據實體之間的Mappers
器。 我使用了我最喜歡的 Kotlin 功能之一——擴展。 我創建了一個名為Mapper.kt
的文件,並將類之間映射的所有方法都放在那裡(當然,它在模型層——域不需要它):
fun User.toEntity() = UserEntity(id?.toLong(), name, isActive) fun UserEntity.toUser() = User(this.id?.toInt(), name, isActive) fun UserLocation.toEntity() = UserLocationEntity(id?.toLong(), latitude, longitude, time, userId.toLong()) fun UserLocationEntity.toUserLocation() = UserLocation(id?.toInt(), latitude, longitude, time, userId.toInt())
我之前提到的小謊言是關於域實體的。 我寫道,他們對 Android 一無所知,但這並不完全正確。 我將@Parcelize
註釋添加到User
實體並在那裡擴展Parcelable
,從而可以將實體傳遞給片段。 對於更複雜的結構,我們應該提供視圖層自己的數據類,並在域和數據模型之間創建映射器。 將Parcelable
添加到域實體是我敢於冒的一個小風險——我知道這一點,如果User
實體發生任何更改,我將為表示創建單獨的數據類並從域層中刪除Parcelable
。
最後要做的是更改我們的依賴注入模塊以提供新創建的Repository
實現而不是以前的MemoryRepository
。 在我們構建並運行應用程序之後,我們可以去 PM 顯示帶有 Room 數據庫的工作應用程序。 我們還可以通知 PM,添加網絡不會花費太多時間,並且我們對管理層想要的任何存儲庫都是開放的。 您可以檢查哪些文件已更改——僅是模型層中的文件。 我們的架構真的很整潔! 每個下一個存儲類型都可以以相同的方式構建,只需擴展我們的存儲庫並提供適當的實現。 當然,我們可能需要多個數據源,例如數據庫和網絡。 然後怎樣呢? 沒什麼,我們只需要創建三個存儲庫實現——一個用於網絡,一個用於數據庫,一個用於選擇正確的數據源(例如,如果我們有一個網絡,從網絡,如果沒有,則從數據庫加載)。
您可以使用標籤 architecture_v2 在 GitHub 上查看此階段的應用程序。
所以,這一天快結束了——你正坐在電腦前喝杯咖啡,準備將應用程序發送到 Google Play,突然項目經理來問你“你能添加一個功能嗎?可以從 GPS 中保存用戶的當前位置嗎?”
變化 2
一切都在改變……尤其是軟件。 這就是為什麼我們需要乾淨的代碼和乾淨的架構。 但是,如果我們不假思索地編碼,即使是最乾淨的東西也可能是骯髒的。 實現從 GPS 獲取位置時的第一個想法是在活動中添加所有位置感知代碼,在我們的SaveLocationDialogFragment
中運行它並使用相應的數據創建一個新的UserLocation
。 這可能是最快的方法。 但是,如果我們瘋狂的 PM 來找我們並要求我們將位置從 GPS 轉移到其他提供商(例如,藍牙或網絡之類的東西)怎麼辦? 這些變化很快就會失控。 我們怎樣才能以乾淨的方式做到這一點?
用戶位置是數據。 獲取位置是一個UseCase
——所以我認為我們的領域和模型層也應該在這裡涉及。 因此,我們還有一個UseCase
需要實現GetCurrentLocation
。 我們還需要一些可以為我們提供位置的東西——一個ILocationProvider
接口,以使UseCase
獨立於諸如 GPS 傳感器之類的細節:
interface ILocationProvider { fun getLocation(): OneOf<Failure, SimpleLocation> fun cancel() } class GetCurrentLocation @Inject constructor(private val locationProvider: ILocationProvider) : UseCase<SimpleLocation, UseCase.NoParams>() { override suspend fun run(params: NoParams): OneOf<Failure, SimpleLocation> = locationProvider.getLocation() override fun cancel() { super.cancel() locationProvider.cancel() } }
你可以看到我們這裡還有一種方法——取消。 這是因為我們需要一種方法來取消 GPS 位置更新。 我們在模型層中定義的Provider
實現在這裡:
class GPSLocationProvider constructor(var activity: Activity) : ILocationProvider { private var locationManager: LocationManager? = null private var locationListener: GPSLocationListener? = null override fun getLocation(): OneOf<Failure, SimpleLocation> = runBlocking { val grantedResult = getLocationPermissions() if (grantedResult.isError) { val error = (grantedResult as OneOf.Error<Failure>).error OneOf.Error(error) } else { getLocationFromGPS() } } private suspend fun getLocationPermissions(): OneOf<Failure, Boolean> = suspendCoroutine { Dexter.withActivity(activity) .withPermission(Manifest.permission.ACCESS_FINE_LOCATION) .withListener(PermissionsCallback(it)) .check() } private suspend fun getLocationFromGPS(): OneOf<Failure, SimpleLocation> = suspendCoroutine { locationListener?.unsubscribe() locationManager = activity.getSystemService(Context.LOCATION_SERVICE) as LocationManager locationManager?.let { manager -> locationListener = GPSLocationListener(manager, it) launch(UI) { manager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0L, 0.0f, locationListener) } } } override fun cancel() { locationListener?.unsubscribe() locationListener = null locationManager = null } }
該提供程序已準備好與 Kotlin 協程一起使用。 如果你還記得的話, UseCase
的 run 方法是在後台線程上調用的——所以我們必須確保並正確地標記我們的線程。 正如你所看到的,我們必須在這里傳遞一個活動——當我們不再需要它們以避免內存洩漏時,取消更新和從監聽器註銷是至關重要的。 由於它實現了ILocationProvider
,我們可以在未來輕鬆地將其修改為其他提供者。 我們還可以輕鬆地測試當前位置的處理(自動或手動),即使沒有在我們的手機中啟用 GPS——我們所要做的就是替換實現以返回一個隨機構建的位置。 為了讓它工作,我們必須將新創建的UseCase
添加到LocationsViewModel
中。 反過來,ViewModel 必須有一個新方法getCurrentLocation
,它將實際調用用例。 只需對 UI 進行一些小的更改即可調用它並在 Dagger 中註冊 GPSProvider——瞧,我們的應用程序就完成了!
概括
我試圖向您展示我們如何開發一個易於維護、測試和更改的 Android 應用程序。 它也應該很容易理解——如果有人新加入你的工作,他們在理解數據流或結構方面應該沒有問題。 如果他們知道架構是乾淨的,他們可以確定 UI 中的更改不會影響模型中的任何內容,並且添加新功能不會超出預期。 但這並不是旅程的終點。 即使我們有一個結構良好的應用程序,也很容易被混亂的代碼更改破壞它“只是暫時,只是為了工作”。 請記住——沒有“暫時”的代碼。 每個違反我們規則的代碼都可以保留在代碼庫中,並且可能成為未來更大的中斷的來源。 如果您在一周後看到該代碼,看起來有人在該代碼中實現了一些強依賴關係,並且要解決它,您必須深入研究應用程序的許多其他部分。 一個好的代碼架構不僅在項目開始時是一個挑戰,它對 Android 應用程序生命週期的任何部分都是一個挑戰。 每次發生變化時都應該考慮和檢查代碼。 要記住這一點,例如,您可以打印並掛起您的 Android 架構圖。 您還可以通過將它們分成三個 Gradle 模塊來稍微強制層的獨立性,其中域模塊不知道其他模塊,並且表示和模型模塊不相互使用。 但即使這樣也不能取代這樣一種意識,即應用程序代碼中的混亂會在我們最意想不到的時候報復我們。