وقتی کاربر می‌خواهد «شرط» بنویسد، نه «برنامه»!

پیش مقدمه

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

وظیفه تیم متناسب‌سازی اینه که به کاربرها اجازه بده تحت یک چهار چوب امن سرویس‌های نرم‌افزار ERP ما رو با منطق کسب‌و‌کار خودشون تطبیق بدن که شامل چیزهایی مثل Rule Engine، گردش کار، فرم‌ساز و … می‌شه. به همین دلیله که اغلب فیچرهای ماژول تیم متناسب‌سازی حالت داینامیک دارن. یکی از نیازمندی‌های مهم در یه سیستم با این ویژگی‌ها، نوشتن عبارت هست که به کاربر اجازه می‌ده شروط خود و قوانینی که مقادیر از روش ساخته می‌شن رو در قالب اون عبارت معرفی کنه؛ این موضوع چالش های خاص خودش رو داره که در ادامه قراره درباره اون‌ها صحبت کنیم.

Google CEL
Google CEL

۱. چرا اصلاً به «ارزیابی عبارت» نیاز داریم؟

هر ERP سازمانی در عمل دو لایه داره: هسته‌ای که تیم فنی توسعه‌ش می‌ده و لایه‌ای از قوانین و رفتارهایی که هر سازمان برای خودش تعریف می‌کنه. تیم متناسب‌سازی دقیقاً روی همین لایه دوم کار می‌کنه.

وقتی یک کاربر در طراح گردش کار می‌نویسه «فقط اگر مبلغ سفارش بیش از حد مجاز باشد، به مدیر ارجاع بده»، یا در فرم‌ساز شرطی می‌گذاره که «فیلد تخفیف فقط وقتی نوع مشتری خاص است نمایش داده شود»، در واقع داره یه منطق تعریف می‌کنه، نه داده. سیستم باید این منطق رو در زمان طراحی بپذیره، در زمان اجرا ارزیابی کنه و نتیجه‌اش رو به مرحله بعدی (مرحله گردش، نمایش فیلد، مقدار پیش‌فرض، …) وارد کنه.

این الگو در چند جای محصول تکرار می‌شه:

  • شرط اجرا و شرط عبور در گردش کار

  • محاسبه مقدار پارامترهای تسک و قوانین تجاری

  • اعتبارسنجی و نمایش شرطی در فرم‌ها و فیلدهای اضافه


۲. بررسی ابزارها و راه‌حل‌ها

اگه برای هر درخواست مشتری یا هر شرط جدید، مجبور بشیم دوباره کار رو به تیم توسعه بسپریم، فرآیندمون نمی‌تونه مقیاس‌پذیر باشه؛ و اگر برای همین کار یک زبان اسکریپتی کامل (مثل JavaScript یا Python) رو در اختیار کاربر بگذاریم، عملاً داریم دروازه‌ای به سمت کد دلخواه، حلقه بی‌نهایت و رفتار غیرقابل پیش‌بینی باز می‌کنیم.

پس نیاز ما دقیقاً اینه: یه Engine که عبارت کاربر رو به صورتی امن، سریع و با نوع مشخص ارزیابی کنه.

اینطوری کاربر به جای اینکه یه برنامه کامل بنویسه، فقط به سوالی مثل اینکه «آیا این شرط برقرار است؟» یا «مقدار این فیلد چقدر است؟» جواب می‌ده.

برای این موضوع روش‌ها و ابزارهای مختلفی وجود داره که به صورت خلاصه بعضی از اون‌ها رو بررسی‌ می‌کنیم:

۱. زبان‌های Embed Scripting

یکی از این روش‌ها Embedded Script‌ها هستن. لایبرری‌هایی که به کاربر اجازه می‌دن اسکریپت‌های خودشون رو با Syntax زبان مورد نظر (معمولاً Python/ JS /Lua) بنویسن و اجراشون می‌کنن.

مسئله اصلی در این ابزارها اینه که اون‌ها Turing Complete هستن و زمان اجراشون غیر قابل پیش‌بینیه؛ از طرفی تأمین امنیت در اجرا برای این زبان‌ها کار پیچیده‌تریه.

۲. استفاده از JSONLogic / JMESPath / JSONPath

 این لایبرری‌ها Rule Engine‌هایی هستن که برپایه JSON بنا شدن و معمولاً نسبت به Scripting‌ها امکانات کمتری دارن و بیشتر برای فیلتر کردن و Query گرفتن از داده‌ها مناسب هستن تا انجام محاسبات و مهم‌تر از همه این که Typed نیستن.

۳. استفاده از  SpEL (Spring) / MVEL / JEXL (دنیای JVM)

این ابزارها Expression Language‌های بسیار قوی هستن که برای Rule و Template‌ها به صورت گسترده استفاده می‌شن؛ اما مشکل این ابزارها اینه که به شدت به به اکوسیستم‌های زبان Java گره خوردن و بر طبق قواعد کوبرنتیز و Claud Native استاندارد نشدن. همچنین پیاده‌سازی‌ اون‌ها در Golang ممکنه نتیجه خوبی نداشته باشه.

۴. پیاده‌سازی DSL داخلی

یک راه دیگه هم اینه که خودمون یک Expression Language بنویسیم. این اقدام در نگاه اول و با شروط کم تعداد ممکنه کار ساده‌ای به نظر برسه، ولی به مرور نیاز به پشتیبانی تایپ‌های مختلف و اپراتورهای مختلف و… پیدا می‌کنیم و بالاخره متوجه می‌شیم که داریم زمان زیادی رو برای نگهداری از زبان نیمه‌کاره غیراستانداردمون مصرف می‌کنیم.

با این اوصاف انتخاب ابزار مناسب، موضوع به شدت حساسیه چون بعداً تغییر دادنش عملاً غیر قابل امکانه؛ چون تعداد زیادی از کاربران، Rule‌های خودشون رو نوشتن و عوض کردن Syntax و فرمت تعریف اون‌ها مثل اینه که بخوای یه پروژه به زبان Python رو تبدیل به یه پروژه Golang بکنی.

پس باید خیلی دقت می‌کردیم و در نهایت به Google Common Expression Language رسیدیم که به ما اجازه این رو می‌داد که عبارات کاربر رو در فضای امن و قابل پیش‌بینی ارزیابی کنیم و بر اساس‌ش تصمیم بگیریم.

۵. CEL یا Common Expression Language

CEL یک زبان متن‌باز غیر تورینگیه که برای سادگی، سرعت و امنیت طراحی شده و سینتکسی شبیه به C, Go و TypeScript داره و توسط کمپانی گوگل توسعه داده شده.

CEL در ابتدا برای حل یک مسئله مشخص طراحی شده بود:

چطور می‌توان منطق شرطی و Policy رو داخل APIها، سرویس‌ها و لایه‌های زیرساختی Embed کرد، بدون اینکه امنیت و کارایی سیستم به خطر بیفتد؟

در یک اکوسیستم Cloud Native نیازهای تکرارشونده‌ای وجود داره:

  • اعتبارسنجی داده‌ها روی ساختارهای protobuf

  • سیاست دسترسی (Authorization)  بر اساس Attributeهای درخواست

  • فیلتر و مسیریابی در Proxyها و Gatewayها

  • قوانین پیکربندی که کاربر یا اپراتور می‌نویسه و سیستم هزاران بار در ثانیه اجرا می‌کنه.

راه‌حل‌های سنتی هر کدوم‌شون مشکلاتی مثل عدم امنیت، پیچیدگی، غیر یکپارچگی و … داشتن؛ پس CEL برای پاسخ به این نیازمندی‌ها شکل گرفت تا امروز یک زبان Typed و Extendable و محدود که امنیت رو تضمین می‌کنه، نیازمندی‌های عمومی رو رفع می‌کنه و امکان شخصی‌سازی داره در اختیار ما قرار بگیره.


۳. CEL دقیقاً چیست؟ (و چه چیزی نیست!)

جمله‌ای که خیلی‌ها رو گیج می‌کنه اینه:

CEL یک زبان برنامه‌نویسی (General Purpose Programming Language)  نیست بلکه یکExpression Language است.

به عبارت دیگه در CEL فقط Expression وجود داره، نه Statement؛ اما این موضوع یعنی چی؟!

در CEL شما Statement ندارید (if به‌صورت بلوک، for، return، assign متغیر) و هر چیزی که می‌نویسین یک Expression هست که در نهایت یک مقدار  رو بر می‌گردونه:

has(account.user_id) || account.amount > 1000
size(emails) > 0 && emails[0].endsWith("@company.com")

عبارات کوچک‌تر رو می‌‌شه ترکیب کرد تا منطق‌مون پیچیده‌تر بشه اما همچنان در قالب «محاسبه مقدار»، و نه «اجرای برنامه».

قبل از اینکه سراغ تجربه خودمون بریم، بد نیست نگاهی به ویژگی‌های اصلی CEL بندازیم؛ ویژگی‌هایی که باعث شدن این ابزار برای مسئله‌ای که داشتیم انتخاب مناسبی باشه.

🔺 تورینگ کامل نبودن (non-Turing Complete)

CEL عمداً Turing-Complete نیست و به همین دلیل حلقه نامحدود و بازگشت بی‌پایان نداره. این یعنی:

  • هر عبارت حتماً تمام می‌شه.

  • زمان اجرا قابل پیش‌بینیه (معمولاً نانوثانیه تا میکروثانیه)

  • ریسک این که «یک عبارت بد کل سرویس رو قفل کند» بسیار کمتره.

🔺 بدون اثرات جانبی (Side-Effect Free)

عبارات CEL داده‌ها رو تغییر نمی‌دن، شبکه رو صدا نمی‌زنن، فایل نمی‌نویسن. فقط ورودی می‌گیرن و خروجی رو بر می‌گردونن که برای Policy و Validation، این موضوع یه ویژگی حیاتیه.

🔺 نحوه اجرای عبارت در CEL

مدل کار CEL سه مرحله داره:

  • Parse : تبدیل متن به AST

  • Check (type-check): بررسی نوع‌ها و سازگاری عبارت با محیط (environment)

  • Eval : اجرا روی داده واقعی

الگوی توصیه‌شده: compile در زمان config/design، eval مکرر در runtime. یعنی عبارت رو یک‌بار parse و check کنید، AST رو ذخیره کنین، و در هر درخواست فقط eval بزنین. CEL برای همین الگوی «compile-once, evaluate-many» بهینه شده.

🔺 CEL کجا استفاده شده؟

 امروز دیگه CEL فقط پروژه داخلی گوگل نیست؛ بلکه در اکوسیستم Cloud Native و امنیت جا افتاده، برای مثال:


۴. مزایای استفاده از CEL

🔺 امنیت Embed کردن عبارات کاربر

اگه به جای CEL از JS یا یک زبان تورینگی استفاده کنیم، عملاً به کاربر اجازه می‌دیم که هر کدی رو اجرا کنه ولی خب می‌تونیم اون رو محدود کنیم؛ اما CEL رویکرد جالب تری داره که با سیاست‌های معماری نرم‌افزارهای ما همسو است.

CEL طراحی، فرمت و قابلیت‌های زبان رو به طوری محدود می‌کنه که کاربر نمی‌تونه در قالب یک Feature Set زبان کار غیرقابل کنترلی انجام بده.

🔺 Type Safety (با Gradual Typing)

CEL از Gradual Typing پشتیبانی می‌کنه و تا جایی که ممکن باشه، نوع‌ها در Compile-Time بررسی می‌شن و در بخش‌های Dynamic (مثل google.protobuf.Struct) این بررسی به Runtime موکول می‌شه. این تعادل بین انعطاف API و اطمینان Type مناسب دنیای واقعیه.

🔺 قابلیت گسترش (Extensibility)

Host می‌تونه:

  • توابع سفارشی اضافه کنه ‍ (()decimal, ()persian_date, ()today)

  • انواع سفارشی تعریف کنه (مثلاً Decimal مالی با Overloadهای + و >)

  • Overload روی عملگرها ثبت کنه.

این همان نقطه‌ای است که در تجربه ما بیشترین کار روی آن انجام شد . CEL خام برای ERP کافی نبود، اما پایه خوبی برای Extension بود. یکی از دلایل اصلی انتخاب آن همین موضوع بود

🔺 خوانایی نسبتا بالا و آشنا برای کاربر

زبان CEL با داشتن سینتکسی شبیه به زبان‌های معروف مثل C/Java/Go باعث می‌شه تا کاربر راحت‌تر باهاش ارتباط بگیره و سریع‌تر یاد بگیره.

request.method == "POST" && request.path.startsWith("/api/")
user.role in ["admin", "editor"]
amount >= decimal("1000.00")

۵. تجربه و چالش های ما با CEL در Go

استفاده از CEL در محصول ما فقط ساخت یک env و یک Eval نیست. در عمل با چند مسئله جدا روبرو شدیم. هر کدوم نیازمندی و چالش خودش رو داشت.در ادامه مسئله‌هایی که داشتیم و راه حلی هایی که براشون پیاده کردیم رو باهم می‌بینیم.

🔺 تعریف توابع بیزینس خاص

ما یه سری توابع و منطق های خاص کاربری خودمون داشتیم و نیاز داشتیم اون‌ها رو به CEL اضافه کنیم مثل محاسبات اعشاری مبلغی و تاریخ های شمسی و میلادی و…

CEL از قبل برای اعشار Double و برای زمان هم Timestamp داره. ولی Double برای محاسبات مالی دقت لازم رو نمی‌ده، و Timestamp هم اون مدل «تاریخ ERP» ما رو پوشش نمی‌ده (با تقویم شمسی و توابع کسب‌وکار). نتیجه‌اش اینه که بدون Extension، عباراتی که به این نوع محاسبات نیاز دارن عملاً قابل ارزیابی نیستن.

CEL به ما اجازه میده تابع‌ها با پیاده سازی گولنگی رو بهش اضافه کنیم برای مثال اینجا یه تابع is_empty اضافه کردیم که null بودن یک مقدار رو بررسی می‌کنه.

cel.Function("is_empty",
              cel.Overload("is_empty",
              []*cel.Type{celgotypes.AnyType},  // نوع ورودی
              cel.BoolType,                  // نوع خروجی

              cel.UnaryBinding(func(arg ref.Val) ref.Val {

                  if arg.Value() == structpb.NullValue_NULL_VALUE {
                      return celgotypes.Bool(true)
                  }
                  return celgotypes.Bool(false)
              }),
              ),
)

همچنین برای هر تابع می‌شه چندتا Overload مختلف تعریف کرد. مثلاً ما لازم داشتیم کاربر بتونه یه مقدار Decimal رو از روی int یا Double یا حتی String بسازه، برای همین این مدل تعریفش کردیم:

cel.Function("decimal",
              cel.Overload("decimal_string", []*cel.Type{StringType}, DecimalType, ...),
              cel.Overload("decimal_double", []*cel.Type{DoubleType}, DecimalType, ...),
              cel.Overload("decimal_int",              []*cel.Type{IntType},      DecimalType, ...),
)

و در  عبارت این شکلی استفاده کرد:

decimal("1.054")
decimal(4)
decimal(1.5)

که همه این تایپ ها در Compile Time بررسی می‌شن  و جلوی خطای کاربر رو می‌گیرن.

🔺 ساخت Environment مناسب برای اجرای هر عبارت

در هر نقطه ای  که کاربر یک عبارت می نویسه باید به مجموعه مشخصی از فیلد ها رو ببینه و دسترسی داشته باشه که شامل فیلد های یک موجودیت یا خروجی یک عملیات و متغیر های تعریف شده و … میشه.

 چالشی که داشتیم این بود که در هر کاربری نیاز داشتیم که Environment مناسب برای اجرای عبارت رو بسازیم و موجودیت های سیستم رو با در نظر گرفتن نوع داده های آن به CEL معرفی کنیم. علاوه بر اون ما چندین و چند ماژول مختلف داریم که نمیخواستیم همه این کد هارو مستقیما به CEL متصل کنیم و همچنین نمیخواستیم برای هر کاربرد مختلف دستی cel.Variable بنویسیم.

برای حل این مساله یک فرمت متادیتای Declarative تعریف کردیم به اسم  ComplexTypeParamDefکه توصیف یک تایپ و فیلد های اونه و هر فیلد تایپ Primitive داره که این تایپ پل بین دیتای گولنگی و دیتای اجرای CEL هست:

🔺 پل بین داده های CEL و Go

همونطور که تو مثال اول گفتیم ما با توجه به اینکه نیاز داشتیم در عبارت محاسبات مبلغی انجام بدیم یا اینکه نیاز داشتیم با تاریخ های شمسی استفاده کنیم برای این کار داخل کد گولنگمون برای تاریخ پکیج خودمون رو پیاده کردیم و برای Decimal از این پکیج استفاده کردیم.

اما مسئله ای که وجود داشت این بود که نیاز داشتیم این تایپ‌ها و رفتار آنها رو داخل عبارات نیز داشته باشیم خوشبختانه CEL این امکان رو به ما می‌داد که بتونیم تایپ خودمون رو تعریف کنیم و همچنین Mapping بین تایپ های گولنگی و CEL داشته باشیم.

 با CEL می‌شه تایپ تعریف کرد و به آن ویژگی و رفتار داد:

var DecimalType = cel.ObjectType("decimal",
              traits.AdderType,
              traits.ComparerType,
              traits.DividerType,
              traits.MultiplierType,
              traits.SubtractorType,
)
type Decimal struct { value decimal.Decimal }

  func (d Decimal) Add(other ref.Val) ref.Val { ... }
  func (d Decimal) Compare(other ref.Val) ref.Val { ... }

و همچنین برای وصل کردن یک تایپ گولنگ به تایپ متناظر در CEL یک Interface به نام Type Provider وجود داره که می‌شه اون رو پیاده کرد:

func (p *customTypeProvider) NativeToValue(value interface{}) ref.Val {
              switch v := value.(type) {
                 case decimal.Decimal:
                    return Decimal{v}
                 case date.Date:
                   return Date{v}
              }

              return p.Registry.NativeToValue(value)
}

 

🔺 اعتبار سنجی Nested Mapها

ما ساختار موجودیت‌هامون رو داخل CEL به صورت “Map” معرفی می‌کنیم و مشکلی که داشتیم این بود که این ساختارها موقع کامپایل Type Safety در اختیارمون نمی‌ذاشتن. برای مثال، فرض کنید آبجکت “Order” دو فیلد “Time” و “Amount” داشته باشه. اگه داخل عبارت بنویسیم:

“order.time > 10”

تا زمان اجرا، CEL متوجه نمی‌شه که “Time” از نوع زمانه و اصلاً نباید با یک عدد مقایسه بشه.

برای حل این مشکل، ساختارهای تو در تو رو با استفاده از Dot Notation به متغیرهای Flat تبدیل کردیم. یعنی به جای اینکه موقع کامپایل فقط یک متغیر به اسم “Order” داشته باشیم، دو متغیر order.timeو order.amount تعریف می‌شن که نوع هر کدوم از قبل مشخصه؛ order.time از نوع زمان و order.amount از نوع Decimal هست. به این ترتیب، CEL می‌تونه ناسازگاری‌های نوع داده رو همون موقع کامپایل تشخیص بده.

نمونه کد مربوط به Flat کردن ساختار داده:

func flatten(prefix string, nested map[string]interface{},
flat map[string]interface{}) {
              for key, value := range nested {
                  flatKey := key
                  if prefix != "" {
                      flatKey = prefix + "." + key
                  }

                  switch typedValue := value.(type) {
                  case map[string]interface{}:
                      flatten(flatKey, typedValue, flat)
                  default:
                      flat[flatKey] = value

               }
     }

🔺 استخراج متغیر ها از AST

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

برای حل این مسئله، موقع ذخیره شدن عبارت، با استفاده از cel.Parse و بدون تعریف  env، درخت اجرای AST عبارت رو تولید می‌کنیم. بعد به صورت بازگشتی داخل این درخت حرکت می‌کنیم و تمام متغیرهایی که در عبارت استفاده شده‌اند رو استخراج می‌کنیم. به این ترتیب، موقع اجرا فقط همون متغیرهای موردنیاز از دیتابیس لود می‌شن.

env, _ := cel.NewEnv()
ast, _ := env.Parse(expression)
expr, _ := cel.AstToParsedExpr(ast)
visitExpr(expr.GetExpr(), used)

visitExprبر اساس نوع node داخل AST پیمایش می‌کنه:

  • IdentExpr: نام متغیر جمع می‌شه.

SelectExpr: فقط ریشه جمع می‌شه مثلا در عبارتorder.amount فقط Order به عنوان متغیر استفاده شده جمع می‌شه.

  • CallExpr: وقتی که یک متغیری در Arg‌های یک تابع استفاده می‌شه.

در نهایت از پیمایش کل AST همه متغیر‌های استفاده شده در عبارت استخراج می‌شن و در زمان اجرا فقط اون‌ها از دیتابیس خونده می‌شن.


۶. درس هایی که گرفتیم

پیاده‌سازی Expression Engine در یک ERP، بیشتر از آنکه یک تصمیم فنی ساده باشه، یک تصمیم معماری بلندمدت هست. ما در این مسیر چند نکته مهم یاد گرفتیم که احتمالاً برای تیم‌هایی با مسئله مشابه مفید باشه.

▪️انتخاب Syntax در واقع انتخاب قرارداد بلندمدت با مشتری است.

وقتی کاربرها شروع می‌کنن Rule و شرط و فرمول‌های خودشون رو روی یک Syntax مشخص بنویسن، اون Syntax دیگه صرفاً یه جزئیات داخلی نیست؛ تبدیل می‌شه به بخشی از قرارداد محصول با مشتری.

به همین دلیل انتخاب Expression Language باید خیلی محافظه‌کارانه انجام بشه. اگر بعداً به این نتیجه برسیم که ابزار انتخاب‌شده مناسب نبوده، مهاجرت فقط تعویض یک Library نیست؛ در عمل باید هزاران Expression موجود رو هم با خودمان جابه‌جا کنیم. ما خیلی زود فهمیدیم که این تصمیم از جنس “فعلاً این رو بزنیم جلو” نیست.

▪️امن بودن رو نباید با Sandbox کردنِ یک زبان کامل اشتباه گرفت.

در نگاه اول ممکنه ساده باشه که بگیم JavaScript یا Python رو محدود می‌کنیم و فقط چند قابلیت خاص می‌دیم. اما در عمل هرچه زبان عمومی‌تر باشه، کنترل کردن آن سخت‌تر می‌شود.

چیزی که در CEL برای ما ارزشمند بود این بود که از اول برای همین مسئله طراحی شده: Expression محدود، بدون side effect، بدون loop نامحدود و با رفتار قابل پیش‌بینی. این تجربه به ما یاد داد که خیلی وقت‌ها بهترین راه امنیت، محدود کردن ابزار پیچیده نیست؛ بلکه از اول ابزار محدود و مناسب انتخاب کردنه.

▪️خود CEL کافی نیست؛ کاربری واقعی در Extensionها ساخته می‌شه.

CEL به‌ تنهایی پایه خیلی خوبی می‌ده، اما برای ERP واقعی معمولاً کافی نیست. در عمل نیازهای واقعی کسب‌وکار سریعاً سراغت میان:

  • Decimal دقیق برای محاسبات مالی

  • تاریخ و تقویم متناسب با دامنه مسئله

  • توابع بیزینسی خاص

برای ما بخش اصلی کار نه صرفاً استفاده از CEL، بلکه ساختن لایه extension استاندارد روی آن بود. اگه کسی بخواد از CEL در محصول مشابه استفاده کنه، باید از اول این رو بپذیره که بخش مهمی از طراحی، مربوط به Typeها و Functionهای سفارشیه.

▪️Type Checking فقط یک قابلیت فنی نیست؛ بخشی از تجربه کاربریه.

در ابتدا ممکنه Type Safety صرفاً یک دغدغه مهندسی به نظر برسه، اما خیلی زود مشخص می‌شود که این موضوع مستقیماً روی تجربه کاربر اثر می‌گذاره.

وقتی کاربر همان موقع طراحی Rule بفهمه که:

  • این فیلد Decimal است.

  • این یکی Date است.

  • این تابع این نوع ورودی رو قبول نمی‌کنه.

  • این مقایسه معتبر نیست.

می‌تونه خیلی زودتر خطاهای خودش رو پیدا کنه.در واقع داریم خطا رو از Runtime به Design Time منتقل می‌کنیم. این یعنی هم اشکال‌زدایی ساده‌تر می‌شه، هم اعتماد کاربر به سیستم بیشتر. یکی از مهم‌ترین درس‌های ما همین بود که Expression Engine خوب فقط Evaluator نیست، Validator هم هست.

▪️فقط Evaluation مهم نیست؛ Lifecycle عبارت هم مهمه.

وقتی از Expression در محصول استفاده می‌کنیم، مسئله فقط این نیست که “یک string رو Eval کنیم”. در عمل برای هر عبارت یک Lifecycle کامل داریم:

  • تعریف توسط کاربر

  • Parse

  • Type-Check

  • ذخیره‌سازی

  • استخراج Dependencyها

  • اجرا در Context مناسب

  • گزارش خطا و دیباگ

این نگاه به ما کمک کرد که Expression رو به‌عنوان یک Feature کامل ببینیم، نه صرفاً یک Utility فنی. مثلاً استخراج متغیرها از AST در ظاهر یک Optimization بود، اما در عمل بخشی از Lifecycle استاندارد Expressionها شد.


جمع بندی

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

در چنین مسئله‌ای، چیزی که نیاز داریم یک زبان برنامه‌نویسی کامل نیست؛ بلکه یک Expression Language امن، typed، قابل گسترش و قابل پیش‌بینیه. برای ما، CEL دقیقاً همین نیاز رو برآورده کرد و به همین دلیل انتخاب مناسبی بود.

البته استفاده از CEL به معنی حل شدن خودکار مسئله نیست. بخش مهم کار تازه بعد از انتخاب آن شروع می‌شه:

  • تعریف Typeهای متناسب با دامنه ERP

  • اضافه کردن Functionهای بیزینسی

  • ساخت envهای دقیق و قابل نگهداری

  • پل زدن بین مدل داده Go و مدل اجرای CEL

  • اعتبارسنجی Expressionها قبل از Runtime

  • و طراحی Lifecycle مناسب برای ذخیره، تحلیل و اجرای عبارت‌ها

در نهایت چیزی که از این مسیر به دست آوردیم فقط یک Evaluator نبود، بلکه یک زیرساخت مشترک برای چندین قابلیت داینامیک محصول بود: از شرط‌های Workflow گرفته تا Validation فرم، محاسبه مقادیر و Ruleهای سفارشی.

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