O método de execução / caso de uso / interator aceita parâmetros de aceitação

8

Na maioria dos exemplos de arquitetura limpa (principalmente projetos Android), notei que as classes de casos de uso / interator (unidades que encapsulam um recurso) geralmente compartilham a classe / interface base, como aqui ou aqui . Outros não ( como aqui ou aqui ), permitindo que os interatores aceitem alguns parâmetros que são usados ​​para executar alguma lógica.

Uma dessas abordagens é melhor que a outra? Estou particularmente interessado em como a abordagem sem parâmetros lida com casos de uso para algo que requer entrada do usuário, por exemplo - digamos que queremos permitir que o usuário edite uma entidade e passe-a para o servidor. Poderíamos injetar a mesma entidade no caso de uso e em quem quer que a invoque, mas isso teria que ser mutável, para que as mudanças sejam refletidas nos dois lugares. Mas não podemos criar modelos imutáveis, que talvez não desejemos (por causa de problemas de encadeamento, etc.). Como lidar com esse caso?

Perdoe-me se eu usar os termos case / interator incorretamente, mas é assim que eles são usados ​​no Android land, o que, reconhecidamente, pode estar um pouco atrasado nos padrões de design

wasil
fonte

Respostas:

6

Deixe-me reiterar o que você está dizendo para garantir que estamos na mesma página.

Na arquitetura limpa

insira a descrição da imagem aqui

Caso A

As classes de casos de uso / interator geralmente compartilham a classe / interface base, como aqui

  public abstract class UseCase {

  private final ThreadExecutor threadExecutor;
  private final PostExecutionThread postExecutionThread;

  private Subscription subscription = Subscriptions.empty();

  protected UseCase(ThreadExecutor threadExecutor,
      PostExecutionThread postExecutionThread) {
    this.threadExecutor = threadExecutor;
    this.postExecutionThread = postExecutionThread;
  }

  /**
   * Builds an {@link rx.Observable} which will be used when executing the current {@link UseCase}.
   */
  protected abstract Observable buildUseCaseObservable();

  /**
   * Executes the current use case.
   *
   * @param UseCaseSubscriber The guy who will be listen to the observable build
   * with {@link #buildUseCaseObservable()}.
   */
  @SuppressWarnings("unchecked")
  public void execute(Subscriber UseCaseSubscriber) {
    this.subscription = this.buildUseCaseObservable()
        .subscribeOn(Schedulers.from(threadExecutor))
        .observeOn(postExecutionThread.getScheduler())
        .subscribe(UseCaseSubscriber);
  }

  /**
   * Unsubscribes from current {@link rx.Subscription}.
   */
  public void unsubscribe() {
    if (!subscription.isUnsubscribed()) {
      subscription.unsubscribe();
    }
  }
}

e aqui

package cat.ppicas.framework.task;

public interface Task<R, E extends Exception> {

    TaskResult<R, E> execute();

}

Caso B

Outros não, e, em vez disso, permitem que os interatores aceitem alguns parâmetros que são usados ​​para executar alguma lógica.

como aqui

package pl.charmas.shoppinglist.domain.usecase;

public interface UseCase<Result, Argument> {
  Result execute(Argument arg) throws Exception;
}

ou aqui

AbstractInteractor.java
GetMarvelCharactersLimit.java
GetMarvelCharactersLimitImp.java
Get MarvelCharactersPaginated.java Get
MarvelCharactersPaginatedImp.java

Pare

Ambos estão certos.

Compartilhar uma interface ou classe base faz parte da herança.

Aceitar parâmetros para que você possa executar a lógica através deles faz parte da composição.

Uma dessas abordagens é melhor que a outra?

Ambos são melhores no que são bons. Embora um princípio de design popular afirme, "favorece a composição sobre a herança" .

Se eu estou entendendo, você está vendo como a composição e a herança podem permitir o polimorfismo e agora que você o viu, está lutando para escolher entre eles. Dizendo isso na linguagem de padrões: você pode usar o padrão de modelo ou o padrão de estratégia para obter polimorfismo.

A herança fornece polimorfismo por si própria. Assim, menos digitação no teclado. A composição requer que você adicione delegação para expor e conectar a interface ao parâmetro. Isso significa mais digitação. Mas a composição não é estaticamente vinculada, por isso é muito flexível e testável.

O executemétodo de caso / interator de uso deve aceitar parâmetros?

Lembre-se de que esse método x.y()e essa função y(x)são essencialmente os mesmos. Um execute() método está sempre obtendo pelo menos um parâmetro.

Estou particularmente interessado em como a abordagem sem parâmetros lida com casos de uso para algo que requer entrada do usuário, por exemplo - digamos que queremos permitir que o usuário edite uma entidade e passe-a para o servidor. Poderíamos injetar a mesma entidade no caso de uso e em quem quer que a invoque, mas isso teria que ser mutável, para que as mudanças sejam refletidas nos dois lugares. Mas não podemos criar modelos imutáveis, que talvez não desejemos (por causa de problemas de encadeamento, etc.). Como lidar com esse caso?

Bem, agora estamos falando sobre entidades, não casos de uso, nem polimorfismo.

Uma entidade tem um ID. É muito bom tornar uma entidade imutável. Por que você deseja que um usuário edite algo imutável? Por que você deseja que as informações existam em dois lugares? Em caso afirmativo, qual lugar sabe melhor?

Não, é melhor deixar o usuário criar algo novo. Se precisar ter um ID, ele obterá um novo ID exclusivo. Caso contrário, é um objeto de valor. Se houver algo mais com um ID preexistente ao qual essas informações devem ser associadas, crie uma nova associação. Não fique bisbilhotando em entidades imutáveis.

É inteiramente possível modelar um mundo em mudança sem atualizar suas entidades, desde que você tenha espaço para continuar criando coisas que representam o que há de novo.

candied_orange
fonte
Eu ainda tenho algumas dúvidas, no entanto. As duas abordagens, A e B, não são exemplos de herança? E ter classe separada para executar alguma lógica é composição. Não vejo que A é herança e B é composição - você poderia elaborar? Quanto a method is always getting at least one parameter- isso é um detalhe técnico. "O executemétodo deve aceitar mais de um parâmetro", se desejar. E, finalmente, a parte sobre a imutabilidade não resolver o problema, quer, que é "como faço para passar usecase uma nova entidade imutável (que reflete as mudanças do usuário) se o seu método execute não aceita parâmetros?
Wasyl