régen a föld Scala alakult új típusú biztonságos módja függőség injekció. Hosszú távon több bajt okoz, mint amennyit ér.
torta minta
emlékeztessük, mi a torta minta. Ahhoz, hogy megértsük, meg kell ismernünk az öntípusokat. Úgy tervezték, hogy keverékként használják őket, és többé-kevésbé így néznek ki:
trait UserRepository { def fetchUsers(userIds: Seq): Seq}class UserRepositoryDbImpl(database: Database) extends UserRepository { def fetchUsers(userIds: Seq): Seq = ...}trait UserRepositoryLogging { self: UserRepository => private lazy val logger: Logger = ... override def fetchUsers(userIds: Seq): Seq = { val users = super.fetchUsers(usersIds) logger.debug(s"""Fetched users by ids: ${users.mkString(", ")}""") users }}val userRepository: UserRepository = new UserRepositoryDbImpl(database) with UserRepositoryLogging
itt hozzáadhatjuk a UserRepositoryLogging
tulajdonságot bármihez, ami UserRepository
megvalósítás – másképp nem fordítana. Ezenkívül a UserRepositoryLogging
belsejében feltételezzük, hogy ez a UserRepository
megvalósítása. Tehát minden elérhető belül UserRepository
ott is elérhető.
most, mivel hozzáférhetünk az öntípushoz használt deklarált típus(ok) hoz, ezt megtehetjük:
trait UserRepository { def fetchUsers(userIds: Seq): Seq}trait UserRepositoryComponent { def userRepository: UserRepository}trait UserRepositoryComponentImpl extends UserRepositoryComponent { lazy val userRepository: UserRepository = new UserRepository { // ... }}trait SomeController { self: UserRepositoryComponent => def handleRequest(userIds: Seq): Response = { // ... userRepository.fetchUsers(userIds) // ... }}object SomeController extends SomeController with UserRepositoryComponentImpl
belülSomeController
deklaráljuk önálló típus, amely biztosítja,, hogy a példányosítható végrehajtása leszuserRepository
módszer. Tehát a mixin hozzáadásával biztosítjuk a megvalósítást, így biztosítjuk a függőség befecskendezését a fordítási időben. Futásidejű reflexió nélkül, típusbiztonsággal, további konfigurációk vagy könyvtárak nélkül.
minden ilyen komponens lehet egy réteg az alkalmazás (üzleti logika réteg, az infrastruktúra réteg, stb), hogy valaki, mint a rétegek a torta. Tehát azáltal, hogy az alkalmazás ilyen módon hatékonyan sütés egy tortát. Így torta minta.
most … miért probléma ez?
függőségek hozzáadása
nagyobb projektekben a szolgáltatások, adattárak és segédprogramok száma biztosan nagyobb lesz, mint 1-2. Tehát a tortád így fog kinézni:
object PaymentTransactionApiController extends TransactionApiController with ConfigComponentImpl with DatabaseComponentImpl with UserRepositoryComponentImpl with SessionRepositoryComponentImpl with SecurityServicesComponentImpl with ExternalPaymentApiServicesComponentImpl with PaymentServicesComponentImpl with TransactionServicesComponentImpl
ami azt illeti, egyszer láttam egy tortát, ahol ComponentImpl
2-3 képernyőn ment keresztül. Nem csak a szükséges alkatrészeket injektálta oda, mindent egyszerre kombináltál: infrastruktúra, domain szolgáltatások, kitartás, üzleti logika…
most képzelje el, mi történik, ha hozzá kell adnia egy függőséget. Először adjon hozzá egy másikwith Dependency
elemet az öntípusához. Ezután ellenőrizze, hogy hol használják a frissített tulajdonságot. Lehet, hogy itt is hozzá kell adnia az öntípust. Felmászik a mixins fára, amíg el nem ér egy osztályt vagy egy tárgyat. Uff.
kivéve, hogy ezúttal ezt maga adta hozzá, tehát tudja, mit kell hozzáadni. De ha csinálsz rebase vagy konfliktus megoldása, lehet, hogy a végén egy olyan helyzetben, amikor néhány új függőség jelent meg, hogy nincs. A fordító hibája csak azt mondja, hogy self-type X does not conform to Y
. 50 + összetevővel akár azt is kitalálhatja, melyik nem sikerül. (Vagy kezdje el bináris keresést az összetevők eltávolításával, amíg a hiba eltűnik).
amikor a torta mérete és mennyisége tovább növekszik, érdemes lehet végrehajtani a száraz szabályt, és kisebb süteményekre osztani, amelyeket később összerakunk. Ez a pokol új mélységeit tárja fel.
függőségek eltávolítása
normál DI esetén, amikor abbahagyja valamilyen objektum használatát, az IDE/fordító megmondhatja, hogy már nincs rá szükség, ezért eltávolíthatja. A torta minta, akkor nem kell tájékoztatni az ilyen dolgokat.
ennek eredményeként a tisztítás sokkal nehezebb, és nagyon is lehetséges, hogy egy bizonyos pillanatban sokkal több függőség lesz a végén, mint amire valóban szüksége van.
fordítási idő
mindez összeadja a fordítási időt. Lehet, hogy első pillantásra nem nyilvánvaló, de a végén:
trait ModuleAComponentImpl { self: ModuleBComponent => }trait ModuleBComponentImpl { self: ModuleAComponent => }
ami egy ciklikus függőség. (Mindannyian tudjuk, hogy rosszak, de lehet, hogy észre sem veszi, hogy éppen létrehozott egyet. Különösen, ha mindent lustán inicializál).
tehát ez a 2 tulajdonság nagyon gyakran össze lesz állítva, és a cink inkrementális fordító nem segít ebben. Ezenkívül a torta bármely összetevőjének módosítása kiváltja a fölötte lévő dolgok újrafordítását a függőségi grafikonon.
tesztelés
a ciklikus függőségek újabb problémát okoznak. Lehet, hogy nagyon jól a végén valami hasonló:
trait AComponentImpl { self: BComponent => lazy val a: A = new A { b.someMethod() // use b }}trait BComponentImpl { self: AComponent => lazy val b: B = new B { a.someMethod() // use a }}object Cake extends AComponentImpl with BComponentImplCake.a // NPE!Cake.b // NPE!
minden összetevő a saját fájljában lesz, így nem veszi észre ezt a függőséget. Tesztek mock nem segít, hogy:
trait BComponentMock { val b: B = mock }val aComponent = new AComponentImpl with BComponentMock {}aComponent.a // works!
így lesz minden rendben és dandy, amíg ténylegesen futtatni a kódot. Tudod – a kód típusbiztos, ami azt jelenti, hogy az építendő érték a típuskorlátokat követi. De ezek a korlátozások nem jelentenek semmit, mivel a Nothing
(dobott kivétel) a kívánt típus tökéletesen érvényes altípusa.
inicializálási sorrend
az előző probléma ehhez kapcsolódik: mi az összetevő inicializálásának sorrendje?
normál DI esetén nyilvánvaló-mielőtt egy dolgot a másikba helyezne, létre kell hoznia. Tehát csak nézd meg a sorrendet, amelyben objektumokat hoz létre.
egy tortával elfelejtheti. Az összetevőkön belüli értékek gyakran lazy val
s vagy object
s. tehát az első elérhető attribútum megpróbálja példányosítani a függőségeit, amely megpróbálja példányosítani a függőségeit stb.
még akkor is, ha a val
S – vel megyünk, sorrend kérdése, hogy milyen torta-tudod, vonás linearizáció. Mindaddig, amíg minden egyes komponenst pontosan egyszer használnak – nem probléma. De ha megszárad a torta, és egyes összetevők ismétlődnek, amikor egyesítjük őket …
egy tökéletes világban az olyan dolgok, mint az inicializálási sorrend, nem számítanak, de gyakran igen, és szeretnénk tudni, hogy pl. mikor kezdődött a kapcsolat a DB-vel vagy a Kafka téma előfizetésével, és naplózni a dolgokat a lehetséges hibák hibakeresése érdekében.
Boilerplate and lock-in
ahogy valószínűleg észrevette, ez a minta sok boilerplate – t generál-alapvetően minden tárolónak vagy szolgáltatásnak csomagolónak kell lennie, csak a függőségek megjegyzéséhez.
Ha megy le, hogy az út észre fogod venni, hogy elég nehéz megszabadulni a torta minta. Minden megvalósítás egy tulajdonságba kerül, és függőségei a hatókörből származnak, nem paramétereken keresztül. Tehát minden osztály esetében először refaktort kell végrehajtania ahhoz, hogy a burkolat nélkül példányosíthassa.
de akkor a függőségeket is újra kell írni és így tovább. A tortamintázat egy csúnya elkötelezettség, amely fáj és ellenzi Önt, amikor megpróbál fokozatosan visszavonulni tőle.
szó szerint, bármilyen más formája di Scala, hogy tudom (futásidejű reflexió a ‘la Guice, makró kommentárok a’ la MacWire, implicit, ezek kombinációi 3) lehetővé teszi, hogy azt mondják, stop! bármelyik pillanatban fokozatosan váltson másik megoldásra.
összefoglaló
Cake minta egy megoldás, hogy a DI probléma, hogy hosszú távon nem több kárt, mint hasznot. Azt hallottam, hogy a Scala fordítóban használták, és sajnálják. Ez volt az alapértelmezett DI mechanizmus a Play Framework számára, de mindezen kérdések miatt a szerzők úgy döntöttek, hogy átváltanak a Guice – ra-igen, a funkcionális programozók a futásidejű reflexiót részesítették előnyben a type-safe cake helyett, így ez sokat mond.
milyen alternatívák vannak?
- sima régi kézi érvelés – nem kell elmagyarázni, hogy
- implicts – megvannak az előnyei, amíg a projekt által fenntartott több, mint magad, és munkatársai elkezd panaszkodni, hogy nem tudja, mi történik, lassú összeállítás, nem értik implices…
- runtime reflection – Guice egy érett keretet csinál DI pontosan úgy, ahogy utálom. Sok projektben használták, a Java programozók imádják – akárcsak a tavaszt. Némelyikük hagyja absztrakció szivárgás átadásával injektor az osztályba. Néhányan azt akarják, hogy a konfigurációjuk rugalmas legyen-olyan rugalmas, hogy az osztályvezetékek futás közben meghibásodhatnak, bleh!
- MacWire-az argumentumok manuális átadása és a makrók használata mindenhez-megvizsgálja az aktuális hatókört fordítási időben, és befecskendezi a függőségeket az Ön számára. Ha néhány hiányzik összeállítás nem sikerül. Közben írja csak
wire
- Airframe – makro-alapú keret, ahol építeni recept DI egy DSL. Támogatja az objektum életciklus-kezelését
- pulp – my shameless auto-promotion. Implikációkat és makró kommentárokat használt annak érdekében, hogy szolgáltatókat generáljon az osztályokhoz
IMHO mindegyiknek jobban kell működnie, mint a torta minta.