Polyglot microservices rendszerekben az interfészdefiníciókkal van a legtöbb baj. C#-ban megírod az IOrderService-t, aztán átírod Java-ban, aztán Python-ban. Három fájlt tartasz karban három helyen, és előbb-utóbb az egyik eltér a többitől. A hiba a rendszerhatáron jön elő, éles környezetben, amikor a két oldal az interfész két különböző verzióját használja.

A forrás egyetlen legyen

A UniContract ezt oldja meg. Az interfészt egyszer írod le egy .contract fájlban, és a tool kigenerálja minden célplatformra. Kézzel nem kell szinkronizálni, ezt a generátor végzi, ezért a platformok változatai nem térhetnek el egymástól.

Így néz ki egy contract definíció:

namespace DataStoring {
    interface IDataStore {
        readonly property Connection: IConnection
        async method IsCollectionExists(name: string) => boolean
        async method GetCollectionByName<T constraint IEntity>(name: string) => ICollection<T>
    }
}

Ebből a definícióból C# interfész, Java interface és Python abstract class generálható. Mindhárom ugyanazt a contractot valósítja meg, és ha a contract megváltozik, az újragenerálás után mindhárom vele változik.

A fordítóhoz hasonló pipeline

Az architektúra egy fordítóét követi. ANTLR 4 alapú lexer és parser dolgozza fel a .contract fájlokat, a syntax tree-ből element builder épít szemantikus modellt, a semantic checker ellenőrzi az összefüggéseket, végül a cserélhető (pluggable) emitterek generálják a célnyelvi kódot.

Az emitterek Python modulok, így bárki írhat sajátot anélkül, hogy a core engine-hez nyúlna. Ha új platform kell (Rust, Go, TypeScript), elég hozzá egy új emitter modult írni.

Tervezési döntések

A constraint-alapú generikusok (<T constraint IEntity>) miatt a típusbiztonság minden platformon megmarad. Ez nem triviális, mert a Java, a C# és a Python egészen másképp kezeli a generikusokat; a contract szintjén viszont egyetlen jelentésük van.

Az async kulcsszóból C#-ban Task, TypeScriptben Promise, Pythonban async def lesz. Azt, hogy egy metódus aszinkron-e, a contract mondja meg, nem kell platformonként külön eldönteni.

Az import rendszerrel a contractok hivatkozhatnak egymásra. A PolyPersist például az összes store interfészét UniContract definíciókból veszi, ezért a C# és a többi platform implementációja ugyanazt az interfészt valósítja meg.

Hol használom?

A PolyPersist összes cross-platform interfészét a UniContract generálja. A D3I is erre épít, hogy az emitterek kimenete egymással összhangban maradjon. Ahol több platformon kell ugyanazt a contractot megvalósítani, ott ebből generálom a kódot.