ddd-forum icon indicating copy to clipboard operation
ddd-forum copied to clipboard

Why usecase controller is extending an infrastructure class?

Open imsergiobernal opened this issue 6 years ago • 7 comments

App layer shouldn't be aware of infrastructure implementation, why we extend an infrastructure implementation class?

I thought that the usecase controller shouldn't know about if the usecase is implemented through websocket, HTTP, TCP/IP or Message Broker; this makes the BaseController coupled to the HTTP protocol (VERBS).

I think the idea would be to return always a Result<ControllerResponse>, develop a infrastructure implementation like HTTPRequestHandler (which requires a UseCase's Controller) with HTTP verbs as methods, and then reuse it by Express, Koa... even at the same runtime or not. (progressive library migration, performance testing...)

We could create another TCPRequestController, MQRequestController, which all of them requires the same as HTTPRequestHandler.

export interface IRequestHandler<RQ> {
    req: RQ;

    handle<RQ>(req: RQ, controller: any): void;
    handle<RQ, RS>(req: RQ, controller: any, res: RS): void;
}
export interface IHttpRequestHandler<RQ, RS> extends IRequestHandler<RQ> {
  res: RS;
}
export abstract class HttpRequestHandler<RQ, RS> implements IHttpRequestHandler<RQ, RS>/*, IHttpRequestHandlerResponses*/ {
  req: any;
  res: any;
  controller: any;

  public handle<RQ, RS>(req: RQ, controller: any, res?: RS): void {
    this.req = req;
    this.res = res;
    this.controller = controller;

    this.controller.call();
  };

  abstract jsonResponse(): any;
}

HttpRequestHandler could have all HTTP verbs implemented or just leave the responsability for the ending library implementation:

export interface IExpressRouteHandler extends IHttpRequestHandler<express.Request, express.Response> {}

export abstract class ExpressRouteHandler extends HttpRequestHandler<express.Request, express.Response> {
  req: express.Request;
  res: express.Response;

  public static jsonResponse(res: express.Response, code: number, message: string) {
    return res.status(code).json({ message });
  }

  public ok(res: express.Response, dto?: string) {
    if(!!dto) {
      return res.status(200).json(dto);
    } else {
      return res.sendStatus(200);
    }
  }

  public created<T>(res: express.Response) {
    return res.sendStatus(201);
  }

  public clientError(message?: string) {
    return ExpressRouteHandler.jsonResponse(this.res, 400, message ? message : 'Unauthorized');
  }

  public unauthorized(message?: string) {
    return ExpressRouteHandler.jsonResponse(this.res, 401, message ? message : 'Unauthorized');
  }

  public paymentRequired(message?: string) {
    return ExpressRouteHandler.jsonResponse(this.res, 402, message ? message : 'Payment required');
  }

  public forbidden(message?: string) {
    return ExpressRouteHandler.jsonResponse(this.res, 403, message ? message : 'Forbidden');
  }

  public notFound(message?: string) {
    return ExpressRouteHandler.jsonResponse(this.res, 404, message ? message : 'Not found');
  }

  public conflict(message?: string) {
    return ExpressRouteHandler.jsonResponse(this.res, 409, message ? message : 'Conflict');
  }

  public tooMany(message?: string) {
    return ExpressRouteHandler.jsonResponse(this.res, 429, message ? message : 'Too many requests');
  }

  public todo() {
    return ExpressRouteHandler.jsonResponse(this.res, 400, 'TODO');
  }

  public fail(error: Error | string) {
    console.log(error);
    return this.res.status(500).json({
      message: error.toString()
    });
  }
}

I tried to deal with this but I've found a concern: What kind of response should the usecase controller return?

Here's my alpha repository https://github.com/imsergiobernal/efecto-kettlebell

imsergiobernal avatar Oct 17 '19 08:10 imsergiobernal

@imsergiobernal

App layer shouldn't be aware of infrastructure implementation, why we extend an infrastructure implementation class?

While that is correct, I'm not sure where the offending code you're referring to is. Could you point me to where it is?

stemmlerjs avatar Oct 17 '19 17:10 stemmlerjs

@stemmlerjs of course, I'm sorry if my english sounds aggressive. For example:

export class DownvoteCommentController extends BaseController {
...
}

in forum/usecases/comments/downvoteComment/DownvoteCommentController

https://github.com/stemmlerjs/ddd-forum/blob/master/src/modules/forum/useCases/comments/downvoteComment/DownvoteCommentController.ts

imsergiobernal avatar Oct 17 '19 18:10 imsergiobernal

@imsergiobernal Your English is great! :)

So, here's my thoughts.

A controller is an infrastructure layer concern, not an application layer concern. And the BaseController class is also part of infrastructure.

Web servers, caches, databases - anything to do with the outside world, are infra.

In our codebase, every controller is a BaseController, which is just a nice base class encapsulating HTTP response logic with intention revealing methods.

You mentioned a usecase controller. Maybe you and I see it differently, but a Use Case is an application layer concern, and a controller is an infrastructure one.

Therefore, a use case controller doesn't exist and is contradictory to the definitions of both.

We do, however, inject via Dependency Injection, use cases (like UpvoteComment) into controllers (like UpvoteCommentController), which is valid to the Dependency Rule.


If we're on the same page of use cases belonging to the application layer and controllers belonging to the infra layer, I don't think there's an issue here.

stemmlerjs avatar Oct 18 '19 03:10 stemmlerjs

So,

why the Controller is inside Usecase's folder and not inside infra folder? Could you re-use the controller with another framework? Could you re-use the controller with another request interface (Infrastructure Event for instance)?

👍

imsergiobernal avatar Oct 19 '19 00:10 imsergiobernal

why the Controller is inside Usecase's folder and not inside infra folder?

There's an architectural principle called Package By Component.

xh9i

Notice how easy it is to determine exactly what the features of this app are? That's called package by component/module. It's when we put everything related to a component (the use cases are our components) into a single module. When we use the name of the use case as the folder name the app is practically telling us what it can do, without needing to look deep inside the files and classes.

That phenomenon is called Screaming Architecture. The app is screaming at us, letting the reader know exactly what problems it solves and what the domain is, with the actors and use cases. The alternative to this is *Package By Infrastructure.

why the Controller is inside Usecase's folder and not inside ~~infra~~ application folder

Package by infrastructure is when you organize code according to the type of construct it is (controllers, application layer, infra, etc). When we do that, it hurts our ability to quickly identify where features are, and features get split across folders and files.

I really punched down on Uncle Bob's approach to designing applications by centering them around the use cases. That means that every time I start a new project, I'll identify the actors, the use cases and which subdomain those actors and use cases belong to.

When we package by component we get +1 readability, +1 cohesion (cohesive units of code).

Let's look a little bit deeper at the createPost/ folder for example.

xh9k

Here's really what I mean. In this cohesive component, we have everything we need in order to support the CreatePost.ts use case.

We have a:

  • controller (to hook it up to infrastructure, express.js web server)
  • dto (to specify the inputs to the use case)
  • errors namespace (to define all the ways that the use case can succeed or fail)

And let's say for some reason we wanted to hook this use case up to a serverless function.

Depending on the service, we might be able to just export the controller and use that, but we might also need to define another adapter in order for the use case to be able to be used by the external infrastructure concern.

My point is: everything is centered around the use cases of the application.

It makes sense to co-locate things that are used together and their entire purpose of being is to work with each other (controller, dto, error namespace, use case).

This is the very essence of the Single Responsibility Principle.

Could you re-use the controller with another framework? Could you re-use the controller with another request interface (Infrastructure Event for instance)?

I don't think you'd want to do that. What's being exported from the module is the express controller and the use case itself.

import { DownvoteComment } from "./DownvoteComment";
import { postRepo, memberRepo, commentRepo, commentVotesRepo } from "../../../repos";
import { postService } from "../../../domain/services";
import { DownvoteCommentController } from "./DownvoteCommentController";

const downvoteComment = new DownvoteComment(
  postRepo, memberRepo, commentRepo, commentVotesRepo, postService
);

const downvoteCommentController = new DownvoteCommentController(
  downvoteComment
)

// we export the express controller and the use case itself
export {
  downvoteComment,                  
  downvoteCommentController 
}

The use case is what has reusability because it's an application layer concern. The express controller does not. It has a specific purpose: to be hooked up to an express RESTful API.

If you for some reason needed to support another delivery mechanism, like GraphQL or SOAP, you could dependency inject the DownvoteComment use case into a GraphQLController or a SoapController (each other specific infrastructure layer concerns).

stemmlerjs avatar Oct 19 '19 01:10 stemmlerjs

@stemmlerjs

Clean Architecture book, Use Case concept, page 188

Notice also that the use case does not describe the user interface other than to informally specify the data coming in from that interface, and the data going back out through that interface. From the use case, it is impossible to tell whether the application is delivered on the web, or on a thick client, or on a console, or is a pure service. This is very important. Use cases do not describe how the system appears to the user. Instead, they describe the application-specific rules that govern the interaction between the users and the Entities. How the data gets in and out of the system is irrelevant to the use cases.

I throw the following question. Can I reuse this use-case to a Console output without duplicating?

imsergiobernal avatar Nov 11 '19 11:11 imsergiobernal

@imsergiobernal Absolutely. To reuse it, you can import the DownvoteComment instance as shown in my example above and then call downvoteComment.execute().

No need to re-write the entire use case.

stemmlerjs avatar Nov 11 '19 11:11 stemmlerjs