Kaique Mitsuo Silva Yamamoto
Arquitetura softwareFrontend webTypeScriptClean CodeSOLID

S — Single Responsibility Principle

Uma classe, uma função, um módulo — uma única razão para mudar. Exemplos full-stack em React, Next.js, NestJS e Express.

S — Single Responsibility Principle (SRP)

"Uma classe deve ter apenas uma razão para mudar." — Robert C. Martin

O que é, em português claro

Imagine uma faca suíça: abre garrafa, corta, abre latinha, limpa unha. Funciona? Funciona. Mas quando você precisa trocar a lâmina, você perde todas as outras funções. E se a lâmina quebrar no meio do acampamento, você perde tudo.

Clean Code diz: uma ferramenta, uma função. A tesoura corta. O abridor abre. Cada um pode ser trocado, consertado ou atualizado sem afetar os outros.

No código, isso significa: se uma função ou classe faz mais de uma coisa, cada "coisa" é um motivo independente para ela mudar — e isso é perigoso.

Onde acontece na vida real

CenárioO que está erradoO que deveria ser
Uma função que valida, salva no banco e envia email3 responsabilidades3 funções separadas
Um componente React que busca dados, formata e renderiza3 responsabilidadeshook + formatter + componente
Um controller que lê o body, valida, chama repo e formata resposta4 responsabilidadescontroller + service + formatter

Exemplo prático — Frontend (React)

❌ Componente com muitas responsabilidades

// UserCard.tsx — faz TUDO: busca, formata, renderiza, trata erro
function UserCard({ userId }: { userId: string }) {
  const [user, setUser] = useState<User | null>(null);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => setUser(data))
      .catch(err => setError(err.message));
  }, [userId]);

  if (error) return <div>Erro: {error}</div>;
  if (!user) return <div>Carregando...</div>;

  // Formatação embutida
  const formattedDate = new Date(user.createdAt).toLocaleDateString('pt-BR');
  const initials = user.name.split(' ').map(n => n[0]).join('').toUpperCase();

  return (
    <div className="user-card">
      <div className="avatar">{initials}</div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
      <span>Membro desde {formattedDate}</span>
    </div>
  );
}

Problemas: O componente busca dados, formata strings, trata erros e renderiza. Se a API mudar de endpoint, se o formato de data mudar, se o layout do card mudar — tudo está acoplado.

✅ Responsabilidades separadas

// hooks/useUser.ts — única responsabilidade: buscar dados
function useUser(userId: string) {
  const [user, setUser] = useState<User | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => { setUser(data); setLoading(false); })
      .catch(err => { setError(err.message); setLoading(false); });
  }, [userId]);

  return { user, error, loading };
}
// utils/format-user.ts — única responsabilidade: formatar dados
function getUserInitials(name: string): string {
  return name.split(' ').map(n => n[0]).join('').toUpperCase();
}

function formatDate(date: string): string {
  return new Date(date).toLocaleDateString('pt-BR');
}
// components/UserCard.tsx — única responsabilidade: renderizar
function UserCard({ userId }: { userId: string }) {
  const { user, error, loading } = useUser(userId);

  if (error) return <div>Erro: {error}</div>;
  if (loading || !user) return <div>Carregando...</div>;

  return (
    <div className="user-card">
      <div className="avatar">{getUserInitials(user.name)}</div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
      <span>Membro desde {formatDate(user.createdAt)}</span>
    </div>
  );
}

Resultado: Se o endpoint mudar, altero useUser. Se o formato de data mudar, altero formatDate. Se o layout mudar, altero UserCard. Cada arquivo tem um motivo para mudar.

Exemplo prático — Backend (NestJS/Express)

❌ Service com muitas responsabilidades

// user.service.ts — faz tudo
class UserService {
  async createUser(data: CreateUserDto) {
    // Validação
    if (!data.email.includes('@')) {
      throw new Error('Email inválido');
    }

    // Hash de senha
    const hashedPassword = await bcrypt.hash(data.password, 10);

    // Persistência
    const user = await this.db.users.create({
      ...data,
      password: hashedPassword,
    });

    // Email de boas-vindas
    await this.email.send({
      to: user.email,
      subject: 'Bem-vindo!',
      body: `Olá ${user.name}, sua conta foi criada.`,
    });

    // Log de auditoria
    await this.db.auditLog.create({
      action: 'USER_CREATED',
      userId: user.id,
    });

    return user;
  }
}

Problemas: 5 responsabilidades em uma função. Se o provedor de email mudar, se a regra de validação mudar, se o log mudar — tudo está junto.

✅ Responsabilidades separadas

// validators/user.validator.ts — só valida
class UserValidator {
  validate(data: CreateUserDto): Result<CreateUserDto, ValidationError> {
    if (!data.email.includes('@')) {
      return { ok: false, error: new ValidationError('Email inválido', 'email') };
    }
    if (data.password.length < 8) {
      return { ok: false, error: new ValidationError('Senha muito curta', 'password') };
    }
    return { ok: true, value: data };
  }
}
// services/password.service.ts — só lida com senha
class PasswordService {
  async hash(plain: string): Promise<string> {
    return bcrypt.hash(plain, 10);
  }

  async compare(plain: string, hashed: string): Promise<boolean> {
    return bcrypt.compare(plain, hashed);
  }
}
// repositories/user.repository.ts — só persiste
class UserRepository {
  constructor(private db: Database) {}

  async create(data: CreateUserDto & { password: string }): Promise<User> {
    return this.db.users.create(data);
  }

  async findByEmail(email: string): Promise<User | null> {
    return this.db.users.findOne({ email });
  }
}
// services/user.service.ts — orquestra, não executa
class UserService {
  constructor(
    private validator: UserValidator,
    private passwordService: PasswordService,
    private repository: UserRepository,
    private emailService: EmailService,
    private auditService: AuditService,
  ) {}

  async createUser(data: CreateUserDto): Promise<Result<User, Error>> {
    const validated = this.validator.validate(data);
    if (!validated.ok) return validated;

    const hashed = await this.passwordService.hash(validated.value.password);
    const user = await this.repository.create({ ...validated.value, password: hashed });

    await this.emailService.sendWelcome(user);
    await this.auditService.log('USER_CREATED', user.id);

    return { ok: true, value: user };
  }
}

Resultado: Se o provedor de email mudar (SendGrid → Resendo), altero EmailService. Se a regra de validação mudar, altero UserValidator. O UserService orquestra sem saber os detalhes.

Regra prática

Pergunte-se: "Se eu mudar X, quantos arquivos preciso abrir?"

  • Se a resposta for 1 → SRP está sendo respeitado.
  • Se a resposta for > 1 → provavelmente há responsabilidades misturadas.

Quando exagerar no SRP

SRP não significa criar 50 arquivos com 3 linhas cada. Se duas operações estão sempre mudando juntas e fazem parte do mesmo conceito, provavelmente são a mesma responsabilidade.

Exemplo de excesso: Separar formatFirstName e formatLastName em arquivos diferentes quando sempre são usadas juntas. Nesse caso, formatName é suficiente.

Referências

On this page