Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Henrique Leite
Henrique Leite

Posted on

     

A simplified version of Clean Arch

The Clean Arch created by Uncle Bob is amazingly useful and robust, but it's very complex for beginners and very costly to maintain, mainly for small startups.

In this article, I'll explain "my version" of Clean Arch, that tries it's best to simplify the original architecture while maintaining good part of it's robustness.

One thing to emphasize is: This is anarchitecture pattern so it's language agnostic and framework agnostic. You can use it in any language, framework, library, project, that you want to.

If you want to know more about Clean Arch, you canread the book orread this article.

Dictionary

Before we start with the architecture, it's very important to define the meaning of some words.

Contracts and Implementations

Contracts are specifications for how to implement something. They don't do anything, they only tell you how to do something, but unlike your project manager, it tell how to do it in the right way. They can be view as abstract classes, interfaces or types.

On the other hand,Implementations are the things that really do something, following the instructions of theContracts. They can be view as classes or functions.

Domains

Domain are a very subjective concept, it's up to you to analyse your business, its requirements and define what should be a domain.

Some examples of domains for a social network:

  • Auth
  • User
  • Post
  • Comment
  • Like

The core concepts

henriqueleite42 Clean Architecture

Controllers

This is the layer to expose your application to the users, it's the only part that the user has access to. It can be a HTTP server, a GraphQL server, a Queue handler, a Cron Job, anything that can call your application.

This layer is responsible for:

  • Receiving the requests (pooling for messages on queues, etc)
  • Validating the inputs using theValidators layer and handling the validation error if it happens
  • Calling theUseCases with the validated input
  • Returning the response (UseCase output) in the correct format (including errors)

Validators

This is the layer to validate the inputs: Ensure that all the necessary data is being received, is in the correct format with the correct types, very simple, very slim.

Models

Models are theContracts for theRepositories andUsecases.

UseCases

This is where all your business rules are at: All the "Can user do this", "If this happens, do this". The UseCases are the "most complex"/biggest part of our system, because it is responsible for using all the other components (Repositories andAdapters) to build a flow to execute something.

Repositories

This layer is responsible for communicating with the database, wrapping your queries (with veeery little of business rules, only the simplest ones, examples below) and abstracting them on simple methods that can be used for yourUseCases.

Adapters

Adapters are responsible for abstracting / wrapping external libraries, APIs and dependencies in general. They can be used both fromRepositories andUseCases.

How do we build something using this architecture?

  • Create a POC: A minimalist version of what we are trying to build, without worry for code readability, mutability or struct, just put everything in one function in one file, used to learn the requirements for the final product.
  • Using the knowledge got from the last step, we create all theContracts (Models) that we need (including the one for the adapters)
    • We usually start from the contracts for theAdapters
    • And then create theModels
  • Implement theAdapters following theirContracts
  • Implement theRepositories following theirContracts
  • Implement theUseCases following theirContracts
  • Implement theValidators that you will need
  • Implement theDelivery layer (HTTP server with it's routes, queue system, cron, etc)
  • Put everything together and it's done!

Example

I'll provide you guys an example in Golang, but this can be applied to any language and framework.

BUT REMEMBER: This is an extremely simple example thatSHOULD NOT BE USED IN PRODUCTION!!!

Folders Structure

Let's start from the folders structure:

Folders Structure

As you can see, it's very basic and straight forward: it has one folder for each core concept of the architecture.

Models Example

Models Example

Not all models must haveUseCases, the same way that not all models must haveRepositories or Entities (representations of the database tables, used to type things usually returned by the Repositories).

In our example we have 2 models: User and Auth.

// internal/models/user.gopackagemodels// ----------------------------////      Repository//// ----------------------------typeCreateUserInputstruct{Emailstring}typeCreateUserOutputstruct{Idstring}typeUserRepositoryinterface{Create(i*CreateUserInput)(*CreateUserOutput,error)}
Enter fullscreen modeExit fullscreen mode

On our user model, we only have thecontract for theRepository.

// internal/models/auth.gopackagemodels// ----------------------------////      UseCase//// ----------------------------typeCreateFromEmailInputstruct{Emailstring`json:"email" validate:"required,email"`}typeAuthOutputstruct{UserIdstring`json:"userId"`}typeAuthUsecaseinterface{CreateFromEmailProvider(i*CreateFromEmailInput)error}
Enter fullscreen modeExit fullscreen mode

And on our auth model, we only have thecontract for theUseCase.

We could have done it in only 1 model, but I choose to split it in to models to explain to you guys the multiple ways that models can be used.

Adapter Example

Adapter Example

You can see that we have 2 main components here:

  • Animplementations folder
  • And anid.go file

Theid.go is theContract for the adapter. UnlikeRepositories andUseCases, the contracts for theAdapters are grouped in the adapters folder and not in theModels.

And inside theimplementations folder we have the real implementations for the contracts.

// internal/adapters/id.gopackageadapterstypeIdAdapterinterface{GenId()(string,error)}
Enter fullscreen modeExit fullscreen mode

On theid.go, you can see that we define aContract for a adapter that generates an ID. you can see that the contract doesn't care if the ID is an UUID, ULID, number, or how it's generated, it only cares that the ID must be an string.

// internal/adapters/implementations/ulid/ulid.gopackageulidimport("github.com/oklog/ulid/v2")typeUlidstruct{}func(adp*Ulid)GenId()(string,error){returnulid.Make().String(),nil}
Enter fullscreen modeExit fullscreen mode

On theimplementations/ulid/ulid.go is where we implement the contract, generating the ID, here we use theoklog/ulid library to generate an ULID.

You can see that the folder and file names are relative to how they are implementing it (using ULID) and not theContract. This is because we can have multiple types of implementations for the same contract, like having bothimplementations/ulid/ulid.go andimplementations/uuid/uuid.go implementing theIdAdapter.

Repository Example

Repository Example

here we have only the implementations directly, because theContracts are defined in theModels.

// internal/repositories/user.gopackagerepositoriesimport("database/sql""errors""example/internal/adapters""example/internal/models")typeUserRepositorystruct{Db*sql.DBIdAdapteradapters.IdAdapter}func(rep*UserRepository)Create(i*models.CreateUserInput)(*models.CreateUserOutput,error){accountId,err:=rep.IdAdapter.GenId()iferr!=nil{returnnil,errors.New("fail to generate id")}_,err=rep.Db.Exec("INSERT INTO users (id, email) VALUES ($1)",accountId,i.Email,)iferr!=nil{returnnil,errors.New("fail to create account")}return&models.CreateUserOutput{Id:accountId,},nil}
Enter fullscreen modeExit fullscreen mode

Usecase Example

// internal/usecases/user.gopackageusecasesimport("errors""example/internal/models")typeAuthUsecasestruct{UserRepositorymodels.UserRepository}func(serv*AuthUsecase)CreateFromEmailProvider(i*models.CreateFromEmailInput)error{_,err:=serv.UserRepository.Create(&models.CreateUserInput{Email:i.Email,})iferr!=nil{returnerrors.New("fail to create user")}returnnil}
Enter fullscreen modeExit fullscreen mode

We can see that the Usecase receives theUserRepository as adependency injection, and then uses it to create an user.

This is a very simplistic version of a usecase, but in here you cold do thing like:

  • Check if there are any other user with the same email (using a Repository) and return an error
  • Send a welcome email (using an Adapter)

Validators Examples

In our case, we already implemented the validators!

// internal/models/auth.go// ...typeCreateFromEmailInputstruct{Emailstring`json:"email" validate:"required,email"`}// ...
Enter fullscreen modeExit fullscreen mode

In the Golang implementation, the input for the usecase already is the validator. Here we are usinggo-playground/validator to do this validations, and the implementation for this is simply usign the tagvalidate.

In other implementations, you may want to create a folderdelivery/dtos and put your validations there, grouped by domain, or any other scenario that fits best your case.

Delivery Example (HTTP)

Delivery Example (HTTP)

In the delivery layer, we have a folder for each delivery type (http, cron, queues, etc) and avalidator.go file to configure the validator. I'll not show you the content ofvalidator.go here because it doesn't matter for the architecture concept, but you can give a look at the repository on the end of this article do see a more detailed implementation.

// internal/delivery/http/index.gopackagehttpimport("encoding/json""net/http""os""example/internal/delivery""example/internal/models""example/internal/utils")funcNewHttpDelivery(authUsecasemodels.AuthUsecase){router:=http.NewServeMux()validator:=delivery.NewValidator()server:=&http.Server{Addr:":"+os.Getenv("PORT"),Handler:router,}router.HandleFunc("POST /auth/email",func(whttp.ResponseWriter,r*http.Request){body:=&models.CreateFromEmailInput{}err:=json.NewDecoder(r.Body).Decode(body)iferr!=nil{http.Error(w,err.Error(),http.StatusBadRequest)return}err=validator.Validate(body)iferr!=nil{http.Error(w,err.Error(),http.StatusBadRequest)return}err=authUsecase.CreateFromEmailProvider(body)iferr!=nil{http.Error(w,err.Error(),err.(*utils.HttpError).HttpStatusCode())return}})server.ListenAndServe()}
Enter fullscreen modeExit fullscreen mode

On the delivery implementation, it receives the AuthUseCase and uses it, validating the input before sending to the usecase.

Putting everything together

// main.gopackagemainimport("database/sql""os""example/adapters/implementations/ulid""example/delivery/http""example/repositories""example/usecases"_"github.com/lib/pq")funcmain(){db,err:=sql.Open("postgres",os.Getenv("DATABASE_URL"))iferr!=nil{panic(1)}deferdb.Close()// ----------------------------//// Adapters//// ----------------------------ulidAdapter:=&ulid.Ulid{}// ----------------------------//// Repositories//// ----------------------------userRepository:=&repositories.UserRepository{Db:db,IdAdapter:ulidAdapter,}// ----------------------------//// Services//// ----------------------------authUsecase:=&usecases.AuthUsecase{UserRepository:userRepository,}// ----------------------------//// Delivery//// ----------------------------http.NewHttpDelivery(authUsecase)}
Enter fullscreen modeExit fullscreen mode

On the main file of your system, you create an instance of every adapter, repository and usecase, and then use the usecases on your delivery layer.

Conclusion

Thanks for reading everything and feel free to share your thoughts on the comments to try to improve this architecture.

If you want a more detailed example (but that is kinda incomplete in some parts) you can checkthis repository.

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

  • Location
    Brazil
  • Joined

Trending onDEV CommunityHot

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp