4

This question came to me when I was trying implement Clean Architecture using Scala, and come across this post.

In the accepted answer, @candiedorange emphasis on the separation of responsibility, and do not break "Tell, don't ask" rule.

But this is still confused me.

As far as I remembered, "Tell, don't ask" rule didn't state that we shouldn't pass return data to something. It just states that caller should not make decisions on the inner states of some objects.

For example, In Scala or functional programming, we often call a function to get something, and pass it to another function to get result we want.

For example, if we would like to compute sum of squares of all even number inside a list, we would very much likely doing the following:

def sumList(xs: List[Int]) = ??? 
val xs = List(1, 2, 3, 4, 5, 6, 7, 9, 0)
val evenList = xs.filer(_ % 2 == 0)
val squares = evenList.map(_ * _)
val sum = sumList(evenList)

Did this breaks the rule of "Tell, don't ask"? If not, why would creating an presenter and pass returning data from use case to get a ViewModel breaks this rule?

While I trying to figure out what is best design, I have created three version of my UseCase. I would like to here opinion about which one is a better design.

Version A. Presenter return Unit(void).

First one is just like the proposed solution in that post, and this code. I pass presenter to the use case object, and the interface of presenter returns nothing.

IMHO, this is really not OK. In Scala, we try to minimize mutability and side-effect. But in this case, Presenter must have side-effect inside itself, or it will be totally useless.

trait Presenter[T] {
  def onResult(value: Try[T]): Unit
}

class GetOrderUseCase(request: Request) {

  def execute(presenter: Presenter[Order]): Unit = {
    val result = Try {
      // doing something with request...etc.
    }
    presenter.onResult(result)
  }
}

I abandoned this solution quickly, our presenter must have some kind of side-effect. And it will take much effort to test this use case. I must provide an presenter that does nothing meaningful (yet still have side-effect, or I wouldn't able to retrieve the result from usecase) to the use case.

This solution is also prevents reuse of usecase. What if I need information that combines two use case to render an view? It will still be doable, but not very convenient.

Version B. Presenter return something.

I quickly realized if just accept that presenter has return value. I would able to create an presenter without any side-effect. Better yet, I may not need to create an interface at all. I could just use Scala function object.

So here is my second version:

type Presenter[T, R] = Function1[Try[T], R]

class GetOrderUseCase(request: Request) { 

  def execute[R](presenter: Presenter[Order, R]): R = { 
    // doing something with request...etc.
    val result: Try[Order] = Try { ??? } 
    presenter(result)
  } 
} 

// When test
val useCase = GetOrderUseCase(request)
val orderStatus = useCase.execute { order => order.status }
orderId shouldBe JustCreated

Not bad, but I still could get the point why we should pass presenter to usecase, and not just return simple data from usecase object.

The only advantage of this solution I can think of, is that in the API, we explicitly ask a Presenter must present to use the use case.

But I'm still not convinced that this is right approach. Why? Let's see what will happen if we just let use case return data.

Version C. UseCase return something.

class ViewModel

trait OrderPresenter {
  def present(data: Try[Order]): ViewModel
}

object GUIOrderPresenter extends OrderPresenter {
  def present(data: Try[Order]): ViewModel = {
    // Convert to ViewModel etc...
  }
}

class GetOrderUseCase(request: Request) {
  def execute(): Try[Order] = Try { ??? }
} 

// On controller
val useCase = new GetOrderUseCase(request)
val viewModel = GUIOrderPresenter(useCase.execute)
render(viewModel)

It seems a much better solution to me. And better yet, since our use case is returning an Try[T], we could combine multiple use case by for keyword provided by Scala.

val orderInfoUseCase = new GetOrderUseCase(request)
val shippingInfoUseCase = new ShippingInfo(request)
val viewModel = 
  for {
    orderInfo <- orderInfoUseCase.execute
    shippingInfo <- shippingInfoUseCase.execute
  } yield SomePresenter(orderInfo, shippingInfo)

render(viewModel.recoverWith(SomePresenter.onError))

Questions

Finally, here is my question:

  1. Does my third version implementation breaks the "Tell, don't ask" rule?

    I would argue no. In most situation, it will be only one use case, and all my controller doing is dumb function all.

  2. Dos my third version implementation breaks the CQRS principle?

    I would argue no neither. In this case, our use case simply returning data, did not change any state to the system. If I really want to following CQRS, then I will make sure use case that has a side-effect, should returning nothing.

  3. Does my Controller follow the Humble Object Pattern? I think so. It's so trivial that most time it's not needed to test. Or I could just test it with acceptance test or integration test.

  4. Does my third implementation leaks application responsibility outside application. If it's a yes, an it's because I've called presenter function inside controller. Then why construct a presenter inside controller, does not count as a leak?

Brian Hsu
  • 201
  • 1
  • 5
  • This post suggests *massive* overthinking. Have you tried just writing a simple application? – Robert Harvey Apr 10 '18 at 21:38
  • @RobertHarvey Yes I did. Currently I'm using Version B and Version C for different use case object in my system (which intended to be run on web / android / desktop, so Clean Architecture did have a important role in my system) . Both of them works, and currently I like Solution C more. Just not sure if there is any disadvantage in the respect of system design. – Brian Hsu Apr 10 '18 at 23:37

0 Answers0