MVI 스키마 모델에 반대하는 사례

여기서 제 관점은 제가 개인 프로젝트와 일상 업무에서 서로 다른 MVI를 사용해서 이룬 경험을 바탕으로 합니다.
우선 MVI는 언뜻 보면 매력적이라고 말씀드리겠습니다.명령과 관찰자 같은 작전 테스트 모델을 결합시키고 기능성 반응식 프로그래밍 기술을 충분히 활용했다.간단히 말하면 MVI는 함수가 아닌 지원 클래스를 정의하는데 그 중에서 사용자가 실행할 수 있는 모든 조작은 Input클래스에 의해 정의되고 보기가 있을 수 있는 모든 상태도 State클래스의 하위 클래스에 의해 정의되어야 한다.
나는 네가 이것이 어떻게 작동하는지 이미 알고 있다고 가정한다. MVI에는 Actions, Results 등 비슷한 종류가 더 많다.그 중에서 각 구체적인 실시에 달려 있다.

MVI 질문.


내가 이런 구조에 대한 가장 큰 불만은 그것이 얼마나 단조롭고 무미건조해지거나, 어떤 실현이 얼마나 자기 의견을 고집하는지가 아니다.
내가 본 구조의 가장 큰 문제는 코드의 전체적인 가독성에 얼마나 큰 손해를 끼쳤는지, 그리고 개발자의 행복감에 영향을 미쳤다는 것이다.
내가 보기에 전통적인 MVI는 인위적으로'무관 코드'를 그룹으로 나누고'관련 코드'가 가능한 한 많은 다른 종류 사이에 분포(저결합, 고내집)하도록 장려한다.
이것은 매우 큰 요구입니다. 제 뜻을 설명해 드리겠습니다.
새 코드를 작성할 때, 당신은 작성/읽기 기능에만 관심을 가지고, 다른 내용은 관심을 두지 않습니다.만약 버그를 복구하고 있다면, 버그가 발생하는 특정한 절차에 주의를 기울여 가능한 한 그것을 복제할 것이다.이 특성이 어떻게 작동하는지에 대한 심지 모형을 빨리 세울수록, 오류를 빨리 복구할 수 있다.따라서 이 기능의 코드가 조합되어 다른 기능과 격리되면 도움이 된다.
불행하게도 MVI의 코드는 일반적으로 다음과 같습니다.
    fun intentToAction(intent: HomeIntent): HomeAction {
        return when (intent) {
            is HomeIntent.LoadAllCharacters -> HomeAction.AllCharacters
            is HomeIntent.ClearSearch -> HomeAction.AllCharacters
            is HomeIntent.SearchCharacter -> HomeAction.SearchCharacters(intent.name)
            is ...
        }
    }
당신의 새로운 기능이 어떻게 세션의 다른 전혀 상관없는 기능 사이에 숨겨져 있는지 보십시오.이 방법의 한 줄에만 관심을 가질 수도 있습니다.

We are never, ever, going to read that snippet of code from top to bottom (you know, like we usually read things). So why are we writing it like that? It's hard to write, it's hard to read.


너는 아마도 "응, 그렇게 나쁘지 않아 보인다."라고 생각할 것이다.지금 when 문장에 10줄이나 20줄이 있다고 상상해 보세요. 그들은 기본적으로 듣기에 매우 비슷한 두 종류FooInput -> FooAction의 간단한 시사만 실행합니다.
그러나 이는 이뿐만이 아니다. 많은 실현에서actions에 비추고results에 비추고results에 비춘다states.
fun Result<List<Persona>>.reduce(isSearchMode: Boolean = false): HomeState {
    return when (this) {
        is Result.Success -> if (isSearchMode) HomeState.ResultSearch(data) else HomeState.ResultAllPersona(data)
        is Result.Error -> HomeState.Exception(exception)
        is Result.Loading -> HomeState.Loading
    }
}
그 싫은 버그를 찾으려고 할 때 머릿속의 코드를 어떻게 따를지 생각해 봐라.View에서 시작하여 ViewModel로 건너뛰고 첫 번째 맵(inputs->actions을 봅니다.그곳에서 명확한 경로가 없거나 IDE 단축키를 사용하여 프로세스가 계속되는 곳으로 이동합니다. (구조는 일반적으로 클라이언트에게 배선을 숨기기 때문입니다.)기본적으로 cmd+faction 명칭 (또는 cmd+b 을 만들어서 이 동작이 처리 중인 위치를 찾으려고 시도해야 한다. 이것은 보통 VM에서 하나의 UseCase 을 정의하고, 그 곳에 방사기를 놓는 것을 실현한다. 이것은 정말 어디에나 있을 수 있다.
네, Actions->Results지도를 찾았습니다. 지도에서 관심 있는 선을 찾았습니다. 지금은요?지금, 지금 너 또 뛰었어!이번에 가면 Reducer발견할 수 있을 거야...또 하나의 지도 제작자!이때 너는 자신이 무엇을 하고 있는지 완전히 잊어버렸다.
MVI는 코드를 읽는 것을 매우 어색한 체험으로 만들었다.이것은 책 한 권을 읽는 것과 유사하며, 책 속의 모든 단어가 새로운 페이지에 있다.너는 다음에 무슨 일이 일어날지 알기 위해 끊임없이 페이지를 넘겨야 한다.너는 결국 코드를 읽는 것보다 코드를 찾는 데 더 많은 시간을 쓴다.

이 모든 추가 비용은 버그를 뻔한 곳에 숨기기 쉽다.스레드, RxJava/협동 루트, 비동기 이벤트, 생명주기 범위 등을 고려하면 더욱 까다로워질 수 있다.
그러나 나는 MVI가 어디에서 왔는지 알고 있다. 만약 여러 개Inputs가 같은 Action를 만들어 낸다면 우리는 몇 가지 일을 간소화할 수 있다.불행하게도 실천에서 상황은 거의 이렇지 않았다.당신의 흐름은 Inputs, Actions 또는 Results`를 공유하지 않습니다.응용 프로그램의 모든 '흐름/용례' 에 대해 당신이 가지고 있을 가능성이 더 높다.

Reducer는 허점이 많은 추상적인 개념입니다.


오해하지 마.코드를 다른 종류로 추출할 충분한 이유가 있다.
ViewModels가 좋은 예입니다.AViewModel의 존재는 특정한 체계 구조가 그것을 필요로 하기 때문이 아니라 명확한 목적이 있기 때문이다.이는 뷰에서 비즈니스 논리에 이르는 인터페이스 역할을 하며 뷰 코드를 비뷰 관련 코드와 분리해 더 큰 범위(적어도 안드로이드에서는 VM의 생명주기 범위가 뷰의 범위보다 약간 넓음)를 표시하고 테스트를 편리하게 하는 등 선택한 구조안을 뛰어넘는 조언이 있다.
같은 방식으로 UseCase류의 존재는 용례가 다시 사용할 수 있는 구성 요소이기 때문에 서로 다른 VMs에서 사용할 수 있다.같은 이치Repositories에서 하나의 메모리 라이브러리는 여러 개의 클라이언트에게 독립적으로 서비스를 제공할 수 있다.모든 이 과정들은 명확한 목표, 범위와 목적을 가지고 있다.여기서 논리는 합리적으로 분할되었다.
한편, MVIReducer를 살펴본다.AReducer는 특정 VM에 특정(재사용 불가)되어 있습니다.또한 감속기의 범위는 VM의 범위와 같습니다.왜 그것을 자신의 종류에 추출합니까?
테스트 목적을 위해서.이것은 아직 나의 이유가 될 수 없다.
자신에게 물었다. "나는 정말 개인적인 방법을 하나의 단독 클래스에 추출해야 한다. 단지 그것들을 테스트하기 위해서인가?"저는 개인적으로 이런 방법들이 개인적인 것이고 이유가 있다고 생각하지 않습니다. 그들은 세부적인 것을 실현하는 것이기 때문에 단원 테스트를 직접 해서는 안 됩니다.그렇지 않으면 프로젝트의 모든 종류에서 각각private 방법을 추출하여 테스트하는 것을 막는 것은 무엇입니까?
**클래스가 너무 크고 복잡해지면 클래스를 간소화하는 것이 정상입니다.그러나 내가 보기에 사물을 다른 유형으로 나누는 것은 합리적인 방식으로 진행해야 한다.
내가 말하고 싶은 것은, 너는 각종 코드를 분할하는 방법을 생각해 낼 수 있지만, 결코 모든 방법이 좋은 것은 아니다.
기능/용례/특성을 바탕으로 가상 기기를 분할하는 것은 일반적으로 어떤 임의의 구조를 통해 가상 기기를 분할하는 것보다 좋고 의미도 있다.예를 들어, 다음 두 가지 클래스를 고려합니다.MediaCache: This class handles caching for images and video Reducer: This class reduces results to states (?) .
여기서 Reducer는 View/VM에 추가되는 모든 새로운 기능의 관리 논리를 지원하기 때문에 무한히 증가할 수 있습니다.이 유형의 목적은 매우 추상적이며 체계 구조와 관련이 있다.
다른 한편, 넣을 수 있는 조작이 매우 적다MediaCache는 기능(클래스의 기능)을 바탕으로 정의된 좋은 범위가 있다.물론, 너는 매우 복잡한 cache 메커니즘을 설계할 수 있지만, 너의 모든 코드는 캐시 범위 안에 있다.Reducers와 달리 점점 더 많은 새로운 기능을 추가하면서 범위가 확대될 것입니다.
논리 추출을 자질구레한 클래스에서 테스트에만 사용하면 간단한 테스트가 점점 무의미해지고 코드 라이브러리에 대량의 마찰을 증가시킬 수 있다.개인적인 테스트 방법이 좋지 않은 이유에 대한 정보를 더 많이 읽고 싶다면, 플라기미르 호리코프(Vladimir Khorikov)의 이 글을 강력히 추천합니다. Unit testing private methods
함수reduce가 하나밖에 없고 상태가 없는 클래스가 있다면'왜 이것은 시작하는 클래스입니까?'라고 스스로에게 물어야 한다.함수를 VM으로 이동하기만 하면 인공 클래스로 추출할 필요가 없습니다.그리고 이 함수를 완전히 제거한다. reducing는 기능에서 상하문에서 발생해야지 한 함수에서 인위적으로 조합해서는 안 된다.

끝낼 생각


나는 이런 구조를 좋아하지 않는다. 또 다른 이유가 많다. 그것은 매우 침입적이다. 그것은 대량의 샘플 파일을 추가했다. 아마도 애매모호할 것이다. 이것들은 모두 MVI만의 것이 아니다. 나는 그 중의 일부 단점을 참을 수 있다.나는 MVI가 일부 응용 프로그램에 더 적합할 수 있다고 믿지만, 모든 상황에서 가독성이 영향을 받을 수 있다.
한마디로 이론적으로 MVI는 우아해 보이지만 대형 프로젝트에 진정으로 적용되는 실현은 아직 보지 못했다.
Jetpack compose가 곧 출시되서 기쁩니다!지금까지 저는 사용자 정의 보기 + 보기 모델의 차원 구조in my apps를 사용해 왔습니다(이것은 구글이 Jetpack Compose에서 사용한 조합 가능한 방법과 유사합니다).그의 방법이 곧 더욱 주류로 변할 것이라는 것을 알게 되어 기쁘다.
Website

편집:


어떤 사람이 나에게 모든 실현이 Reducer를 자신의 클래스에서 추출하는 것은 아니라고 말했다.나는 이것이 좋은 일이라고 생각한다.그럼에도 불구하고 대부분의 MVI 구현은 같은 함수/클래스에서 모든 기능을 줄일 필요가 없다(거대한 WHE 문장을 초래하고 관련되지 않은 기능을 혼합하며 관련되지 않은 코드를 그룹화하고 전체 내용을 읽기 어렵게 만든다).
조합 가능한 하위 뷰 모델을 사용하면 큰 when statements/mappers가 나타나지 않는다는 것도 알려졌다.내가 보기에, 이 when statements 를 더 작은 파일로 나누는 것은 근본적인 문제를 해결할 수 없고, 더 많은 샘플 파일만 추가할 뿐이다.더 많은 과정은 단지 너를 다른 곳으로 재정비할 뿐이다.쓴 내용과 수직으로 코드를 읽고 있습니다. (다른 곳으로 이동하기 전에 한 줄씩)마찬가지로, 너는 결국 코드를 찾는 데 실제 코드를 읽는 것보다 더 많은 시간을 쓴다.
마지막으로 나는 이 구조를 보았다. https://github.com/orbit-mvi/orbit-mvi
나는 시도해 본 적이 없지만, 나는 내가 본 것을 좋아한다. 그것은 많은 MVI가 정한 제한을 버리고 의미 있는 제한을 보류했다.특징 흐름은 선형으로 읽히고 상태 축소는 상하문에서 이루어진다(하나의 큰 축소기 함수/클래스로 나누는 것이 아니라).

좋은 웹페이지 즐겨찾기