Slovník pojmů
Polymorfismus
Polymorfismus dovoluje, aby aplikační služba používala kartu, bankovní převod i testovací fake přes stejný významový kontrakt. Nejde jen o shodný název metody, ale o dodržení stejného pozorovatelného chování.
Stručná definice
Více typů, jeden skutečný kontrakt.
Volající pracuje s rozhraním nebo rodičovským typem. Konkrétní implementace se vybere při sestavení aplikace nebo za běhu a dynamické volání provede její metodu. Pokud implementace zachová kontrakt, přidání nové varianty nemění volající kód.
Polymorfismus může vzniknout přes dědičnost i přes interface. Dependency injection pouze dodá vybranou závislost; samo ještě nedokazuje, že kontrakt je malý, srozumitelný a behaviorálně dodržený.
Jaký problém řeší
Odděluje stabilní pravidlo od variantního detailu.
Hodí se tam, kde aplikace má několik skutečně zaměnitelných implementací se stejným významem.
- platební gateway pro kartu, převod a test
- úložiště s databázovou a paměťovou implementací
- notifikace e-mailem, SMS nebo fake pro test
- integrační adaptér pro několik providerů
Praktický příklad
Checkout závisí na PaymentGateway, ne na poskytovateli
CheckoutService zná jen kontrakt PaymentGateway. CardGateway a BankTransferGateway jej implementují pro skutečný provoz, FakePaymentGateway pak vrací předvídatelný výsledek v testu.
Nová brána se přidá bez změny CheckoutService jen tehdy, když vrací PaymentStart se stejným významem. Pokud potřebuje zcela jiné pokračování nebo zpřísní pravidla, je potřeba upravit kontrakt či oddělit use case, ne rozdíl skrýt za interface.
PHP
interface PaymentGateway
{
public function start(Order $order): PaymentStart;
}
final class CardGateway implements PaymentGateway
{
public function start(Order $order): PaymentStart
{
return PaymentStart::redirectToCardProvider();
}
}
final class BankTransferGateway implements PaymentGateway
{
public function start(Order $order): PaymentStart
{
return PaymentStart::waitingForTransfer();
}
}
final class FakePaymentGateway implements PaymentGateway
{
public function start(Order $order): PaymentStart
{
return PaymentStart::completedForTest();
}
}
final class CheckoutService
{
public function __construct(private PaymentGateway $gateway) {}
public function beginPayment(Order $order): PaymentStart
{
return $this->gateway->start($order);
}
}
Jak funguje
Výběr platební implementace přes kontrakt
Schéma ukazuje spolupráci přes interface, nikoli nutnost vytvořit interface pro každou třídu.
- CheckoutService Závisí na typu PaymentGateway.
- Společný kontrakt Určuje význam operace start a návratové hodnoty.
- Konkrétní gateway CardGateway nebo BankTransferGateway provede vlastní technické kroky.
- Testovací fake Dá testu předvídatelnou implementaci stejného kontraktu.
Hlavní části a varianty
Kontrakt je důležitější než počet tříd.
Polymorfní návrh má přinést skutečnou zaměnitelnost, ne jen další vrstvu názvů.
Rozhraní
Vyjadřuje operace a jejich význam, na kterém mohou záviset klienti.
Rodičovský typ
Může poskytnout polymorfní hranici přes dědičnost, pokud potomci zachovají kontrakt.
Substituce
Jedna implementace nahradí druhou bez změny správnosti volajícího kódu.
Testovací implementace
Fake nebo stub dovolí ověřit službu bez skutečné externí platební brány.
Omezení a časté chyby
Abstrakce musí odpovídat skutečné variabilitě.
Kde pomáhá
- přidat novou kompatibilní bránu bez změny klienta
- testovat službu s fake implementací
- oddělit business pravidlo od technického providera
Rizika
- interface pro každou třídu bez druhého klienta nebo kontraktu
- implementace s rozdílným behaviorálním významem
- mnoho malých implementací zhoršujících orientaci
- představa, že DI samo zajistí dobrý návrh
Praktické použití
Zaměnitelnost je třeba dokázat scénářem.
Rozhraní má vzniknout tehdy, když existuje skutečný stabilní kontrakt, více implementací nebo rozumná potřeba izolovat hranici v testu. Ne každá třída potřebuje vlastní interface.
Pro každou implementaci je vhodné ověřit stejný kontraktní scénář. Jinak aplikace jen přesune podmínky z klienta do implementací a ztratí předvídatelnost.
Polymorfismus není dědičnost ani přetěžování metod podle signatur a není totéž co generické typy. Dědičnost může nabídnout společný rodičovský typ, rozhraní však často stačí bez něj. Pro malou stabilní volbu může být čitelnější obyčejná podmínka; abstrakci má smysl přidat až pro skutečnou zaměnitelnost.
Kontraktní test pro PaymentGateway může například ověřit, že každá implementace vrátí srozumitelný stav zahájení platby a neprovede platbu dvakrát při stejném požadavku. Fake pak testuje CheckoutService bez sítě, ale nesmí předstírat chování, které skutečná brána neumí. Takové scénáře chrání substituci lépe než samotná shoda názvů metod.
Volající služba proto nesmí podle konkrétní třídy obcházet význam rozhraní a doplňovat vlastní výjimky pro jednu gateway. Když takové větvení vznikne, je potřeba zjistit, zda kontrakt neobsahuje dvě odlišné odpovědnosti. Někdy je správným řešením rozdělit use case nebo ponechat jednoduchou větev, ne za každou cenu zachraňovat příliš obecné rozhraní.
Polymorfismus je užitečný hlavně na stabilní hranici, kde se technický detail může měnit častěji než business pravidlo. CheckoutService tak může zůstat soustředěný na zahájení platby, zatímco konkrétní gateway řeší komunikaci s poskytovatelem. Pokud se každá implementace musí ověřovat řetězcem if podle své třídy, společný kontrakt pravděpodobně nepopisuje skutečné společné chování.
Kontrola změny
Ověřte chování každé implementace stejnou otázkou.
Kontrakt nesmí být jen seznam metod; musí popsat, co klient může očekávat.
- popsat význam vstupu a výstupu rozhraní
- testovat alternativní implementace společným scénářem
- nepřidávat interface bez skutečné hranice
- ponechat jednoduchý if tam, kde je čitelnější
Časté otázky
Polymorfismus v praxi
Je pro polymorfismus nutná dědičnost?
Ne. Často stačí interface, který implementují nezávislé třídy.
Musí mít každá třída interface?
Ne. Interface má hodnotu při skutečném kontraktu nebo zaměnitelnosti, ne jako povinný doplněk.
Co je substituce?
Možnost nahradit jednu implementaci jinou bez porušení očekávání klienta.
Kdy je if lepší?
Když je rozhodnutí malé, stabilní a abstrakční hierarchie by přidala více složitosti než jasnosti.
Osobní zkušenost
Kontrakty mají chránit změnu, ale zůstat malé a pravdivé.
V architektuře API a PHP aplikací používám polymorfismus tam, kde skutečně oddělí stabilní pravidlo od proměnlivé integrace.