10.2 GRASP (General Responsibility Assignment Software Patterns)
Resumo
GRASP — 9 princípios para atribuir responsabilidade às classes no OOP.
Principais: Information Expert (Order calcula o total), Creator (Order cria os items), Controller (thin), Low Coupling (DI).
Além disso: High Cohesion (uma responsabilidade), Polymorphism (no lugar do if), Indirection (intermediários), Protected Variations (interfaces estáveis).
Conteúdo
- O que é
- 1. Information Expert
- 2. Creator
- 3. Controller
- 4. Low Coupling
- 5. High Cohesion
- 6. Polymorphism
- 7. Pure Fabrication
- 8. Indirection
- 9. Protected Variations
- No Laravel
- Na entrevista
- Exercícios práticos
O que é
GRASP: 9 princípios para atribuir responsabilidade a classes e objetos no OOP.
Para quê:
- Distribuir responsabilidade do jeito certo
- Código fácil de manter
- Low coupling, high cohesion
9 princípios:
- Information Expert
- Creator
- Controller
- Low Coupling
- High Cohesion
- Polymorphism
- Pure Fabrication
- Indirection
- Protected Variations
1. Information Expert
Princípio: A responsabilidade vai para a classe que tem a informação para cumprir.
❌ Ruim:
class OrderController
{
public function show($id)
{
$order = Order::with('items')->find($id);
// Controller calcula o total (não é responsabilidade dele!)
$total = 0;
foreach ($order->items as $item) {
$total += $item->price * $item->quantity;
}
return view('orders.show', ['order' => $order, 'total' => $total]);
}
}✅ Bom (Information Expert):
class Order extends Model
{
// Order conhece os items → ele calcula o total
public function getTotalAttribute(): float
{
return $this->items->sum(fn($item) => $item->price * $item->quantity);
}
}
class OrderController
{
public function show($id)
{
$order = Order::with('items')->find($id);
return view('orders.show', ['order' => $order]);
}
}
// Na view
{{ $order->total }}2. Creator
Princípio: A classe B cria A se:
- B contém A
- B agrega A
- B tem os dados para inicializar A
❌ Ruim:
class OrderController
{
public function store(Request $request)
{
$order = Order::create([...]);
// Controller cria items (não é responsabilidade dele!)
foreach ($request->items as $itemData) {
OrderItem::create([
'order_id' => $order->id,
'product_id' => $itemData['product_id'],
'quantity' => $itemData['quantity'],
'price' => Product::find($itemData['product_id'])->price,
]);
}
}
}✅ Bom (Creator):
class Order extends Model
{
// Order contém items → Order cria os items
public function addItem(int $productId, int $quantity): OrderItem
{
$product = Product::find($productId);
return $this->items()->create([
'product_id' => $productId,
'quantity' => $quantity,
'price' => $product->price,
]);
}
}
class OrderController
{
public function store(Request $request)
{
$order = Order::create([...]);
foreach ($request->items as $itemData) {
$order->addItem($itemData['product_id'], $itemData['quantity']);
}
}
}3. Controller
Princípio: O Controller trata eventos do sistema (HTTP requests) e delega a lógica de negócio para outros objetos.
❌ Ruim (Fat Controller):
class OrderController extends Controller
{
public function store(Request $request)
{
// Validação
$validated = $request->validate([...]);
// Lógica de negócio
$order = Order::create([...]);
foreach ($validated['items'] as $item) {
$product = Product::find($item['product_id']);
if ($product->stock < $item['quantity']) {
throw new OutOfStockException();
}
$product->decrement('stock', $item['quantity']);
$order->items()->create([...]);
}
// Email
Mail::to($order->user)->send(new OrderCreated($order));
// Log
Log::info("Pedido criado", ['order_id' => $order->id]);
return response()->json($order);
}
}✅ Bom (Thin Controller):
class OrderController extends Controller
{
public function store(
StoreOrderRequest $request,
OrderService $orderService
) {
// Controller só coordena
$order = $orderService->create($request->validated());
return response()->json($order);
}
}
class OrderService
{
public function create(array $data): Order
{
return DB::transaction(function () use ($data) {
$order = Order::create([...]);
foreach ($data['items'] as $itemData) {
$this->addItemToOrder($order, $itemData);
}
event(new OrderCreated($order));
return $order;
});
}
private function addItemToOrder(Order $order, array $itemData): void
{
$product = Product::lockForUpdate()->find($itemData['product_id']);
if ($product->stock < $itemData['quantity']) {
throw new OutOfStockException();
}
$product->decrement('stock', $itemData['quantity']);
$order->items()->create([
'product_id' => $product->id,
'quantity' => $itemData['quantity'],
'price' => $product->price,
]);
}
}4. Low Coupling
Princípio: Minimizar dependências entre classes.
❌ High Coupling:
class OrderService
{
public function create(array $data): Order
{
$order = Order::create([...]);
// Dependência direta de uma classe concreta
$mailer = new SmtpMailer();
$mailer->sendOrderConfirmation($order);
$logger = new FileLogger();
$logger->log("Pedido criado");
return $order;
}
}✅ Low Coupling (Dependency Injection):
class OrderService
{
public function __construct(
private MailerInterface $mailer,
private LoggerInterface $logger
) {}
public function create(array $data): Order
{
$order = Order::create([...]);
// Dependência da interface
$this->mailer->sendOrderConfirmation($order);
$this->logger->info("Pedido criado");
return $order;
}
}5. High Cohesion
Princípio: A classe tem uma responsabilidade clara. Os métodos da classe se relacionam.
❌ Low Cohesion:
class UserService
{
// Tudo numa classe só (responsabilidades diferentes!)
public function register(array $data) { }
public function login(string $email, string $password) { }
public function sendPasswordReset(string $email) { }
public function updateProfile(User $user, array $data) { }
public function deleteAccount(User $user) { }
public function exportToCSV(User $user) { }
public function calculateStatistics() { }
}✅ High Cohesion:
// Separar em classes coesas
class AuthService
{
public function register(array $data) { }
public function login(string $email, string $password) { }
public function sendPasswordReset(string $email) { }
}
class ProfileService
{
public function update(User $user, array $data) { }
public function delete(User $user) { }
}
class UserExportService
{
public function toCSV(User $user) { }
}
class UserStatisticsService
{
public function calculate() { }
}6. Polymorphism
Princípio: Use polimorfismo no lugar de if/switch.
❌ Sem polimorfismo:
class OrderProcessor
{
public function process(Order $order)
{
if ($order->payment_method === 'credit_card') {
$this->processCreditCard($order);
} elseif ($order->payment_method === 'paypal') {
$this->processPayPal($order);
} elseif ($order->payment_method === 'crypto') {
$this->processCrypto($order);
}
}
}✅ Com polimorfismo:
interface PaymentGateway
{
public function charge(Order $order): Payment;
}
class CreditCardGateway implements PaymentGateway
{
public function charge(Order $order): Payment { }
}
class PayPalGateway implements PaymentGateway
{
public function charge(Order $order): Payment { }
}
class CryptoGateway implements PaymentGateway
{
public function charge(Order $order): Payment { }
}
class OrderProcessor
{
public function process(Order $order, PaymentGateway $gateway)
{
$gateway->charge($order);
}
}
// Uso
$gateway = match ($order->payment_method) {
'credit_card' => new CreditCardGateway(),
'paypal' => new PayPalGateway(),
'crypto' => new CryptoGateway(),
};
$processor->process($order, $gateway);7. Pure Fabrication
Princípio: Crie uma classe artificial para uma responsabilidade que não cabe em nenhum objeto de domain.
Exemplo: Log não é domain, mas você precisa.
// Pure Fabrication: classe para responsabilidade técnica
class Logger
{
public function log(string $message): void
{
// Responsabilidade técnica (não é domain)
}
}
class OrderService
{
public function __construct(private Logger $logger) {}
public function create(array $data): Order
{
$order = Order::create([...]);
// Usamos Pure Fabrication
$this->logger->log("Pedido {$order->id} criado");
return $order;
}
}Outros exemplos de Pure Fabrication:
- Repository (acesso ao banco)
- Cache
- EventDispatcher
- Validator
8. Indirection
Princípio: Use um intermediário para baixar o coupling.
❌ Dependência direta:
class OrderService
{
public function create(array $data): Order
{
$order = Order::create([...]);
// Dependência direta do SMTP
$mailer = new SmtpMailer();
$mailer->send($order->user->email, 'Pedido criado', '...');
return $order;
}
}✅ Indirection (intermediário):
// Intermediário: Laravel Mail facade
class OrderService
{
public function create(array $data): Order
{
$order = Order::create([...]);
// Indirection via Mail facade
Mail::to($order->user)->send(new OrderCreated($order));
return $order;
}
}
// Mail facade — intermediário entre OrderService e SMTP9. Protected Variations
Princípio: Proteja o sistema de mudanças com interfaces estáveis.
❌ Sem proteção:
class OrderService
{
public function create(array $data): Order
{
$order = Order::create([...]);
// Dependência direta da Stripe API
$stripe = new StripeClient(config('stripe.key'));
$stripe->charges->create([
'amount' => $order->total * 100,
'currency' => 'brl',
]);
return $order;
}
}
// Se precisar trocar Stripe por PayPal → reescreve o OrderService✅ Protected Variations:
// Interface estável
interface PaymentGateway
{
public function charge(int $amount, string $currency): Payment;
}
class StripeGateway implements PaymentGateway
{
public function charge(int $amount, string $currency): Payment
{
$stripe = new StripeClient(config('stripe.key'));
// ...
}
}
class PayPalGateway implements PaymentGateway
{
public function charge(int $amount, string $currency): Payment
{
// PayPal API
}
}
class OrderService
{
public function __construct(private PaymentGateway $gateway) {}
public function create(array $data): Order
{
$order = Order::create([...]);
// Protegidos de mudança no payment gateway
$this->gateway->charge($order->total, 'brl');
return $order;
}
}
// Dá para trocar o gateway sem mexer no OrderServiceNo Laravel
// Information Expert
class Order extends Model
{
public function getTotalAttribute() { /* Order conhece os items */ }
}
// Creator
class Order extends Model
{
public function addItem($productId, $quantity) { /* Order cria os items */ }
}
// Controller (thin)
class OrderController extends Controller
{
public function store(StoreOrderRequest $request, OrderService $service)
{
return $service->create($request->validated());
}
}
// Low Coupling (DI)
class OrderService
{
public function __construct(
private PaymentGateway $gateway,
private LoggerInterface $logger
) {}
}
// High Cohesion (Single Responsibility)
class AuthService { /* só auth */ }
class ProfileService { /* só profile */ }
// Polymorphism
interface PaymentGateway { }
class StripeGateway implements PaymentGateway { }
// Pure Fabrication
class OrderRepository { /* responsabilidade técnica */ }
// Indirection
Mail::to($user)->send(new OrderCreated($order));
// Protected Variations
interface PaymentGateway { /* interface estável */ }Na entrevista
"GRASP — 9 princípios para atribuir responsabilidade. Information Expert: a responsabilidade vai para quem tem a informação (Order calcula o total). Creator: a classe cria o que ela contém (Order cria os items). Controller: thin, delega para o Service. Low Coupling: dependências via interface (DI). High Cohesion: uma responsabilidade clara (AuthService separado de ProfileService). Polymorphism: no lugar de if/switch. Pure Fabrication: classes artificiais para responsabilidade técnica (Logger, Repository). Indirection: intermediários (Mail facade). Protected Variations: interfaces estáveis para proteger de mudança (PaymentGateway)."
Exercícios práticos
Exercício 1: Information Expert + Creator
Reescreva o código aplicando Information Expert e Creator.
// Ruim: Controller faz tudo
class OrderController extends Controller
{
public function store(Request $request)
{
$order = Order::create([
'user_id' => $request->user_id,
'status' => 'pending',
]);
// Controller calcula o total (não é com ele!)
$total = 0;
foreach ($request->items as $itemData) {
$product = Product::find($itemData['product_id']);
$price = $product->price;
$quantity = $itemData['quantity'];
// Controller cria items (não é com ele!)
OrderItem::create([
'order_id' => $order->id,
'product_id' => $product->id,
'quantity' => $quantity,
'price' => $price,
]);
$total += $price * $quantity;
}
$order->update(['total' => $total]);
return response()->json($order);
}
}Solução
// ✅ Information Expert + Creator
// Order conhece os items → Order calcula o total (Information Expert)
class Order extends Model
{
public function getTotalAttribute(): float
{
return $this->items->sum(fn($item) => $item->subtotal);
}
// Order contém items → Order cria os items (Creator)
public function addItem(int $productId, int $quantity): OrderItem
{
$product = Product::findOrFail($productId);
return $this->items()->create([
'product_id' => $product->id,
'quantity' => $quantity,
'price' => $product->price,
]);
}
public function items()
{
return $this->hasMany(OrderItem::class);
}
}
// OrderItem conhece o próprio subtotal (Information Expert)
class OrderItem extends Model
{
public function getSubtotalAttribute(): float
{
return $this->price * $this->quantity;
}
public function product()
{
return $this->belongsTo(Product::class);
}
}
// Service coordena o processo
class OrderService
{
public function create(int $userId, array $items): Order
{
return DB::transaction(function () use ($userId, $items) {
$order = Order::create([
'user_id' => $userId,
'status' => 'pending',
]);
foreach ($items as $item) {
$order->addItem($item['product_id'], $item['quantity']);
}
// total é calculado sozinho via accessor
return $order->fresh(['items']);
});
}
}
// Controller só coordena (princípio Controller)
class OrderController extends Controller
{
public function store(Request $request, OrderService $orderService)
{
$order = $orderService->create(
$request->user_id,
$request->items
);
return response()->json([
'order' => $order,
'total' => $order->total, // Calculado pelo Order
]);
}
}
// Vantagens:
// - Order responde pela própria lógica
// - Fácil de testar
// - Código reutilizável
// - Responsabilidade claraExercício 2: Low Coupling + Protected Variations
Refatore o código para baixar o coupling e proteger de mudanças.
// Ruim: High Coupling
class OrderService
{
public function create(array $data): Order
{
$order = Order::create($data);
// Dependência direta do Stripe
$stripe = new \Stripe\StripeClient(config('services.stripe.key'));
$charge = $stripe->charges->create([
'amount' => $order->total * 100,
'currency' => 'brl',
'source' => $data['card_token'],
]);
$order->update(['payment_id' => $charge->id]);
// Dependência direta do SMTP
$mailer = new \Swift_Mailer(
new \Swift_SmtpTransport('smtp.gmail.com', 587)
);
$message = (new \Swift_Message('Confirmação de pedido'))
->setFrom('noreply@example.com')
->setTo($order->user->email)
->setBody('Seu pedido foi confirmado');
$mailer->send($message);
return $order;
}
}
// Problemas:
// - Não dá para trocar Stripe por PayPal
// - Não dá para trocar SMTP por outro transport
// - Difícil de testarSolução
// ✅ Low Coupling + Protected Variations
// Interface estável (Protected Variations)
interface PaymentGateway
{
public function charge(float $amount, string $token): Payment;
}
class StripeGateway implements PaymentGateway
{
public function __construct(private string $apiKey) {}
public function charge(float $amount, string $token): Payment
{
$stripe = new \Stripe\StripeClient($this->apiKey);
$charge = $stripe->charges->create([
'amount' => $amount * 100,
'currency' => 'brl',
'source' => $token,
]);
return new Payment(
id: $charge->id,
amount: $amount,
status: $charge->status
);
}
}
class PayPalGateway implements PaymentGateway
{
public function charge(float $amount, string $token): Payment
{
// Implementação do PayPal
}
}
// Notification interface (Protected Variations)
interface Notifier
{
public function send(User $user, string $message): void;
}
class EmailNotifier implements Notifier
{
public function send(User $user, string $message): void
{
Mail::to($user->email)->send(new GenericEmail($message));
}
}
class SmsNotifier implements Notifier
{
public function send(User $user, string $message): void
{
// Implementação de SMS
}
}
// Service com Low Coupling (via DI)
class OrderService
{
public function __construct(
private PaymentGateway $paymentGateway,
private Notifier $notifier
) {}
public function create(array $data): Order
{
return DB::transaction(function () use ($data) {
$order = Order::create($data);
// Não importa qual gateway (Low Coupling)
$payment = $this->paymentGateway->charge(
$order->total,
$data['card_token']
);
$order->update(['payment_id' => $payment->id]);
// Não importa qual notifier (Low Coupling)
$this->notifier->send(
$order->user,
"Seu pedido #{$order->id} foi confirmado"
);
return $order;
});
}
}
// Service Provider para o binding
class AppServiceProvider extends ServiceProvider
{
public function register()
{
// Dá para trocar fácil por PayPalGateway
$this->app->bind(PaymentGateway::class, function () {
return new StripeGateway(config('services.stripe.key'));
});
// Dá para trocar fácil por SmsNotifier
$this->app->bind(Notifier::class, EmailNotifier::class);
}
}
// Teste (fácil!)
class OrderServiceTest extends TestCase
{
public function test_creates_order_with_payment()
{
// Mock das dependências
$gateway = Mockery::mock(PaymentGateway::class);
$gateway->shouldReceive('charge')
->once()
->andReturn(new Payment(id: 'test_123', amount: 100, status: 'succeeded'));
$notifier = Mockery::mock(Notifier::class);
$notifier->shouldReceive('send')->once();
$service = new OrderService($gateway, $notifier);
$order = $service->create([...]);
$this->assertEquals('test_123', $order->payment_id);
}
}
// Vantagens:
// - Troca gateway/notifier sem dor
// - Fácil de testar
// - Protegido de mudança na API
// - Low couplingExercício 3: High Cohesion + Polymorphism
Refatore o Fat Service aplicando High Cohesion e Polymorphism.
// Ruim: Low Cohesion (tudo numa classe só)
class UserService
{
public function register(array $data) { /* ... */ }
public function login(string $email, string $password) { /* ... */ }
public function logout(User $user) { /* ... */ }
public function updateProfile(User $user, array $data) { /* ... */ }
public function uploadAvatar(User $user, $file) { /* ... */ }
public function deleteAccount(User $user) { /* ... */ }
public function sendNotification(User $user, string $message, string $type)
{
// Ruim: switch no lugar de polymorphism
switch ($type) {
case 'email':
Mail::to($user->email)->send(new GenericEmail($message));
break;
case 'sms':
$this->sendSms($user->phone, $message);
break;
case 'push':
$this->sendPushNotification($user->id, $message);
break;
}
}
}Solução
// ✅ High Cohesion: separar em classes coesas
// 1. Autenticação (coesa)
class AuthService
{
public function register(array $data): User
{
$user = User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => Hash::make($data['password']),
]);
event(new UserRegistered($user));
return $user;
}
public function login(string $email, string $password): ?User
{
if (Auth::attempt(['email' => $email, 'password' => $password])) {
return Auth::user();
}
return null;
}
public function logout(User $user): void
{
Auth::logout();
}
}
// 2. Perfil (coeso)
class ProfileService
{
public function update(User $user, array $data): User
{
$user->update($data);
event(new ProfileUpdated($user));
return $user->fresh();
}
public function uploadAvatar(User $user, UploadedFile $file): User
{
$path = $file->store('avatars', 's3');
$user->update(['avatar' => $path]);
return $user;
}
public function delete(User $user): void
{
$user->delete();
event(new AccountDeleted($user));
}
}
// 3. Polymorphism para notificações
// Interface
interface NotificationChannel
{
public function send(User $user, string $message): void;
}
// Implementações
class EmailChannel implements NotificationChannel
{
public function send(User $user, string $message): void
{
Mail::to($user->email)->send(new GenericEmail($message));
}
}
class SmsChannel implements NotificationChannel
{
public function __construct(private SmsProvider $provider) {}
public function send(User $user, string $message): void
{
$this->provider->send($user->phone, $message);
}
}
class PushChannel implements NotificationChannel
{
public function __construct(private PushProvider $provider) {}
public function send(User $user, string $message): void
{
$this->provider->send($user->id, $message);
}
}
// Notification Service
class NotificationService
{
private array $channels = [];
public function addChannel(string $name, NotificationChannel $channel): void
{
$this->channels[$name] = $channel;
}
public function send(User $user, string $message, string $channelName): void
{
$channel = $this->channels[$channelName]
?? throw new InvalidArgumentException("Canal desconhecido: $channelName");
$channel->send($user, $message);
}
public function broadcast(User $user, string $message, array $channels): void
{
foreach ($channels as $channelName) {
$this->send($user, $message, $channelName);
}
}
}
// Uso
$notificationService = new NotificationService();
$notificationService->addChannel('email', new EmailChannel());
$notificationService->addChannel('sms', new SmsChannel($smsProvider));
$notificationService->addChannel('push', new PushChannel($pushProvider));
// Enviar em um canal
$notificationService->send($user, 'Olá!', 'email');
// Broadcast em vários canais
$notificationService->broadcast($user, 'Mensagem importante', ['email', 'sms', 'push']);
// Vantagens:
// - Cada classe tem uma responsabilidade (High Cohesion)
// - Fácil adicionar canal novo (Open/Closed)
// - Sem switch/if (Polymorphism)
// - Fácil testar cada componenteParte do PHP/Laravel Interview Handbook | Feito com ❤️ pela equipe CodeMate