kubuszok.com

1. Února 2018• série: Náhodný o Scala • kategorie: scala dort vzor dependency injection

kdysi dávno v zemi Scala se objevil nový typ-bezpečný způsob, injekce závislost. Z dlouhodobého hlediska přináší více problémů, než stojí za to.

dort vzor

připomeňme, co je dort vzor. Abychom tomu porozuměli, musíme znát vlastní typy. Byly určeny k použití jako mixiny a vypadají víceméně takto:

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

Zde můžeme přidat vlastnost UserRepositoryLogging něco, co je UserRepository realizace – to bych sestavit jinak. Navíc uvnitř UserRepositoryLogging předpokládáme, že se jedná o implementaci UserRepository. Takže vše přístupné v UserRepository je také přístupné tam.

Teď, když máme přístup, co byl prohlášen za typ(y), používané pro self-typ, můžeme to udělat:

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

V SomeController prohlašujeme, self-typ, který zajišťuje, že jeho instantiable provádění bude mít userRepository metoda. Takže přidáním mixin zajišťujeme implementaci, a tak zajistíme, že závislost je injektována v době kompilace. Bez odrazu za běhu, s typovou bezpečností, bez dalších konfigurací nebo knihoven.

každá taková komponenta může být vrstvou naší aplikace (business logic layer, infrastructure layer atd.), kterou někdo porovnává s vrstvami dortu. Takže tím, že vytvoříte aplikaci takovým způsobem, že účinně pečete dort. Tak dort vzor.

teď … proč je to problém?

přidání závislostí

ve větších projektech bude počet služeb, úložišť a utilit jistě větší než 1-2. Takže váš dort bude vypadat jako:

object PaymentTransactionApiController extends TransactionApiController with ConfigComponentImpl with DatabaseComponentImpl with UserRepositoryComponentImpl with SessionRepositoryComponentImpl with SecurityServicesComponentImpl with ExternalPaymentApiServicesComponentImpl with PaymentServicesComponentImpl with TransactionServicesComponentImpl

Jako věc skutečnosti, jsem viděl jednou na dort, kde ComponentImpl šel přes 2-3 obrazovkách. Nevstřikovali jste tam pouze komponenty-ty potřebné-najednou, kombinovali jste vše dohromady najednou: infrastruktura, doménové služby, vytrvalost, obchodní logika…

nyní si představte, co se stane, když potřebujete přidat závislost. Začnete přidáním dalšího with Dependency do svého vlastního typu. Poté zkontrolujete, kde se používá aktualizovaná vlastnost. Možná budete muset přidat vlastní typ i zde. Vylezete na strom mixinů, dokud nedosáhnete třídy nebo předmětu. Uff.

kromě toho, tentokrát jste to přidali sami, takže víte, co je třeba přidat. Ale když děláte rebase nebo řešení konfliktu, můžete skončit v situaci, kdy se objeví nějaká nová závislost, kterou nemáte. Chyba kompilátoru říká pouze to, že self-type X does not conform to Y. S 50 + komponenty, můžete také hádat, který z nich selže. (Nebo začít dělat binární vyhledávání s odstraněním komponent, dokud chyba zmizí).

když velikost dortu a množství dále roste, možná budete chtít implementovat suché pravidlo a rozdělit ho na menší koláče, které budou později sestaveny. To odhaluje nové hloubky pekla.

odstranění závislostí

s normálním DI když přestanete používat nějaký objekt, váš IDE / kompilátor vám může říci, že již není potřeba, takže jej můžete odstranit. S dort vzor, nebudete informováni o takových věcech.

výsledkem je, že vyčištění je mnohem obtížnější a je docela možné, že v určitém okamžiku skončíte s mnohem více závislostmi, než skutečně potřebujete.

doba kompilace

to vše přispívá k době kompilace. Na první pohled to nemusí být zřejmé, ale můžete skončit:

trait ModuleAComponentImpl { self: ModuleBComponent => }trait ModuleBComponentImpl { self: ModuleAComponent => }

což je cyklická závislost. (Všichni víme, že jsou špatné, ale možná si ani nevšimnete, že jste právě vytvořili jeden. Zvláště pokud inicializujete vše líně).

takže tyto vlastnosti 2 budou velmi často kompilovány společně a inkrementální kompilátor zinku s tím nepomůže. Navíc změny kterékoli ze složek v dortu způsobí rekompilaci věcí nad ním v grafu závislosti.

testování

cyklické závislosti vytvářejí další problém. Můžete velmi dobře skončit s něčím jako:

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!

každá komponenta bude ve svém vlastním souboru, takže si této závislosti nevšimnete. Testy s předstíranou vám nepomůže s tím:

trait BComponentMock { val b: B = mock }val aComponent = new AComponentImpl with BComponentMock {}aComponent.a // works!

takže to bude vše v pořádku a dandy, dokud jste skutečně spustit kód. Víte-kód je typově bezpečný, což znamená, že hodnota, kterou vytvoříte, bude následovat omezení typu. Tato omezení však nic neznamenají, protože Nothing (hozená výjimka) je dokonale platný podtyp požadovaného typu.

pořadí inicializace

předchozí problém souvisí s tímto: jaké je pořadí inicializace komponenty?

s normálním DI je zřejmé-než vložíte jednu věc do druhé, musíte ji vytvořit. Takže se jen podíváte na pořadí, ve kterém vytváříte objekty.

s dortem můžete na to zapomenout. Hodnoty v rámci komponenty budou často lazy valnebo objecty. Takže první přístupný atribut se bude snažit vytvořit instanci jeho závislosti, který se bude snažit vytvořit instanci jeho závislosti, atd.

i když jdeme s val s je to otázka pořadí, ve kterém jsme složili dort-víte, rys linearizace. Dokud bude každá součást použita přesně jednou-žádný problém. Ale pokud máte Sušenou vás dort, a některé komponenty opakovat, při sloučení je…

V dokonalém světě takové věci, jako je inicializace cílem by nemělo vadit, ale dost často to dělá, a chtěli bychom vědět, např. když nějaké připojení k DB nebo předplatné Kafka téma začal, a ukládat věci do pořádku ladění potenciální selhání.

Boilerplate a lock-in

Jak jste si pravděpodobně všimli, tento vzor generuje mnoho boilerplate-v podstatě každé úložiště nebo služba by musela mít wrapper, pouze pro anotaci závislostí.

jakmile se vydáte touto cestou, všimnete si, že je docela obtížné se zbavit dortu. Každá implementace je vložena do znaku a její závislosti pocházejí z rozsahu, nikoli z parametrů. Takže pro každou třídu byste museli nejprve provést refaktor, abyste jej mohli vytvořit bez obalu.

ale pak by musely být také změněny závislosti a tak dále. Dort vzor je ošklivý závazek, že bolesti a oponuje vám, když se pokusíte odstoupit od něj postupně.

doslova jakákoli jiná forma DI ve Scale, kterou znám (runtime reflection a ‚la Guice, makro anotace a‘ la MacWire, implicits, kombinace těch 3) Vám umožňuje říci stop! v každém okamžiku a postupně přejít na jiné řešení.

Shrnutí

Dort vzor je řešení DI problém, že v dlouhodobém horizontu více škody, než užitku. Z toho, co jsem slyšel, že byl použit v kompilátoru Scala, a litují toho. To byl výchozí DI mechanismus pro Play Framework, ale protože na všechny tyto otázky se autoři rozhodli přejít na Guice – ano, funkční programátoři přednost runtime reflexe, typ-safe dort, tak, že říká hodně.

jaké jsou tedy alternativy?

  • obyčejný starý ruční předávání parametrů – není třeba vysvětlovat, že
  • implicits – mají své výhody, dokud váš projekt je udržována tím, že více než na sebe, a kolegové začnou si stěžovat nevědět, co se děje, pomalu kompilace, není porozumění implicits…
  • runtime reflexe – Guice je zralý rámce dělat DI přesně tak, jak to nesnáším. Byl použit v mnoha projektech, programátoři Java to milují-stejně jako milují jaro. Někteří z nich nechávají abstrakci unikat průchodem injektoru do třídy. Někteří z nich chtějí, aby jejich konfigurace byla flexibilní – tak flexibilní , že vedení třídy může za běhu selhat, bleh!
  • MacWire-v polovině předávání argumentů ručně a pomocí maker pro všechno-skenuje aktuální rozsah v době kompilace a vstřikuje závislosti pro vás. Pokud některé chybí kompilace selže. Ti zatím napsat jen wire
  • Draku – makro-based rámec, kde budete stavět recept na DI s DSL. Má podporu pro správu životního cyklu objektů
  • pulp-my shameless auto-promotion. To používá implicits a makro popisy za účelem generování služeb pro třídy

IMHO každý z nich by měl fungovat lépe, než dort vzor.

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna.