عنوان:

‫هنر بکارگیری پیشوندهای بولی: چرا نباید متغیرهای خود را "Flag" نام‌گذاری کنید


نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۹/۱۸ ۱۰:۳۰
آدرس: www.dntips.ir
در دنیای توسعه نرم‌افزار، نام‌گذاری متغیرها یکی از جنبه‌های کلیدی است که اغلب نادیده گرفته می‌شود. تصور کنید که یک کدبیس قدیمی را به ارث برده‌اید و با متغیرهایی مانند open، flag یا done روبرو می‌شوید. این نام‌ها در نگاه اول ساده به نظر می‌رسند، اما در عمل، ابهام آن‌ها می‌تواند منجر به باگ‌های پنهان و سردرگمی شود؛ به‌ویژه در ساعات نیمه‌شب هنگام رفع عیب. متغیرهای بولی (boolean) به دلیل طبیعت دوگانه‌شان (بله/خیر)، مانند مین‌های زمینی عمل می‌کنند؛ اگر نام آن‌ها واضح نباشد، تفسیر اشتباه آن‌ها آسان است. در این مقاله، به بررسی مشکل نام‌گذاری ضعیف متغیرهای بولی می‌پردازیم و راه‌حل‌هایی عملی برای بهبود خوانایی کد ارائه می‌دهیم. این رویکرد نه تنها کد شما را حرفه‌ای‌تر می‌کند، بلکه همکاری تیمی را تسهیل کرده و از بروز خطاها جلوگیری می‌نماید.

توضیح مشکل اصلی
متغیرهای بولی اغلب با نام‌های مبهمی مانند flag، status یا done تعریف می‌شوند که هیچ اطلاعاتی در مورد معنای واقعی‌شان ارائه نمی‌دهند. برای مثال، کد زیر را در نظر بگیرید:
public class OrderProcessor
{
    private bool open;
    private bool flag;
    private bool done;
    private bool status;

    public void Process(bool check)
    {
        if (open && flag)
        {
            flag = false;
            status = true;
        }

        if (done)
        {
            // چه اتفاقی اینجا می‌افتد؟
        }
    }
}
آیا این کد خوانا است؟ خیر. open به چه معناست؟ آیا فروشگاه باز است یا فایل باید باز شود؟ flag چیست؟ این نام مانند یک یادداشت موقت است که می‌گوید "بعداً درستش می‌کنم". و done آیا یک سؤال است ("آیا تمام شده؟") یا یک دستور ("آن را تمام‌شده علامت بزن")؟
این ابهام‌ها منجر به باگ می‌شوند. یک بازبینی‌کننده کد ممکن است if (done) را بر اساس حدس تفسیر کند، و یک نگهدارنده ممکن است flag = false را تغییر دهد و منطق را ناخواسته معکوس کند. سؤال‌هایی مانند if (!flag)... آیا این به معنای روشن کردن است یا خاموش کردن؟ متغیرهای بولی مانند سؤالاتی هستند که کد می‌پرسد، و مقدار بولی پاسخ "بله" یا "خیر" است. اگر نام متغیر سؤال واضحی نباشد، پاسخ بی‌معنی می‌شود. این مشکل در تیم‌های بزرگ‌تر تشدید می‌شود، جایی که توسعه‌دهندگان مختلف با زمینه‌های متفاوت کد را می‌خوانند. علاوه بر این، ابزارهای خودکار مانند refactoring IDEها ممکن است این ابهام‌ها را بدتر کنند، زیرا نمی‌توانند مقصود انسانی را تشخیص دهند.

راه‌حل: استفاده از چهار پیشوند اصلی
برای پوشش ۹۹٪ سناریوهای نام‌گذاری بولی، تنها چهار پیشوند کافی است. اگر به این‌ها پایبند باشید، هر متغیر بولی به یک سؤال گرامری واضح تبدیل می‌شود:
IS: برای توصیف هویت و وضعیت فعلی. معمولاً با یک صفت جفت می‌شود.
  • خوب: isActive (آیا فعال است؟)، isDeleted (آیا حذف‌شده است؟)، isEmpty (آیا خالی است؟).
  • بد: isAccess (ناهماهنگی گرامری؛ از hasAccess استفاده کنید).
HAS: برای توصیف مالکیت یا وجود ویژگی. با یک اسم جفت می‌شود.
  • خوب: hasAccess (آیا دسترسی دارد؟)، hasChildren (آیا فرزند دارد؟)، hasValidationErrors (آیا خطاهای اعتبارسنجی دارد؟).
  • بد: hasActive (ناهماهنگی گرامری؛ از isActive استفاده کنید).
CAN: برای بررسی مجوزها یا اقدامات ممکن.
  • خوب: canEdit (آیا می‌تواند ویرایش کند؟)، canDelete (آیا می‌تواند حذف کند؟)، canRetry (آیا می‌تواند دوباره امتحان کند؟).
  • بد: canAdmin (مبهم؛ از isAdmin یا canAdminister استفاده کنید).
SHOULD: برای قوانین کسب‌وکار یا تصمیم‌گیری‌های سیستم. این پیشوند "آنچه می‌توانیم انجام دهیم" را از "آنچه می‌خواهیم انجام دهیم" جدا می‌کند.
  • خوب: shouldRetry (آیا باید دوباره امتحان کند؟)، shouldCacheResponse (آیا باید پاسخ را کش کند؟).
  • بد: shouldUser (ناقص؛ shouldCreateUser؟).
با رعایت این پیشوندها، از اختلاط آن‌ها، مانند isAccess یا hasActive اجتناب کنید، زیرا خواننده را مجبور به توقف و بازخوانی می‌کند. این اصطکاک جایی است که باگ‌ها وارد می‌شوند.

برای وضوح بیشتر، جدول زیر حوزه‌ی هر پیشوند را خلاصه می‌کند:
پیشوندحوزهعملکردمثال
ISهویت / وضعیتتوصیف وضعیت فعلی شیءisActive, isEmpty
HASمالکیتتوصیف وجود ویژگی یا مالکیتhasChildren, hasAccess
CANقابلیتتوصیف مجوز یا اقدام ممکنcanEdit, canRetry
SHOULDقصد / منطقتوصیف توصیه قوانین کسب‌وکارshouldCache, shouldRetry

نکته: در زبان‌های برنامه‌نویسی مانند جاوا یا سی‌شارپ، این پیشوندها با کنوانسیون‌های camelCase سازگار هستند، اما در پایتون می‌توانید از snake_case مانند is_active استفاده کنید.

قانون "بدون منفی‌ها"
مهم‌ترین قانون: هرگز از نفی/انکار/منفی کردن در نام متغیر استفاده نکنید. از نام‌هایی مانند isNotEnabled، hasNoAccess یا isDisabled اجتناب کنید.
چرا؟ زیرا در نهایت باید وضعیت مخالف را بررسی کنید. if (!isDisabled) مغز را مجبور به محاسبه‌ی دو منفی می‌کند: "اگر غیرفعال نیست...". در مقابل، if (isEnabled) واضح و فوری است.
نام‌های منفی در زمان نوشتن طبیعی به نظر می‌رسند ("باید بررسی کنم آیا غیرفعال است")، اما برای خوانندگان آینده هزینه‌بر هستند. ابزارهای refactoring حتی ممکن است isDisabled را به isNotDisabled تغییر دهند و درک کد را مشکل‌تر کنند.
استثنا: تنها زمانی منفی مجاز است که با یک API خارجی یا ویژگی HTML منفی مانند noValidate همخوانی داشته باشد. حتی در این موارد، آن را در مرز جدا کنید: bool shouldValidate = !request.noValidate;.
این قانون بر اساس اصول روان‌شناختی خوانایی کد است؛ مغز انسان منفی‌ها را کندتر پردازش می‌کند، و در کدهای پیچیده، این مورد می‌تواند به خطاهای منطقی منجر شود.

زمینه مهم است (تله بولی)
این قوانین برای ویژگی‌ها (properties) عالی کار می‌کنند، اما برای پارامترها (آرگومان‌های تابع) شکست می‌خورند. این "تله بولی" است:
// کد واقعی از NHibernate
schemaExport.Execute(false, true, false);
دومین true چه می‌کند؟ هیچ ایده‌ای ندارید و باید کد منبع کتابخانه را باز کنید.
اگر سیگنچر تابعی مانند Execute(bool, bool, bool) دیدید، با یک نقص طراحی روبرو هستید.
راه‌حل‌ها:
  • تقسیم تابع: اگر بولی رفتار را کاملاً تغییر دهد، دو تابع بسازید.
// بد
email.Send(message, true); // true یعنی "فوری"؟ "اولویت بالا"؟
// خوب
email.SendImmediately(message);
email.SendQueued(message);
  • استفاده از Enum: اگر حالت‌هایی وجود دارد، آن‌ها را نام‌گذاری کنید.
// بد
file.Write(data, true);
// خوب
file.Write(data, WriteMode.Append);
  • استفاده از شیء پیکربندی: برای چندین فلگ، یک شیء پاس دهید.
// بد
export.Execute(false, true, false);
// خوب
export.Execute(new ExportOptions {
    Script = false,
    Export = true,
    JustDrop = false
});
نکته: در طراحی APIها، از اصول SOLID پیروی کنید؛ تله بولی اغلب نقض اصل Single Responsibility است.

آنتی‌پترن‌ها
در بازبینی کد، به این بوی بدها توجه کنید:
متغیرهای "شانه بالا": نام‌هایی مانند flag، done یا check هیچ اطلاعاتی نمی‌دهند.
  • بد: if (check)
  • اصلاح: if (isPaymentVerified)
ناهماهنگی گرامری: شکستن قوانین پیشوند.
  • بد: if (user.hasActive)
  • اصلاح: if (user.isActive)
دو منفی: دیدن ! کنار Not یا No.
  • بد: if (!isNotEnabled)
  • اصلاح: if (isEnabled)
بولی چندمنظوره: یک متغیر که سه چیز متفاوت را پیگیری می‌کند.
  • بد: isValid (در واقع بررسی وجود کاربر، ایمیل و فعال بودن).
  • اصلاح: صریح باشید. bool isReadyForBilling = user.Exists && user.HasEmail && user.IsActive;
فلگ شناور: بولی محلی که برای مراحل مختلف بازاستفاده می‌شود.
  • بد:
bool error = false;
if (!Save()) error = true;
if (!error && !SendEmail()) error = true;
return error;
  • اصلاح: از شیء نتیجه استفاده کنید یا زود بازگشت دهید. بولی را بازیافت نکنید.

نتیجه‌گیری
نام‌گذاری نه تنها یک مسئله سبکی است، بلکه نشانه‌ای از همدلی است. وقتی یک فلگ بولی را flag نام‌گذاری می‌کنید، برای لحظه فعلی خودتان می‌نویسید. اما وقتی آن را isProcessed می‌نامید، برای کسی که شش ماه بعد باید باگی را در این فایل رفع کند، می‌نویسید که احتمالاً خودتان هستید. با آینده‌ی خود مهربان باشید! با رعایت پیشوندهای IS، HAS، CAN و SHOULD، اجتناب از منفی‌ها و جلوگیری از تله بولی، کد شما نه تنها خواناتر می‌شود، بلکه قابل اعتمادتر و آسان‌تر برای نگهداری خواهد بود. در نهایت، این اصول کوچک می‌توانند تفاوت بزرگی در کیفیت پروژه‌های شما ایجاد کنند. اگر این رویکرد را در کدهای خود پیاده کنید، متوجه کاهش زمان دیباگینگ و افزایش رضایت تیمی خواهید شد.

نظرات

  • محمد شریفی در ۱۴۰۴/۰۹/۱۸ ۱۱:۱۱
    در چه شرایطی استفاده از Enum یا کلاس پیکربندی به‌جای بولی‌ها منطقی‌تره و چه معیارهایی برای تصمیم‌گیری وجود داره؟
    • وحید نصیری در ۱۴۰۴/۰۹/۱۸ ۱۱:۵۱
      شرایط استفاده از Enum یا کلاس پیکربندی به جای بولی‌ها

      در طراحی کد، متغیرها یا پارامترهای بولی (boolean) ساده و کارآمد هستند، اما در برخی سناریوها می‌توانند منجر به ابهام، کاهش خوانایی و مشکلات نگهداری شوند؛ پدیده‌ای که به عنوان "تله بولی" (Boolean Trap) شناخته می‌شود. در چنین مواردی، جایگزین‌هایی مانند Enum (نوع شمارش‌پذیر) یا کلاس پیکربندی (config object) منطقی‌تر هستند. در ادامه، به بررسی شرایط مناسب برای استفاده از این جایگزین‌ها می‌پردازم و معیارهای تصمیم‌گیری را توضیح می‌دهم. این اصول بر اساس بهترین شیوه‌های برنامه‌نویسی (مانند اصول SOLID و Clean Code) شکل گرفته‌اند و هدف‌شان افزایش وضوح و قابلیت گسترش کد است.

      مقدمه: چرا بولی‌ها گاهی مشکل‌ساز می‌شوند؟
      بولی‌ها ذاتاً دوحالی هستند (true/false)، اما وقتی به عنوان پارامتر تابع استفاده می‌شوند، معنای‌شان بدون زمینه مشخص نیست. برای مثال، در تابعی مانند SendEmail(message, true)، true ممکن است به معنای "فوری ارسال شود"، "اولویت بالا" یا چیز دیگری باشد. این ابهام زمانی تشدید می‌شود که چندین بولی در یک تابع وجود داشته باشد، مانند Execute(false, true, false) و خواننده کد باید مستندات یا کد منبع را بررسی کند تا بفهمد هر کدام چه می‌کند. جایگزین‌ها مانند Enum و کلاس پیکربندی این مشکل را با نام‌گذاری صریح حل می‌کنند.

      شرایط استفاده از Enum به جای بولی
      Enum زمانی منطقی‌تر است که یک بولی نشان‌دهنده‌ی حالت‌های محدود و مشخصی باشد که ممکن است در آینده گسترش یابند. Enum خوانایی را افزایش می‌دهد و از خطاهای تایپی جلوگیری می‌کند، زیرا مقادیر آن‌ها ثابت و نام‌گذاری‌شده هستند.
      شرایط مناسب:
      • وقتی بولی بیش از دو حالت بالقوه دارد: اگر امروز فقط true/false دارید، اما ممکن است حالت سومی (مانند "unknown" یا "partial") اضافه شود، Enum بهتر است. مثلاً به جای Write(data, true) (که true یعنی append)، از Enum استفاده کنید: Write(data, WriteMode.Append).
      • وقتی بولی رفتار تابع را به شیوه‌های مختلف تغییر می‌دهد: اگر true یک حالت و false حالتی دیگر را فعال می‌کند، Enum می‌تواند حالت‌ها را صریح‌تر کند. مثلاً در پردازش پرداخت: به جای ProcessPayment(secure: true)، از ProcessPayment(mode: PaymentMode.Secure).
      • در سیستم‌هایی با قوانین کسب‌وکار پیچیده: مانند حالت‌های سفارش (Pending, Processing, Completed) که بولی ساده نمی‌تواند پوشش دهد.
      • وقتی نیاز به سوئیچینگ یا شرطی‌سازی بر اساس حالت دارید: Enum اجازه می‌دهد از switch-case استفاده کنید که خواناتر از چندین if است.

      مثال کد:
      // بد: با بولی
      public void WriteFile(string data, bool append) {
          if (append) { /* append logic */ } else { /* overwrite logic */ }
      }
      
      // خوب: با Enum
      public enum WriteMode { Append, Overwrite }
      
      public void WriteFile(string data, WriteMode mode) {
          switch (mode) {
              case WriteMode.Append: /* append logic */ break;
              case WriteMode.Overwrite: /* overwrite logic */ break;
          }
      }

      شرایط استفاده از کلاس پیکربندی به جای بولی
      کلاس پیکربندی (یا شیء گزینه‌ها) زمانی مناسب است که چندین بولی مستقل یا مرتبط وجود داشته باشد. این رویکرد کد را ماژولار می‌کند و اجازه می‌دهد پارامترها را بدون تغییر سیگنچر تابع گسترش دهید.
      شرایط مناسب:
      • وقتی چندین بولی در پارامترهای تابع وجود دارد: اگر بیش از یک یا دو بولی مستقل را دارید، ابهام افزایش می‌یابد. مثلاً به جای Export(script: false, export: true, drop: false)، از یک شیء استفاده کنید.
      • وقتی بولی‌ها اختیاری هستند یا مقادیر پیش‌فرض دارند: کلاس پیکربندی اجازه می‌دهد فقط مقادیر مورد نیاز را تنظیم کنید، بدون نیاز به پاس دادن تمام پارامترها.
      • در APIهای پیچیده یا کتابخانه‌ها: جایی که کاربران خارجی تابع را فراخوانی می‌کنند، شیء پیکربندی خوانایی را برای آن‌ها افزایش می‌دهد و از positional arguments (پارامترهای موقعیتی) جلوگیری می‌کند.
      • وقتی نیاز به گروه‌بندی گزینه‌ها دارید: مانند تنظیمات صادرات فایل (شامل فرمت، مسیر، و گزینه‌های امنیتی) که بولی ساده نمی‌تواند پوشش دهد.
      • در سناریوهای testable: شیء پیکربندی آسان‌تر تست می‌شود، زیرا می‌توانید mock کنید.

      مثال کد:
      // بد: با چندین بولی
      public void Export(bool script, bool export, bool justDrop) { /* logic */ }
      
      // خوب: با کلاس پیکربندی
      public class ExportOptions {
          public bool Script { get; set; } = false;
          public bool Export { get; set; } = true;
          public bool JustDrop { get; set; } = false;
      }
      
      public void Export(ExportOptions options) { /* logic using options.Script, etc. */ }
      این رویکرد با اصل Open-Closed Principle سازگار است، زیرا می‌توانید فیلدهای جدیدی را بدون تغییر تابع اضافه کنید.

      معیارهای تصمیم‌گیری
      برای انتخاب بین بولی، Enum یا کلاس پیکربندی، از این معیارها استفاده کنید:
      تعداد حالت‌ها یا گزینه‌ها:
      • ۱-۲ حالت ساده: بولی کافی است (مثل isActive).
      • بیش از ۲ حالت بالقوه: Enum (برای گسترش‌پذیری).
      • چندین گزینه مستقل (۳+): کلاس پیکربندی (برای جلوگیری از پارامترهای طولانی).
      خوانایی و ابهام:
      • اگر نام بولی واضح نباشد یا نیاز به مستندات داشته باشد، جایگزین کنید.
      • ارزیابی کنید: آیا خواننده کد بدون دیدن پیاده‌سازی می‌فهمد پارامتر چه می‌کند؟ اگر نه، Enum یا پیکربندی بهتر است.
      گسترش‌پذیری آینده:
      • اگر احتمال اضافه شدن حالت‌ها یا گزینه‌ها وجود دارد، از بولی اجتناب کنید. Enum و پیکربندی انعطاف‌پذیرتر هستند.
      پیچیدگی منطق:
      • اگر بولی‌ها منجر به شرطی‌سازی پیچیده (nested ifها) می‌شوند، Enum با switch یا پیکربندی با دسترسی مستقیم بهتر است.
      نوع استفاده (پارامتر vs. ویژگی):
      • برای پارامترهای تابع: جایگزین‌ها اولویت دارند.
      • برای ویژگی‌های کلاس: بولی اغلب کافی است، مگر اینکه بخشی از حالت پیچیده باشد.
      عملکرد و هزینه:
      • بولی‌ها سبک‌تر هستند، اما در برنامه‌های بزرگ، خوانایی مهم‌تر از عملکرد جزئی است. Enum و کلاس‌ها هزینه ناچیزی دارند.
      استانداردهای پروژه:
      • بررسی کنید آیا تیم شما از الگوهایی مانند Builder Pattern برای پیکربندی استفاده می‌کند یا خیر.