kubuszok.com

1 Feb 2018• serie: Random about Scala • tags: scala cake pattern dependency injection

lang geleden ontstond in het land van Scala een nieuwe type-veilige manier van dependency injection. Op lange termijn brengt het meer problemen met zich mee dan het waard is.

Cake patroon

laten we eraan herinneren wat een cake patroon is. Om het te begrijpen moeten we zelftypen kennen. Ze waren bedoeld om te worden gebruikt als mixins en zien er min of meer zo uit:

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

Hier kunnen we eigenschap UserRepositoryLoggingtoevoegen aan iets datUserRepositoryimplementatie – het zou anders niet compileren. Bovendien, binnen UserRepositoryLogging gaan we ervan uit dat het de implementatie is van UserRepository. Dus alles wat toegankelijk is binnen UserRepository is daar ook toegankelijk.

nu, omdat we toegang kunnen krijgen tot het (de) gedeclareerde type (s) dat (die) gebruikt werd voor het zelftype, mogen we dit doen:

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

binnen SomeControllerwe declareren self-type, dat ervoor zorgt dat de instanteerbare implementatieuserRepositorymethode heeft. Dus door het toevoegen van de mixin zorgen we voor de implementatie en zorgen we ervoor dat de afhankelijkheid wordt geïnjecteerd tijdens het compileren. Zonder runtime reflectie, met type-veiligheid, geen extra configuraties of bibliotheken.

elk van deze componenten kan een laag van onze applicatie zijn (business logic laag, de infrastructuur laag, enz.), die iemand vergeleek met de lagen van de cake. Dus, door het creëren van uw toepassing op een dergelijke manier bent u effectief het bakken van een taart. Dus cake patroon.

nu … waarom is dat een probleem?

afhankelijkheden toevoegen

In grotere projecten zal het aantal services, repositories en hulpprogramma ‘ s zeker groter zijn dan 1-2. Uw taart zal er dus uitzien als:

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

omdat ik ooit een taart zag waar ComponentImpl 2-3 schermen doorliep. Je injecteerde er niet alleen componenten-die-nodig-zijn-tegelijk, je combineerde alles in één keer.: infrastructure, domain services, persistence, business logic…

stel je nu voor wat er gebeurt als je een afhankelijkheid moet toevoegen. U begint met het toevoegen van een andere with Dependency aan uw zelf-type. Dan, u controleren waar de bijgewerkte eigenschap wordt gebruikt. Mogelijk moet je hier ook zelftype toevoegen. Je klimt in de boom van mixins tot je een klasse of een object bereikt. Uff.

behalve dat je dit deze keer zelf hebt toegevoegd, zodat je weet wat er moet worden toegevoegd. Maar als je rebase doet of een conflict oplost, kan het zijn dat je in een situatie terechtkomt waarin er een nieuwe afhankelijkheid verschijnt die je niet hebt. Compilerfout zegt alleen dat self-type X does not conform to Y. Met 50 + componenten, kun je net zo goed raden, welke mislukt. (Of beginnen met het doen van een binaire zoekopdracht met het verwijderen van componenten tot de fout verdwijnt).

wanneer de cakegrootte en de hoeveelheid verder groeit, zou men de droge regel kunnen toepassen en deze in kleinere cakes kunnen splitsen, die later in elkaar zullen worden gezet. Dit onthult nieuwe diepten van de hel.

afhankelijkheden verwijderen

met normale DI wanneer u stopt met het gebruik van een object, kan uw IDE / compiler u vertellen dat het niet langer nodig is, dus u kunt het verwijderen. Met taart patroon, zult u niet worden geïnformeerd over dergelijke dingen.

als gevolg hiervan is opruimen veel moeilijker en is het heel goed mogelijk dat je op een bepaald moment met veel meer afhankelijkheden eindigt dan je echt nodig hebt.

compileertijd

dit alles komt overeen met de compileertijd. Het is misschien niet duidelijk op het eerste gezicht, maar je kunt eindigen met:

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

wat een cyclische afhankelijkheid is. (We weten allemaal dat ze slecht zijn, maar je zou niet eens merken, dat je net een gemaakt. Vooral als je alles rustig initialiseert).

dus deze 2 eigenschappen zullen heel vaak samen gecompileerd worden, en zink incrementele compiler zal daar niet bij helpen. Bovendien wijzigingen aan een van de componenten in een taart zal hercompilatie van dingen erboven in de afhankelijkheid grafiek veroorzaken.

testen

cyclische afhankelijkheden creëren een ander probleem. Je zou heel goed eindigen met iets als:

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!

elke component zal in zijn eigen bestand staan, dus u zult deze afhankelijkheid niet merken. Tests met mock zullen je daarbij niet helpen:

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

dus het zal allemaal prima en dandy zijn totdat je de code daadwerkelijk uitvoert. Je weet wel-de code is type-safe, wat betekent dat de waarde die u zult construeren type beperkingen zal volgen. Maar deze beperkingen betekenen niets omdat Nothing (gegooid uitzondering) is een perfect geldig subtype van uw vereiste type.

Initialisatievolgorde

het vorige probleem is gerelateerd aan dit probleem: wat is de volgorde van de componentinitialisatie?

met normale DI is het duidelijk-voordat je het ene in het andere plaatst moet je het aanmaken. Je kijkt naar de volgorde waarin je objecten creëert.

met een taart kun je het vergeten. Waardes binnen componenten zullen vaak lazy vals of objects zijn. dus eerst benaderd attribuut zal proberen zijn afhankelijkheden te installeren, die zal proberen zijn afhankelijkheden te installeren, enz.

zelfs als we gaan met vals is het een kwestie van volgorde waarin we cake – je weet wel, trait linearisatie gecomponeerd. Zolang elk onderdeel precies één keer wordt gebruikt-geen probleem. Maar als je je cake droogt, en sommige componenten herhalen, als je ze samenvoegt…

in een perfecte wereld zouden dingen als initialisatievolgorde er niet toe moeten doen, maar heel vaak wel, en we zouden graag willen weten wanneer bijvoorbeeld een verbinding met de DB of een abonnement op Kafka topic is gestart, en dingen loggen om potentiële fouten te debuggen.

Boilerplate en lock-in

zoals u waarschijnlijk hebt opgemerkt, genereert dit patroon veel boilerplate – in principe zou elke repository of service wrapper moeten hebben, alleen om de afhankelijkheden te annoteren.

zodra je die weg opgaat zul je merken dat het vrij moeilijk is om van het cake patroon af te komen. Elke implementatie wordt binnen een eigenschap geplaatst, en de afhankelijkheden ervan komen uit de scope, niet via parameters. Dus voor elke klasse, je zou moeten uitvoeren refactor eerste om te kunnen instantiate het zonder de wrapper.

maar dan zouden de afhankelijkheden ook opnieuw moeten worden geactiveerd, enzovoort. Taartpatroon is een lelijke verbintenis, die pijn doet en je tegenwerkt wanneer je probeert je er geleidelijk aan van terug te trekken.

letterlijk, elke andere vorm van DI in Scala, die ik weet (runtime reflectie a ‘la Guice, macro annotaties a’ la MacWire, implicits, combinaties van deze 3) kunt u zeggen stop! op elk moment, en geleidelijk over te schakelen naar een andere oplossing.

samenvatting

Cake patroon is een oplossing voor een DI probleem, dat op lange termijn meer kwaad dan goed doet. Van wat ik hoorde werd het gebruikt in Scala compiler, en ze betreuren het. Het was de standaard di mechanisme voor Play Framework, maar vanwege al deze problemen auteurs besloten om over te schakelen naar Guice – Ja, functionele programmeurs de voorkeur runtime reflectie te type-safe cake, dus dat vertelt veel.

dus, wat zijn de alternatieven?

  • gewoon oud handmatig argument doorgeven – geen noodzaak om uit te leggen dat
  • impliceert – heeft zijn voordelen totdat uw project wordt onderhouden door meer dan uzelf, en collega ‘ s beginnen te klagen over het niet weten, wat er gebeurt, trage Compilatie, het niet begrijpen van impliceert…
  • runtime reflectie – Guice is een volwassen framework DI doet op precies de manier die ik haat. Het werd gebruikt in veel projecten, Java programmeurs houden ervan – net zoals ze houden van de lente. Sommigen laten abstractie lekken door de injector in de klas te brengen. Sommigen van hen willen dat hun configuratie flexibel is – zo flexibel dat de klasse bedrading tijdens runtime kan mislukken, bleh!
  • MacWire-halverwege het handmatig doorgeven van argumenten en het gebruik van macro ‘ s voor alles – scant het huidige scope in compilatietijd en injecteert afhankelijkheden voor u. Als sommige ontbreken, mislukt de compilatie. Je schrijft ondertussen gewoon wire
  • Airframe – macro-based framework waar je recept voor DI met een DSL bouwt. Het heeft ondersteuning voor object lifecycle management
  • pulp-my shameless auto-promotion. Het gebruikte implicits en macro annotaties om providers voor klassen

IMHO te genereren elk van hen zou beter moeten uitwerken, dan cake patroon.

Geef een antwoord

Het e-mailadres wordt niet gepubliceerd.