Wzorzec UseCase: strukturyzacja logiki biznesowej w backend-developmentie
UseCase to architektoniczny wzorzec opisujący scenariusze biznesowe w aplikacjach. Pomaga organizować kod wokół konkretnych działań systemu, takich jak tworzenie zamówienia czy rejestracja użytkownika, zamiast abstrakcyjnych operacji technicznych. To podejście poprawia czytelność, ułatwia testowanie i skalowanie projektów, zwłaszcza w złożonych systemach backendowych.
Czym jest UseCase i dlaczego jest potrzebny
UseCase reprezentuje kompletny scenariusz biznesowy na poziomie aplikacji (Warstwa Aplikacji). Zarządza sekwencją kroków w scenariuszu, delegując reguły biznesowe do serwisów domenowych i encji. Na przykład, zamiast technicznej operacji "zapisz wiersz do bazy danych", UseCase opisuje działanie "użytkownik składa zamówienie". Rozwiązuje to problem rozpraszania logiki po kontrolerach i serwisach, który pojawia się wraz z rozwojem projektu. Bez wyraźnych granic scenariuszy kod staje się trudny do zrozumienia i utrzymania.
Kluczowe zalety UseCase:
- Jasna struktura: System jest opisany poprzez wyraźne możliwości biznesowe, a nie abstrakcyjne encje takie jak Service czy Manager.
- Lepsza czytelność: Architektura staje się zrozumiała na poziomie logiki biznesowej, co ułatwia odpowiedź na pytanie "Co potrafi aplikacja?".
- Łatwość testowania: Testy integracyjne na poziomie scenariusza pozwalają sprawdzić całą ścieżkę użytkownika bez ryzyka naruszenia procesu biznesowego.
Ograniczenia i zalecenia dotyczące użycia
W większości przypadków nie zaleca się wywoływania jednego UseCase z drugiego, aby uniknąć ukrytych zależności i przekształcenia UseCase w wielokrotnie używane bloki logiki. Jednak w złożonych scenariuszach jest to dopuszczalne przy braku cyklicznych zależności i zachowaniu jasności scenariuszy. Jeśli pojawi się potrzeba ponownego użycia logiki, warto ją wyodrębnić do oddzielnych serwisów lub komponentów domenowych, które mogą być później stosowane w różnych UseCase.
Gdzie są wywoływane UseCase:
- Controller
- Console Command
- Job
- Event Listener
UseCase służy jako punkt wejścia do logiki biznesowej aplikacji, przez który świat zewnętrzny wchodzi w interakcję z systemem.
Dlaczego warstwa Service może być niewystarczająca
Na początku projektu warstwa Service często wystarcza, ale wraz z rozwojem logiki biznesowej pojawiają się problemy. Na przykład, tworzenie zamówienia z prostej operacji zapisu do bazy może wiązać się z dodatkowymi krokami: sprawdzanie zapasów, stosowanie rabatów, pobieranie salda, wysyłanie powiadomień. Prowadzi to do powstania klas-bóstw (God-object) z wieloma zależnościami, metodami i efektami ubocznymi, co utrudnia czytanie kodu i pisanie testów.
UseCase oferuje proste zasady: jeden UseCase = jeden proces biznesowy. Powtarzalna logika jest wyodrębniana do oddzielnych serwisów i komponentów domenowych, co zachowuje sterowalność kodu nawet przy wzroście złożoności.
Struktura UseCase na przykładzie PHP i Laravel
Typowa struktura UseCase obejmuje trzy komponenty:
- Handler: Zarządza sekwencją wykonania scenariusza, zawiera warunki kroków, ale nie domenową logikę biznesową.
- DataInput: Obiekt danych wejściowych bez logiki, tylko typizowane dane, najlepiej niezmienne.
- DataOutput: Obiekt wyniku wykonania, formułujący wyraźny kontrakt (np. DTO).
Przykład tworzenia zamówienia:
class StoreOrderDataInput
{
public function __construct(
public readonly int $userId,
public readonly array $items,
) {}
}
class StoreOrderDataOutput
{
public function __construct(
public readonly int $orderId,
public readonly string $status,
) {}
}
class StoreOrderHandler
{
public function __construct(
private OrderRepository $orders
) {}
public function handle(StoreOrderDataInput $input): StoreOrderDataOutput
{
$order = new Order(
userId: $input->userId,
);
$orderSaved = $this->orders->save($order);
return new StoreOrderDataOutput(
orderId: $orderSaved->id,
status: $orderSaved->status
);
}
}
Organizacja i testowanie UseCase w projektach
W Laravel zaleca się przechowywanie UseCase w strukturze, np. app/UseCases/V1, z wersjonowaniem dla bezpiecznej zmiany kontraktów. Katalogi wewnątrz odpowiadają na pytanie "Co potrafi system?", np. StoreOrder, CancelOrder, RegisterUser. Odzwierciedla to możliwości biznesowe systemu, a nie szczegóły techniczne.
Testowanie UseCase upraszcza się dzięki testom integracyjnym na poziomie scenariusza. Przykład testu dla StoreOrderHandler:
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
class StoreOrderFeatureTest extends TestCase
{
use RefreshDatabase;
public function testUserCanCreateOrder(): void
{
$user = User::factory()->create();
$payload = [
'items' => [
['id' => 10, 'qty' => 2],
['id' => 15, 'qty' => 1],
],
];
$response = $this->actingAs($user)
->postJson('/api/orders', $payload);
$response->assertStatus(200);
$response->assertJsonStructure([
'orderId',
'status'
]);
$this->assertDatabaseHas('orders', [
'user_id' => $user->id
]);
}
}
Co jest ważne
- UseCase opisuje scenariusze biznesowe, a nie operacje techniczne, poprawiając strukturę aplikacji.
- Wzorzec rozwiązuje problem rozpraszania logiki w dużych projektach, czyniąc kod bardziej sterowalnym.
- Jeden UseCase odpowiada jednemu procesowi biznesowemu, z wyodrębnieniem powtarzalnej logiki do oddzielnych serwisów.
- Testowanie jest ułatwione dzięki testom integracyjnym, które sprawdzają cały scenariusz.
- Struktura projektu odzwierciedla możliwości biznesowe, co przyspiesza włączanie nowych deweloperów.
— Editorial Team
Brak komentarzy.