Brick Broadcasting
(CC BY-NC 2.0)

Psal jsem o tom, v čem se liší servisní firma od produktové a že produktové firmy mají jistá úskalí. Jedním z nich je zpětná kompatibilita. A mluvíme-li o zpětné kompatibilitě, jistě v tom bude hrát roli API.

Pravidelní čtenáři vědí, že jeden z důvodů, proč píšu tento blog, je čistě sobecký: Díky tomu si utřídím myšlenky. Je možné, že až se k nim za pár let vrátím, sám sobě se vysměju. Jindy mi to pomáhá dohledat si návod. A co si dnes myslím o API?

API versus SPI

Čtěte Tulacha, tam to všechno je! Pokud se necítíte na celou knihu Practical API Design, mrkněte alespoň na jednotlivé články, například APIvsSPI. Termín API je možná příliš obecný a spoustu problémů, alespoň v našem případě, plyne z toho, že se dostatečně nerozlišuje (případně dokonce míchá) client API (či jen prostě API) a SPI (Service Provider API, provider API). Řekněme, že programujete nějaký systém, do kterého lze dopsat vlastní pluginy. Plugin může volat funkčnost systému přes API. Rozšíření, které bude volané systémem, předepisuje právě SPI (můžeme také nazvat extension point).

Tady by chtělo příklad a hned problémový. java.util.List je obvykle API, které chcete volat, ale pro implementátory třídy ArrayList to bylo SPI.

Jak se z toho nezbláznit

Jako ideální stav se mi jeví mít oddělený repositář, nebo alespoň release cyklus samotného systému, API i SPI. Mám chuť říct, že nám to umožní vydávat verze zcela nezávisle, ovšem omezení rozsahu vzájemně kompatibilních verzí potřebujete hlídat v kouzelné tabulce zvané compatibility matrix.

Poučení ze SOLID

K druhému z návrhových principu SOLID, tedy OCP (Open-Closed Principle) se stále propracovávám.

Třídy by měly být otevřené pro rozšiřování, ale uzavřené pro změny.

V našem případě, kdy se produkt tvořil (a k dokončení bylo daleko), kladli všichni nějaké požadavky, které nebylo možné v dané čase uspokojit, takže systém nebyl vhodně uzavřený pro změny, nebylo jasně definované API ani SPI, proto kdekdo sahal na co se mu zlíbilo a dědil od tříd, které našel. Ostatně bez toho by nic nedodali. K tomu se vystavilo spoustu protected metod, nikoliv však final, aby hrály roli API, nýbrž roli SPI (mohli si je překrýt).

Na zásadní změny (i vhledem ke zpětné kompatibilitě) nebylo ani pomyšlení. Začali jsme tedy uzavírat. Protected metody jsme téměř přestali psát, zato jsme hojně sahali po modifikátoru final. Implementace jsme začali psát package private (protože jinak je někdo mohl mylně považovat za SPI). Přidání argumentu do konstruktoru je binárně zpětně nekompatibilní změna, museli byste jich podporovat více současně, takže je nahrazujeme buildery.

Se servisní částí firmy se špičkujeme, oni nás mají za profesory. Ale kdo bude API roky udržovat, co?

Zavření pro změny bychom tedy měli, ale co teď s otevřeností pro rozšiřování? Chtěl bych ukázat dvě možnosti.

Rozhraní k implementaci

V rámci SPI můžeme definovat rozhraní vhodné k implementaci, které systém volá s definovaným kontextem. Dependency injection posbírá všechny instance, nebo jen podle určitého klíče, záleží na vás. Spring třeba umí vyhledat beanu určité třídy a daného jména. Můžete implementace registrovat ve svém systému dynamicky jinak či pouze staticky, představivosti se meze nekladou, .

Události

Volnějším kontraktem jsou události. Ne nadarmo je programovací jazyk Erlang (a nejen on) postavený na zasílání zpráv (message passing).

Přidržím se svého světa Spring frameworku a jeho ApplicationEvent. Je potřeba si uvědomit, zda je pro vás žádoucí, aby listener mohl výjimkou shodit byznys logiku v místě, kde je událost publikovaná (asi spíš ne). Dále zda chcete událost za každou cenu nebo až v případě, kdy úspěšně doběhla i logika po publikování, tedy po commitu. K tomu slouží TransactionPhase.AFTER_COMMIT přepínač u @TransactionalEventListener, kde zachytáváme interní události našeho systému a publikujeme události, které jsou součástí SPI a u kterých je garantované, že jsou publikované až po commitu. Pozor, má to taky svoje mouchy.

V odkazovaném článku upozorňují na Domain Driven Design, že nemáme posílat jen userId, ale celého uživatele respektive stav v daném okamžiku, protože v případě konzumace události už to může být úplně jinak. Dodávám: Ať daný objekt není JPA entita, přece nemusíte opakovat stejnou chybu jako my.

Java Interface

Přidání metody do java rozhraní je zpětně nekompatibilní změna. Java 8 zavedla výchozí implementace default methods asi hlavně proto, aby vyřešili zpětnou kompatibilitu například u kolekcí. Nabízí se použít funkčnost i pro naše SPI (u API to problém není). Znovu Jaroslav Tulach, tentokrát DefaultMethods, kde proti tomu brojí. Vyřešíte tak binární kompatibilitu (ne vždy), s chováním je to horší. Uznává použití u listener adapterů.

Osobně jsem se možná příliš upínal k nafukování stávajících rozhraní. Hodí se zvážit, zda nemůžete založit nové rozhraní, což nevylučuje, že se nakonec nesejdou v jedné implementační třídě.

Nekompatibilní změnou je pochopitelně i odebrání metody či změna signatury. Stávající metody je potřeba zachovat a mít na paměti, že je stále někdo může volat. Nejprve je nutné označit za deprecated a poskytnout návod, jak zmigrovat na nový kód.

Závěr

Důsledně rozlišujte a oddělujte API a SPI. Včas uzavřete pro změny a najděte si vhodný způsob pro rozšiřování. Naučte se systematicky pracovat s označováním kódu za depracated. Za živelným vývojem se jen obtížně dělá tlustá čára.

Související