از Scala Macros و Quasiquotes برای کاهش Boilerplate استفاده کنید
انتشار: فروردین 11، 1400
بروزرسانی: 15 آذر 1404

از Scala Macros و Quasiquotes برای کاهش Boilerplate استفاده کنید

در مورد ما، ما می خواهیم فرزندان کلاس مهر و موم شده خود را بازیابی کنیم:

اعلانات مربوطه Scala به این صورت است:

بیایید AST را ایجاد کنیم و آن را به عنوان نتیجه تابع ماکرو برگردانیم:

در بالا، ما یک کنترل کننده برای UserEvent کلاس، اما هر زمان که یک رویداد مشتق شده مانند UserCreated منتشر شد، پردازنده کلاس خود را در رجیستری پیدا نمی کند.

بنابراین کد دیگ بخار شروع می شود

type Handler[Event] = (_ <: Event) => Unitprivate case class EventProcessorImpl[Event]( handlers: Map[Class[_ <: Event], List[Handler[Event]]] = Map[Class[_ <: Event], List[Handler[Event]]]()) extends EventProcessor[Event] { override def addHandler[E <: Event: ClassTag]( handler: E => Unit ): EventProcessor[Event] = { val eventClass = implicitly[ClassTag[E]].runtimeClass.asInstanceOf[Class[_ <: Event]] val eventHandlers = handler .asInstanceOf[Handler[Event]] :: handlers.getOrElse(eventClass, List()) copy(handlers + (eventClass -> eventHandlers)) } override def process(event: Event): Unit = { handlers .get(event.getClass) .foreach(_.foreach(_.asInstanceOf[Event => Unit].apply(event))) }}val handler = new EventHandlerImpl[UserEvent] val processor = EventProcessor[UserEvent] .addHandler[UserCreated](handler) .addHandler[NameChanged](handler) .addHandler[EmailChanged](handler) .addHandler[UserDeleted.type](handler)

اگرچه Scala یک نحو مختصر دارد که معمولاً به جلوگیری از boilerplate کمک می کند، توسعه دهندگان همچنان می توانند موقعیت هایی را پیدا کنند که کد تکراری می شود و به راحتی نمی توان آن را برای استفاده مجدد مجدداً تغییر داد. ماکروهای اسکالا را می توان با شبه نقل قول ها برای غلبه بر چنین مشکلاتی استفاده کرد و کد اسکالا را تمیز و قابل نگهداری نگه داشت.

ما می توانیم با شبه نقل قول هایی با نحو مختصر و خواندنی تر به همین نتیجه برسیم:

اکنون می توانیم ماکرو موجود در آن را فراخوانی کنیم UserEvent کلاس و کدی را برای ثبت کنترل کننده برای همه زیر کلاس ها ایجاد می کند:

sealed trait UserEventfinal case class UserCreated(name: String, email: String) extends UserEventsealed trait UserChanged    extends UserEventfinal case class NameChanged(name: String)      extends UserChangedfinal case class EmailChanged(email: String)    extends UserChangedcase object UserDeleted     extends UserEvent

حالا کد کار می کند! ولی تکراریه

اما این تنها تا زمانی صادق است که توسعه دهندگان یاد بگیرند که چگونه از ماکروها و شبه نقل قول ها برای تولید کدهای تکراری در زمان کامپایل استفاده کنند.

Use Case: ثبت همان Handler برای همه زیرشاخه های یک کلاس والد

در طول توسعه یک سیستم میکروسرویس، من می خواستم یک کنترلر واحد برای همه رویدادهای مشتق شده از یک کلاس خاص ثبت کنم. برای جلوگیری از حواس پرتی ما با ویژگی های چارچوبی که استفاده می کردم، در اینجا یک تعریف ساده از API آن برای ثبت کنترل کننده های رویداد آورده شده است:

زبان اسکالا به توسعه دهندگان این فرصت را می دهد که کدهای شی گرا و عملکردی را در یک نحو واضح و مختصر بنویسند (مثلاً در مقایسه با جاوا). کلاس های موردی، توابع با مرتبه بالاتر و استنتاج نوع برخی از ویژگی هایی هستند که توسعه دهندگان Scala می توانند از آنها برای نوشتن کدهایی استفاده کنند که نگهداری راحت تر و خطای کمتری داشته باشد.

متوجه شدم که در طول آزمایش ها هرگز از کنترل کننده فراخوانی نشده است. من به کد از در حد متوسط، چارچوبی که من استفاده می کردم.

val handler = new EventHandlerImpl[UserEvent]  val processor = EventProcessorMacro.addHandlers(EventProcessor[UserEvent],handler)

برای ساختن AST خود، می توانیم کلاس های AST را دستکاری کنیم یا از Scala استفاده کنیم شبه نقل قول ها. استفاده از کلاس های AST می تواند کدی تولید کند که خواندن و نگهداری آن دشوار است. در مقابل، شبه نقل قول ها به طور چشمگیری پیچیدگی کد را کاهش می دهند و به ما اجازه می دهند از نحوی استفاده کنیم که بسیار شبیه به کد تولید شده است.

import c.universe._    val symbol = weakTypeOf[Event].typeSymboldef subclasses(symbol: Symbol): List[Symbol] = {    val children = symbol.asClass.knownDirectSubclasses.toList    symbol :: children.flatMap(subclasses(_))  }    val children = subclasses(symbol)
ما می توانیم برای هر کلاس رویداد خاص یک کنترلر ثبت کنیم. اما اگر بخواهیم یک handler برای ثبت نام کنیم چه می شود همه کلاس های رویداد؟ اولین تلاش من ثبت نام کنترل کننده برای UserEvent کلاس انتظار داشتم برای همه رویدادها به آن استناد شود.

راه حل این است که یک هندلر را برای هر کلاس رویداد بتن ثبت کنید. ما می توانیم این کار را به این صورت انجام دهیم:

سلسله مراتب رویدادهای Scala نزولی از UserEvent.  سه نسل مستقیم وجود دارد: UserCreated (دارای نام و ایمیل که هر دو رشته هستند)، UserChanged و UserDeleted.  علاوه بر این، UserChanged دو فرزند دارد: NameChanged (با نامی که یک رشته است) و EmailChanged (دارای یک ایمیل که یک رشته است).
سلسله مراتب کلاس رویداد Scala.

متوجه شدم که پیاده سازی پردازشگر رویداد، کنترل کننده ها را در یک نقشه با کلاس ثبت شده به عنوان کلید ذخیره می کند. هنگامی که یک رویداد منتشر می شود، به دنبال کلاس خود در آن نقشه می گردد تا کنترل کننده را وادار به فراخوانی کند. پردازشگر رویداد در این خطوط پیاده سازی می شود:

کد از پروژه کامل به درستی کامپایل می کند و موارد آزمایشی نشان می دهد که کنترل کننده واقعاً برای هر زیر کلاس ثبت شده است UserEvent. اکنون می توانیم به ظرفیت کد خود برای مدیریت انواع رویدادهای جدید اطمینان بیشتری داشته باشیم.

کد تکراری؟ ماکروهای اسکالا را برای نوشتن آن دریافت کنید

برای نشان دادن سود سادگی، بیایید عبارت ساده را در نظر بگیریم a + 2. ایجاد این با کلاس های AST به شکل زیر است:

که این کد را تولید می کند:

اکنون که فهرستی از کلاس های رویداد برای ثبت داریم، می توانیم AST را برای کدی که ماکرو Scala ایجاد می کند بسازیم.

ایجاد کد Scala: AST ها یا Quasiquotes؟

بیایید به یک نگاه کنیم macro اعلانی که ثبت کنترل کننده رویداد را برای همه زیر کلاس های شناخته شده یک کلاس مشخص ایجاد می کند:

حفظ آن نیز دشوار است، زیرا هر بار که یک نوع رویداد جدید را معرفی می کنیم، باید آن را تغییر دهیم. همچنین ممکن است مکان های دیگری در پایگاه کد خود داشته باشیم که مجبور شویم همه انواع بتن را فهرست کنیم. ما همچنین باید مطمئن شویم که آن مکان ها را اصلاح کنیم.

متأسفانه، کد Scala در مقابل boilerplate مصون نیست و توسعه دهندگان ممکن است برای یافتن راهی برای بازسازی و استفاده مجدد از چنین کدهایی تلاش کنند. به عنوان مثال، برخی از کتابخانه ها توسعه دهندگان را مجبور می کنند تا با فراخوانی یک API برای هر زیر کلاس a، خود را تکرار کنند کلاس مهر و موم شده.

برای اینکه کلان خود را ساده نگه داریم، از شبه نقل قول استفاده می کنیم.

این ناامید کننده است، زیرا UserEvent یک کلاس مهر و موم شده است، به این معنی که تمام زیر کلاس های مستقیم آن در زمان کامپایل شناخته می شوند. اگر بتوانیم از این اطلاعات برای جلوگیری از دیگ بخار استفاده کنیم، چه؟

ماکروها برای نجات

val exp = Apply(Select(Ident(TermName("a")), TermName("$plus")), List(Literal(Constant(2))))

به طور معمول، توابع Scala بر اساس پارامترهایی که در زمان اجرا به آنها ارسال می کنیم، مقداری را برمی گرداند. می توانید فکر کنید ماکروهای اسکالا به عنوان توابع ویژه ای که مقداری کد را در گردآوری زمان برای جایگزینی فراخوان های خود با.

val calls = children.foldLeft(q"$processor")((current, ref) =>  q"$current.addHandler[$ref]($handler)")c.Expr[EventProcessor[Event]](calls)

توجه داشته باشید که برای هر پارامتر (شامل پارامتر نوع و نوع برگشتی)، روش پیاده سازی یک عبارت AST مربوطه را به عنوان پارامتر دارد. مثلا، c.Expr[EventProcessor[Event]] مسابقات EventProcessor[Event]. پارامتر c: Context زمینه تلفیقی را می پیچد. ما می توانیم از آن برای دریافت تمام اطلاعات موجود در زمان کامپایل استفاده کنیم.

به تماس بازگشتی توجه کنید subclasses روشی برای اطمینان از اینکه زیر کلاس های غیرمستقیم نیز پردازش می شوند.

کد بالا با عبارت پردازنده دریافت شده به عنوان یک پارامتر و برای هر کدام شروع می شود Event زیر کلاس، یک فراخوانی به آن ایجاد می کند addHandler روش با زیر کلاس و تابع handler به عنوان پارامتر.

با نگاهی به امضای بالا، یک توسعه دهنده ممکن است انتظار داشته باشد که یک کنترل کننده ثبت شده برای یک نوع مشخص برای رویدادهای زیرگروه های آن فراخوانی شود. برای مثال، بیایید سلسله مراتب طبقاتی زیر را در نظر بگیریم User چرخه حیات موجودیت:

val exp = q"a + 2"
trait EventProcessor[Event] { def addHandler[E <: Event: ClassTag]( handler: E => Unit ): EventProcessor[Event] def process(event: Event)}

همچنین کتابخانه های محبوبی مانند مک وایر، که از ماکروهای Scala برای کمک به توسعه دهندگان برای تولید کد استفاده می کند. من قویاً هر توسعه دهنده Scala را تشویق می کنم تا در مورد این ویژگی زبان بیشتر بیاموزند یک دارایی ارزشمند در مجموعه ابزار شما



منبع

داشتن یک پردازنده رویداد برای هر Event نوع، ما می توانیم کنترل کننده ها را برای زیر کلاس های ثبت کنیم Event با addHandler روش.

com.example.event.processor.EventProcessor.apply[com.example.event.handler.UserEvent]().addHandler[UserEvent](handler).addHandler[UserCreated](handler).addHandler[UserChanged](handler).addHandler[NameChanged](handler).addHandler[EmailChanged](handler).addHandler[UserDeleted](handler)
val handler   = new EventHandlerImpl[UserEvent]val processor = EventProcessor[UserEvent].addHandler[UserEvent](handler)

در حالی که macro ممکن است به نظر برسد که اینترفیس مقادیری را به عنوان پارامتر در نظر می گیرد، پیاده سازی آن در واقع آن را جذب می کند درخت نحو انتزاعی (AST) - نمایش داخلی ساختار کد منبع که کامپایلر از آن پارامترها استفاده می کند. سپس از AST برای تولید یک AST جدید استفاده می کند. در نهایت، AST جدید جایگزین فراخوانی ماکرو در زمان کامپایل می شود.

def addHandlers[Event](      processor: EventProcessor[Event],      handler: Event => Unit  ): EventProcessor[Event] = macro setEventHandlers_impl[Event]      def setEventHandlers_impl[Event: c.WeakTypeTag](c: Context)(      processor: c.Expr[EventProcessor[Event]],      handler: c.Expr[Event => Unit]  ): c.Expr[EventProcessor[Event]] = {  // implementation here}