S — Single Responsibility Principle
Baixar PDFUma 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ário | O que está errado | O que deveria ser |
|---|---|---|
| Uma função que valida, salva no banco e envia email | 3 responsabilidades | 3 funções separadas |
| Um componente React que busca dados, formata e renderiza | 3 responsabilidades | hook + formatter + componente |
| Um controller que lê o body, valida, chama repo e formata resposta | 4 responsabilidades | controller + 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.