Light logo

کپسول‌ها

با اصول اولیه کپسول‌ها در ReArch آشنا شوید.

کپسول‌ها

کپسول‌ها اساسی‌ترین بلوک‌های سازنده ReArch هستن.
کپسول‌ها داده‌ها رو کپسوله می‌کنن، و برای همین به این اسم نام‌گذاری شدن.

نظریه

وقتی شروع به طراحی یه اپلیکیشن می‌کنی،
اغلب راحت‌تره که به داده‌هایی که داری مدل‌سازی می‌کنی
و نحوه تعامل اون داده‌ها با داده‌های دیگه توی اپلیکیشنت فکر کنی.
در نتیجه، وقتی می‌ری که یه حالت غیرساده (non-trivial state) بسازی،
به احتمال زیاد اون حالت به یه حالت دیگه توی اپلیکیشنت وابسته‌ست.

توابع حالت

کپسول‌ها بهت اجازه می‌دن به این شکل فکر کنی، چون خودشون توابع حالت هستن
(و به‌صورت کلی‌تر، اثرات جانبی، ولی بعداً بیشتر درباره‌شون می‌گیم).
این ممکنه یه کم گنگ به نظر بیاد، پس یه مثال ملموس اینجاست:

dart
کپسول‌های ساده count و countPlusOne
int countCapsule(CapsuleHandle use) => 0;

int countPlusOneCapsule(CapsuleHandle use) => use(countCapsule) + 1;

توجه کن که اینجا countPlusOneCapsule چطور countCapsule رو مصرف می‌کنه؛
یعنی countPlusOneCapsule یه تابع حالته، که حالت‌ش همون countCapsule فعلیه!

همون‌طور که توی مثال بالا می‌بینی، کپسول‌ها چیز خاصی نیستن؛
هر کپسول فقط یه تابعه که یه CapsuleHandle مصرف می‌کنه.
قدرت یه کپسول از این میاد که چطور از CapsuleHandle توی کپسول استفاده می‌کنی.

تغییرناپذیری (Immutability)

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

CapsuleHandle

خب این پارامتر جادویی CapsuleHandle که همه کپسول‌ها مصرف می‌کنن چیه؟
CapsuleHandle اصلاً جادویی نیست؛
فقط یه اجاره موقت از Container (صفحه بعدی) برای ساخت داده‌های یه کپسوله،
با توجه به حالت کانتینر.

به‌طور خاص‌تر، CapsuleHandle ترکیبی از دو نوع دیگه‌ست:

  • CapsuleReader به کپسول اجازه می‌ده داده‌های فعلی خودش و کپسول‌های دیگه رو بخونه.
  • SideEffectRegistrar به کپسول اجازه می‌ده اثرات جانبی رو ثبت کنه.

CapsuleReader

CapsuleReader همون سینتکس آشنا رو ارائه می‌ده: use(...) توی دارت و get(...) توی Rust.

استفاده از CapsuleReader
int countCapsule(CapsuleHandle _) => 0;

int countPlusOneCapsule(CapsuleHandle use) {
  return use(countCapsule) + 1;
}

SideEffectRegistrar

SideEffectRegistrar مجموعه آشنای اثرات جانبی use.fooBar() توی دارت
و register(effect1(), effect2()) توی Rust رو ارائه می‌ده.
برای اطلاعات بیشتر، مستندات مربوط به اثرات جانبی رو ببین.

کپسول‌های عمومی (Generic)

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

مثال کپسول‌های عمومی
List<T> Function(T, int) repeatedItemFactory<T>(CapsuleHandle use) {
  return (itemToRepeat, repetitions) => List.generate(repetitions, (_) => itemToRepeat);
}

// ...
final repeatedIntsFactory = container.read(repeatedItemFactory<int>);
final repeatedStringsFactory = container.read(repeatedItemFactory<String>);
final repeatedStrings = repeatedStringsFactory("این ۱۲۳۴ بار تکرار می‌شه!", 1234);

کاهش بازسازی‌ها

کاهش بازسازی‌ها یه بهینه‌سازی مفیده که توی ReArch می‌تونی از طریق این روش‌ها انجام بدی:

  1. استفاده شرطی از کپسول‌ها
  2. کپسول‌های درون‌خطی (inline capsules)

کی نیاز به کاهش بازسازی‌ها داری؟

کپسول زیر رو در نظر بگیر.

کپسول با بازسازی‌های بیش‌ازحد
FooBar expensiveOperationUsingCount(CapsuleHandle use) {
  final count = use(countCapsule);
  final isWatching = use(isWatchingCapsule);
  return someExpensiveOperation(isWatching ? count : null);
}

این به نظر مشکلی نداره تا وقتی متوجه بشی که هر وقت count تغییر می‌کنه،
someExpensiveOperation/some_expensive_operation دوباره فراخوانی می‌شه،
حتی اگه isWatching/is_watching فالس باشه!
این بازسازی‌های اضافی رو می‌تونی با روش زیر حذف کنی.

محدود کردن بازسازی‌های اضافی
FooBar expensiveOperationUsingCount(CapsuleHandle use) {
  if (use(isWatchingCapsule)) {
    // داشتن این use توی یه شرط، بازسازی‌هایی که به خاطر  
    // countCapsule وقتی isWatching فالسه ایجاد می‌شن رو متوقف می‌کنه.  
    return someExpensiveOperation(use(countCapsule));
  }
  return someExpensiveOperation(null);
}

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

کپسول‌های درون‌خطی

کپسول‌های درون‌خطی فقط کپسول‌های معمولی هستن (که یادتونه—just functions هستن)،
ولی خاصن چون کلوژر (closures) هستن.

دو راه برای ساختن کپسول‌های درون‌خطی وجود داره:

  • myCapsule.map()، یه متد الحاقی便利 که یه کپسول درون‌خطی می‌سازه
  • به‌صورت دستی (ولی اینجا باید احتیاط کنی)!

هر کدوم یه سری موارد استفاده کمی متفاوت رو حل می‌کنن.
با این حال، تا جایی که می‌تونی بهتره کپسول‌های سطح بالای واسطه‌ای جدید بسازی،
و بذاری ReArch خودش بهینه‌سازی‌هاش رو برای محدود کردن بازسازی‌ها انجام بده.
یادت باشه، کپسول‌ها ارزونن و برای ترکیب‌پذیری طراحی شدن.
فقط توی موارد خیلی نادر باید از کپسول‌های درون‌خطی استفاده کنی،
و وقتی این کار رو می‌کنی، مطمئن شو که خیلی ارزون هستن (در حالت ایده‌آل فقط یه جست‌وجوی زمان ثابت).

.map()

اپلیکیشن نمونه فلاتر از .map() برای کاهش بعضی بازسازی‌ها استفاده می‌کنه،
پس برای یه مثال کامل‌تر اون رو ببین.
ولی اینجا یه مثال ساده‌ست که بهت ایده می‌ده .map() چطور کار می‌کنه.

dart
مثال کپسول درون‌خطی با .map()
class MyListItem extends RearchConsumer {
  const MyListItem(this.listIndex, {super.key});

  final int listIndex;

  Widget build(BuildContext context, WidgetHandle use) {
    // با این کپسول درون‌خطی، فقط وقتی داده توی  
    // myList[listIndex] تغییر می‌کنه بازسازی می‌کنیم، به‌جای کل myList.  
    final dataAtIndex = use(
      // این یه کپسول درون‌خطی جدید می‌سازه که یه شاخص خاص از myList رو می‌گیره:  
      myListCapsule.map((myList) => myList[listIndex]),
    );
    return Text('$dataAtIndex');
  }
}

کپسول‌های درون‌خطی دستی

برخلاف متد راحتی .map()، کپسول‌های درون‌خطی دستی کلوژر رو به‌صورت صریح تعریف می‌کنن.
این وقتی مفیده که کپسول درون‌خطی‌ت به >۱ کپسول دیگه وابسته باشه،
که باید خیلی نادر باشه.

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

dart
مثال کپسول درون‌خطی دستی
class MyListItem extends RearchConsumer {
  const MyListItem(this.listIndex, {super.key});

  final int listIndex;

  Widget build(BuildContext context, WidgetHandle use) {
    // با این کپسول درون‌خطی، فقط وقتی داده توی  
    // myList[listIndex] تغییر می‌کنه بازسازی می‌کنیم، به‌جای کل myList.  
    final dataAtIndex = use(
      // این یه کپسول درون‌خطی جدید می‌سازه که یه شاخص خاص از myList رو می‌گیره:  
      (CapsuleReader use) => use(myListCapsule)[listIndex],

      // هرچند کد زیر کار می‌کنه، ولی یه روش بد به حساب میاد  
      // (برای توضیح ادامه رو بخون):  
      // (use) => use(myListCapsule)[listIndex],
    );
    return Text('$dataAtIndex');
  }
}

توجه کن که بالا پارامتر به‌عنوان CapsuleReader use تعریف شده؛
هرچند اینجا برای کپسول درون‌خطی یه CapsuleHandle کامل می‌گیری،
باید مراقب باشی که از هیچ اثر جانبی‌ای (use.fooBar()) استفاده نکنی وگرنه باعث نشتی می‌شی.
برای همین، یه روش خوبه که جلوی خودت رو بگیری از استفاده از اثرات جانبی
با تعریف نوع پارامتر به‌عنوان CapsuleReader به‌جای اینکه بذاری به‌صورت خودکار
به‌عنوان CapsuleHandle استنباط بشه (که اگه صراحتاً CapsuleReader رو مشخص نکنی این اتفاق می‌افته).
در واقع می‌تونی همین الگو رو برای کپسول‌های معمولی‌ت هم استفاده کنی
اگه بخوای بگی که نمی‌تونه از اثرات جانبی استفاده کنه (یعنی یه کپسول idempotent باشه)،
ولی این توی جاهای دیگه مستندات توضیح داده نشده تا مبتدی‌ها رو گیج نکنه.