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


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

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

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

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

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

trait EventProcessor[Event] {
  def addHandler[E <: Event: ClassTag](
      handler: E => Unit
  ): EventProcessor[Event]

  def process(event: Event)
}

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

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

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

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

sealed trait UserEvent
final case class UserCreated(name: String, email: String) extends UserEvent
sealed trait UserChanged                                  extends UserEvent
final case class NameChanged(name: String)                extends UserChanged
final case class EmailChanged(email: String)              extends UserChanged
case object UserDeleted                                   extends UserEvent

ما می توانیم برای هر کلاس رویداد خاص یک کنترلر ثبت کنیم. اما اگر بخواهیم یک handler برای ثبت نام کنیم چه می شود همه کلاس های رویداد؟ اولین تلاش من ثبت نام کنترل کننده برای UserEvent کلاس انتظار داشتم برای همه رویدادها به آن استناد شود.

val handler   = new EventHandlerImpl[UserEvent]
val processor = EventProcessor[UserEvent].addHandler[UserEvent](handler)

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

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

type Handler[Event] = (_ <: Event) => Unit

private 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)))
  }
}

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

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

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

val handler = new EventHandlerImpl[UserEvent]  
val processor = EventProcessor[UserEvent]  
  .addHandler[UserCreated](handler)  
  .addHandler[NameChanged](handler)  
  .addHandler[EmailChanged](handler)  
  .addHandler[UserDeleted.type](handler)

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

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

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

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

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

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

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

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
}

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

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

import c.universe._  
  
val symbol = weakTypeOf[Event].typeSymbol

def subclasses(symbol: Symbol): List[Symbol] = {  
  val children = symbol.asClass.knownDirectSubclasses.toList  
  symbol :: children.flatMap(subclasses(_))  
}  
  
val children = subclasses(symbol)

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

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

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

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

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

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

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

val exp = q"a + 2"

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

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

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

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

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

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

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

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)

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

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

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

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



منبع

Matthew Newman

Matthew Newman Matthew has over 15 years of experience in database management and software development, with a strong focus on full-stack web applications. He specializes in Django and Vue.js with expertise deploying to both server and serverless environments on AWS. He also works with relational databases and large datasets
[ Back To Top ]