Como escrever UseCase na arquitetura oficial do Android?

1. Finalidade do Caso de Uso

Na especificação de arquitetura mais recente do Android, a camada de domínio (muitas vezes traduzida como camada de domínio ou camada de domínio de rede) é introduzida . É recomendável usar o UseCase para encapsular alguma lógica de negócios complexa.

A arquitetura Android mais recente: https://developer.android.com/topic/architecture/domain-layer

Na arquitetura MVVM tradicional, estamos acostumados a usar o ViewModel para transportar a lógica de negócios.Com a expansão da escala de negócios, o ViewModel se torna cada vez mais gordo e suas responsabilidades não são claras.

Os princípios de design de separação de preocupações e responsabilidade única (SRP) propostos pela Clean Architecture são amplamente reconhecidos, então o Android introduz o conceito de UseCase na Clean Architecture na arquitetura mais recente. ViewModel pertence à UI Layer, concentrando-se mais no gerenciamento de UiState. A lógica de negócios independente da UI afunda no UseCase. Depois que o UseCase é desacoplado do ViewModel, a lógica comum também pode ser fornecida nos ViewModels.

O conceito de UseCase foi introduzido no código de amostra inicial do aplicativo de tarefas da arquitetura Android. A arquitetura mais recente apenas torna a ideia de UseCase mais clara. Os exemplos de UseCase mais recentes podem ser aprendidos no NIA oficial.

  • todo-app: https://github.com/android/architecture-samples/tree/todo-mvp-clean
  • NIA: https://github.com/android/nowinandroid/tree/main

2. Características do UseCase

O documento oficial considera que o UseCase deve ter as seguintes características:

não mantém o estado

Você pode definir seus próprios tipos de estrutura de dados, mas não pode conter instâncias de estado e trabalhar como uma função pura. É até mesmo recomendado diretamente que você reescreva a lógica no método de chamada e chame a instância como uma função.

Aqui está um exemplo da NIA: GetRecentSearchQueriesUseCase:

responsabilidade única

Cumprindo rigorosamente uma única responsabilidade, um UseCase só faz uma coisa, e até seu nome é um comportamento específico. Uma olhada no diretório de arquivos do UseCase provavelmente saberá a função geral do aplicativo.

Todos os UseCases no seguinte NIA:

dispensável

UseCase é definido como uma função opcional no documento oficial e definido sob demanda. Em um cenário de negócios simples, a interface do usuário tem permissão para acessar diretamente o repositório. Se considerarmos UseCase como a função de UI e isolamento de dados, haverá muitos UseCases de pouco valor no projeto e pode haver apenas uma linha de código chamando Repoitory.

3. Como definir UseCase

Como mencionado acima, embora o documento oficial forneça algumas definições básicas para o UseCase, afinal é um novo conceito, e muitas pessoas ainda se sentem confusas e carecem de orientação efetiva quando realmente escrevem código. Sobre a questão de como definir UseCase, ela precisa ser discutida mais extensivamente para formar um consenso que possa ser referido. Este artigo também nasce com esse propósito, pode ser considerado como uma introdução.

Opcional ou Obrigatório?

Em primeiro lugar, o documento oficial acredita que o UseCase é opcional. Embora sua intenção original seja boa, nem todo mundo quer muitos UseCase One-Liner, mas como uma especificação de arquitetura, evite ambigüidades. O resultado final geralmente é "nada".

Quando o negócio está apenas começando, muitas vezes é definido no Repositório porque é relativamente simples. À medida que a escala do negócio se expande, o UseCase deve ser adicionado adequadamente para encapsular alguma lógica de negócios complexa, mas o custo de reconstrução no projeto real neste momento será tornar os desenvolvedores "preguiçosos" ", o UseCase acabou morrendo.

Que tal abandonar o UseCase? Isso pode causar responsabilidades pouco claras e expansão infinita do Repository, e o Repository geralmente tem mais de um método. A dependência direta do ViewModel no Repository também viola outro princípio importante no SOLID, ISP. O ViewModel será recompilado devido a alterações não relacionadas no Repository.

**ISP (Princípio de Segregação de Interface, Princípio de Segregação de Interface)** requer que a interface seja separada em interfaces menores e mais específicas para que o chamador precise apenas conhecer os métodos que precisa usar. Isso aumenta a flexibilidade e a reutilização do código e reduz as dependências e o acoplamento do código.

Para reduzir o custo do julgamento no estágio inicial e o custo da reconstrução subsequente, se tivermos a expectativa de crescimento contínuo do negócio, podemos considerar o uso do UseCase como uma opção obrigatória. Claro, o melhor é estudar como reduzir o código do template trazido pelo UseCase.

Classe ou objeto?

A sugestão oficial é usar Classpara definir o UseCase, e instanciar um novo objeto toda vez que ele for usado, o que vai causar uma sobrecarga repetitiva, então você pode usar objectpara definir o UseCase?

UseCase pode teoricamente existir como um singleton, mas Class tem as duas vantagens a seguir sobre Object:

  • O UseCase quer funcionar como uma função pura, e a Classe comum pode garantir que uma nova instância seja criada toda vez que for usada, evitando assim problemas como compartilhamento de estado e efeitos colaterais.
  • Classes comuns podem ser injetadas em diferentes repositórios por meio de parâmetros de construção, e o UseCase é mais propício para reutilização e teste de unidade

Se esperamos fortemente que o UseCase tenha um ciclo de vida mais longo, com a ajuda do framework DI, as classes comuns também podem ser facilmente suportadas. Por exemplo, no Dagger, basta adicionar @Singletonanotações

@Singleton
class GetRecentSearchQueriesUseCase @Inject constructor(
    private val recentSearchRepository: RecentSearchRepository,
) {
    
    
    operator fun invoke(limit: Int = 10): Flow<List<RecentSearchQuery>> =
        recentSearchRepository.getRecentSearchQueries(limit)
}

Classe ou Função?

Já que queremos usar o UseCase como uma função, por que não defini Function-lo ? Por exemplo como abaixo

fun GetRecentSearchQueriesUseCase : Flow<List<RecentSearchQuery>> 

Isso segue os princípios do FP, mas perde os benefícios do encapsulamento do OOP:

  • UseCase geralmente precisa depender de objetos Repository, e uma classe UseCase pode encapsular Repository como armazenamento de membros. Uma Função UseCase precisa ser passada pelo chamador através de parâmetros, sem falar no alto custo de uso. Se o tipo ou número de Repositórios dos quais o UseCase depende mudar, o chamador precisa modificá-lo de acordo
  • A função não pode isolar a interface do usuário e os dados, e o ViewModel ainda precisa depender diretamente do Repositório para passar parâmetros para o UseCase
  • UseCase Class pode definir alguns métodos privados, que são mais capazes de implementar lógica complexa do que Function

Pode-se ver que Function não pode substituir Class na definição de UseCase. Claro, Classe também traz algumas desvantagens:

  • A exposição de vários métodos quebra os princípios do SRP. Por isso, a recomendação oficial de usar verb in present tense + noun/what (optional) + UseCaseverbos para nomeação é também para deixar mais claras as responsabilidades.
  • Carregando estado variável, este é o pensamento inercial de todos que escrevem OOP
  • Muito código clichê

Interface de função?

Pela análise anterior, sabemos que a definição de UseCase precisa ter as vantagens de FP e OOP. Isso me fez pensar em Function (SAM) Interface . Function Interface é uma interface de método único, que pode criar um objeto de classe anônimo a baixo custo, garantindo que o objeto possa ter apenas um método e tenha um certo grau de encapsulamento, podendo contar com o Repositório por meio de "fechamento". Além disso, o Kotlin fornece uma maneira simplificada de escrever SAM, o que também reduz o código clichê até certo ponto.

Interfaces funcionais (SAM) :
https://kotlinlang.org/docs/fun-interfaces.html

Use a interface Function para definir o código de GetRecentSearchQueriesUseCase da seguinte forma:

fun interface GetRecentSearchQueriesUseCase : () -> Flow<List<RecentSearchQuery>>

Use-o para criar uma instância de UseCase enquanto implementa a lógica na função

val recentSearchQueriesUseCase = GetRecentSearchQueriesUseCase {
    
    
    //...
}

Como implemento o Repositório na implementação da função? Isso é coletado pelo contêiner DI. O código de amostra oficial usa Hilt para separar ViewModel e UseCase, e ViewModel não se preocupa com os detalhes da criação do UseCase. Abaixo está o código NIA, GetRecentSearchQueriesUseCase é injetado automaticamente SearchViewModelno arquivo .

@HiltViewModel
class SearchViewModel @Inject constructor(
    recentSearchQueriesUseCase: GetRecentSearchQueriesUseCase // UseCase 注入 VM
    //...
) : ViewModel() {
    
     
    //...
}

O GetRecentSearchQueriesUseCase da interface Function não possui construtor e precisa @Moduleser instalado , provideGetRecentSearchQueriesUseCasee o RecentSearchRepository no parâmetro pode ser obtido e usado automaticamente do contêiner.

@Module
@InstallIn(ActivityComponent::class)
object UseCaseModule {
    
    
    @Provides
    fun provideGetRecentSearchQueriesUseCase(recentSearchRepository: RecentSearchRepository) =
        GetRecentSearchQueriesUseCase {
    
     limit ->
            recentSearchRepository.getRecentSearchQueries(limit)
        }
}

Não houve nenhum problema ao usar o Koin como DI container, o código é o seguinte:

single<GetRecentSearchQueriesUseCase> {
    
    
    GetRecentSearchQueriesUseCase {
    
     limit ->
       recentSearchRepository.getRecentSearchQueries(limit) 
    }
}

4. Resumo

Como um novo conceito na estrutura oficial, o UseCase ainda não está profundamente enraizado no coração das pessoas. É necessário explorar continuamente métodos de uso razoáveis. Este artigo fornece algumas reflexões básicas:

  • Considerando a extensibilidade da arquitetura, recomenda-se a introdução forçada de UseCase entre ViewModel e Repository, mesmo que a lógica de negócios atual não seja complicada
  • UseCase não possui estado mutável, mas depende do repositório. Ele precisa ter as características de FP e OOP. É mais adequado usar definição de classe em vez de função
  • Antes de introduzir o UseCase, o framework DI deve ser introduzido para garantir o acoplamento de ViewModel e UseCase.
  • Function Interface é outra maneira de definir UseCase além de Class, que é propício para um código mais funcional

Supongo que te gusta

Origin blog.csdn.net/vitaviva/article/details/130837188
Recomendado
Clasificación