DEV Community

Davi Orlandi
Davi Orlandi

Posted on

Implementando Clean Architecture com TypeScript

Clean Architecture é uma filosofia de design de software que visa criar sistemas fáceis de manter, testar e entender. Ela enfatiza a separação de responsabilidades, garantindo que cada parte do sistema tenha uma única responsabilidade. Neste artigo, exploraremos como implementar Clean Architecture usando TypeScript.

Índice

  1. Introdução à Clean Architecture
  2. Princípios Fundamentais
  3. Configurando o Projeto
  4. Estrutura de Pastas
  5. Entidades
  6. Casos de Uso
  7. Interfaces
  8. Frameworks e Drivers
  9. Juntando Tudo
  10. Conclusão

Introdução à Clean Architecture

Clean Architecture, introduzida por Robert C. Martin (Uncle Bob), proporciona uma separação clara entre as diferentes partes de um sistema de software. A ideia principal é manter a lógica de negócios central independente de fatores externos, como bancos de dados, UI ou frameworks.

Princípios Fundamentais

  1. Independência: A lógica de negócios deve ser independente de UI, banco de dados ou sistemas externos.
  2. Testabilidade: O sistema deve ser fácil de testar.
  3. Separação de Responsabilidades: Diferentes partes do sistema devem ter responsabilidades distintas.
  4. Manutenibilidade: O sistema deve ser fácil de manter e evoluir.

Configurando o Projeto

Primeiro, vamos configurar um projeto TypeScript. Você pode usar npm ou yarn para inicializar um novo projeto.

mkdir clean-architecture-ts
cd clean-architecture-ts
npm init -y
npm install typescript ts-node @types/node --save-dev
Enter fullscreen mode Exit fullscreen mode

Crie um arquivo tsconfig.json para configurar o TypeScript.

{
  "compilerOptions": {
    "target": "ES6",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true
  }
}
Enter fullscreen mode Exit fullscreen mode

Estrutura de Pastas

Um projeto com clean architecture geralmente tem a seguinte estrutura de pastas:

src/
├── entities/
├── usecases/
├── interfaces/
├── frameworks/
└── main.ts
Enter fullscreen mode Exit fullscreen mode

Entidades

Entidades representam a lógica de negócios central. Elas são a parte mais importante do sistema e devem ser independentes de fatores externos.

// src/entities/user.entity.ts
export class User {
    constructor(id: string, public email: string, public password:string) {}

    static create(email: string, password: string) {
        const userId = uuid()
        return new User(userId, email, password)
    }
}
Enter fullscreen mode Exit fullscreen mode

Casos de Uso

Casos de uso contêm as regras de negócios específicas da aplicação. Eles orquestram a interação entre entidades e interfaces.

// src/usecases/create-user.usecase.ts
import { User } from "../entities/user.entity";
import { UsersRepository } from "../interfaces/users.repository"

interface CreateUserRequest {
  email: string;
  password: string;
}

export class CreateUserUseCase {
  constructor(private userRepository: UserRepository) {}

  async execute(request: CreateUserRequest): Promise<void> {
    const user = User.create(request.email, request.password)
    await this.userRepository.save(user);
  }
}
Enter fullscreen mode Exit fullscreen mode

Interfaces

Interfaces são os contratos entre os casos de uso e o mundo externo. Elas podem incluir repositórios, serviços ou qualquer sistema externo.

// src/interfaces/users.repository.ts
import { User } from "../entities/user.entity";

export interface UserRepository {
  save(user: User): Promise<void>;
}
Enter fullscreen mode Exit fullscreen mode

Frameworks e Drivers

Frameworks e drivers contêm os detalhes de implementação das interfaces. Eles interagem com sistemas externos, como bancos de dados ou APIs.

// src/frameworks/in-memory-users.repository.ts
import { User } from "../entities/User";
import { UserRepository } from "../interfaces/users.repository";

export class InMemoryUsersRepository implements UserRepository {
  private users: User[] = [];

  async save(user: User): Promise<void> {
    this.users.push(user);
  }
}
Enter fullscreen mode Exit fullscreen mode

Juntando Tudo

Finalmente, vamos criar um ponto de entrada para conectar tudo.

// src/main.ts
import { CreateUser } from "./usecases/create-user.usecase";
import { InMemoryUserRepository } from "./frameworks/in-memory-users.repository";

const userRepository = new InMemoryUserRepository();
const createUser = new CreateUserUseCase(userRepository);

createUser.execute({ email: "john.doe@example.com", password: "123456" })
  .then(() => console.log("User created successfully"))
  .catch(err => console.error("Failed to create user", err));
Enter fullscreen mode Exit fullscreen mode

Compile e execute o projeto:

tsc
node dist/main.js
Enter fullscreen mode Exit fullscreen mode

Conclusão

Seguindo os princípios da Clean Architecture, podemos criar um sistema que é manutenível, testável e adaptável a mudanças. TypeScript fornece tipagem forte e recursos modernos de JavaScript que ajudam a impor esses princípios. Com uma clara separação de responsabilidades, nosso código se torna mais fácil de entender e evoluir ao longo do tempo.

Top comments (2)

Collapse
 
cquintella profile image
𝖈𝖖𝖚𝖎𝖓𝖙𝖊𝖑𝖑𝖆

Gostei, tenho seguido a Clean Architecture, no entanto tenho uma dúvida se no type script uso DTO ou interface. O que vcs usam, classicamente a Interface seria mais pra comportamentos e não para dados, no typescript a comunidade parece usar para os dois. Qual seria melhor?

export type UserDTO = {
id: number;
name: string;
email: string;
password:string;
}

vs.

export interface UserInterface {
id:number;
name:string;
email:string;
password:string;
}

Collapse
 
marialuizaleitao profile image
Maria Leitão

Muito boa a explicação!!