Skip to content

Instantly share code, notes, and snippets.

@lievendoclo
Last active October 27, 2023 18:21
Show Gist options
  • Save lievendoclo/89366bd751514498bb74ffa9ca388d26 to your computer and use it in GitHub Desktop.
Save lievendoclo/89366bd751514498bb74ffa9ca388d26 to your computer and use it in GitHub Desktop.
Presenter in Clean Architecture
// shared datastructures
data class ResponseModel(val value: String)
data class JsonResponse(val jsonValue: String)
// example 1
interface FooUseCase {
fun <T> T doSomething(presenter: (ResponseModel -> T))
}
@RestController
@RequestMapping("/bar")
class Controller(val useCase: FooUseCase) {
@GetMapping
fun bar() : JsonResponse {
return useCase.doSomething { responseModel -> JsonResponse(responseModel.value) }
}
}
// example 2
interface FooUseCase {
fun doSomething(presenter: Consumer<ResponseModel>)
}
class JsonPresenter() : Consumer<ResponseModel> {
private var presented : JsonResponse
fun accept(responseModel responseModel) {
presented = JsonResponse(responseModel.value)
}
fun getPresentedState() = presented
}
@RestController
@RequestMapping("/bar")
class Controller(val useCase: FooUseCase) {
@GetMapping
fun bar() : JsonResponse {
val presenter = JsonPresenter()
useCase.doSomething(presenter)
return presenter.getPresentedState();
}
}
// example 3
interface FooUseCase {
fun doSomething(presenter: Consumer<ResponseModel>)
}
class JsonPresenter(val httpResponse: HttpServletResponse) : Consumer<ResponseModel> {
fun accept(responseModel: ResponseModel) {
httpResponse.getWriter().println(ObjectMapper().writeAsString(responseModel))
}
}
@RestController
@RequestMapping("/bar")
class Controller(val useCase: FooUseCase) {
@GetMapping
fun bar(val httpResponse: HttpResponse) {
val presenter = JsonPresenter(httpResponse)
useCase.doSomething(presenter)
}
}
@lievendoclo
Copy link
Author

You can assume that the use case implementation calls the presenter at the end and in the case of example 1 returns its value

@lievendoclo
Copy link
Author

It becomes an issue when the presenter also handles the exception cases.
Then you get something like this:

interface FooUseCase { 
   fun doSomething(presenter: ResponseModelPresenter)
}

interface ResponseModelPresenter {
   fun success(responseModel: ResponseModel) 
   fun failed(reason: String)
}

class JsonPresenter(val httpResponse: HttpServletResponse) : ResponseModelPresenter {
  fun success(responseModel: ResponseModel) {
     httpResponse.getWriter().println(ObjectMapper().writeAsString(responseModel))
  }

  fun failed(reason: String) {
     httpResponse.getWriter().println(ObjectMapper().writeAsString(responseModel))
  }
}

@RestController
@RequestMapping("/bar")
class Controller(val useCase: FooUseCase) {
   @GetMapping
   fun bar(val httpResponse: HttpResponse) {
     val presenter = JsonPresenter(httpResponse)
     useCase.doSomething(presenter)
   }
}

In that case, it's on the use case to call the correct method on the presenter based on whether the happy path succeeded or some error condition happened.

@dominsights
Copy link

Example 3 is following the correct flow of control described by Robert Martin. In the other examples the flow of control returns from the interactor back to the controller. Thank you for providing all examples.

@clemgrim
Copy link

question: why do you pass the presenter to the use case, instead of doing this:

@RestController
@RequestMapping("/bar")
class Controller(val useCase: FooUseCase) {
   @GetMapping
   fun bar(val httpResponse: HttpResponse) {
     val presenter = JsonPresenter(httpResponse)
     val response = useCase.doSomething()

     return presenter.accept(response);
   }
}

I guess I'm missing something :p

@CSalih
Copy link

CSalih commented Jan 27, 2023

Hi @clemgrim, here is a practical example why:
Imagine you have a controller and a cli witch calles doSomething(). If doSomerhing() throws an error you need to handle the exception twice rather then doing this in your use case once.
Giving the presenter to your use case gives you the ability to handle the error as part of your business rules.

doSomething(presenter) {
    try{
         presenter.present(…)
    catch() {
         // … doing some stuff (e.g audit log, …)
         prensenter.presentError(….)
    }
}

There are some violations doing this within the controller, you can read more details about that here Crossing boundaries .

@jadrovski
Copy link

jadrovski commented Oct 27, 2023

Nice!
We are hacking presenting this way...
In usecase class you better return something that can be created by any method of a presenter, just to be sure that all code branches presents something.

public class SomeUseCase {
  public Presented execute(..., Presenter presenter) {
    // we should return something of "Presented" type!
    return presenter.presentSuccess(); // thanks to presenter, it returns that object.
  }
  
  public interface Presenter {
    Presented presentSuccess();
  }
}
public abstract class AbstractPresenter<T> {
  protected final ResponseContainer<T> responseContainer;

  public AbstractPresenter(ResponseContainer<T> responseContainer) {
    this.responseContainer = responseContainer;
  }

  protected Presented setResponse(T response) {
    this.responseContainer.response = response;
    return new Presented();
  }
}

Somewhere in the same package as AbstractPresenter, "Presented" marker class have package-private constructor:

public final class Presented {
  Presented() {
  }
}

So, only AbstractPresenter (and all children of it) can create Presented object and return it to usecase to return it from execute method.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment