در فصل قبل، در این مورد که چگونه اطلاعات زیادی را در نامها بگنجانید صحبت کردیم. در این فصل تمرکز ما بر روی موضوعی متفاوت است با این عنوان: مراقب نامهایی باشید که میتوانند اشتباه فهمیده شوند.
کلید طلایی: با پرسیدن سوالاتی از خودتان، نامهای خود را به دقت بررسی کنید، که دیگر افراد چه معانی دیگری میتوانند از این نام برداشت کنند؟
در این مورد واقعا سعی کنید خلاق بوده و به طور فعال به دنبال تفاسیر اشتباه[1] بگردید. این مرحله به شما کمک میکند تا نامهای مبهم را کشف و در نتیجه بتوانید آنها را تغییر دهید.
در مورد مثالهای این فصل، میخواهیم با صدای بلند فکر کنیم، همان گونه که در حال مشاهده تعبیرات غلط درباره یک نام هستیم، بحث نموده و نام بهتری را انتخاب کنیم.
فرض کنید در حال نوشتن کدی هستید که یک مجموعه از نتایج پایگاه داده را به شکل زیر دستکاری میکند:
results = Database.all_objects.filter("year <= 2011")
به نظر شما نتایج این کد شامل چه مواردی است؟
- اشیائی که مربوط به قبل از سال 2011 هستند؟
- اشیائی که سال آنها قبل از 2011 نیست؟
مشکل این است که filter یک کلمه مبهم است. واضح نیست که آیا معنی آن «انتخاب[2] کردن» یا «مستثنی[3] کردن» است. بهتر است از نام filter اجتناب کنید، زیرا به راحتی فهمی اشتباه از آن صورت میگیرد.
اگر میخواهید «انتخاب» انجام دهید کلمه select() نام بهتری است و اگر میخواهید «مستثنی کنید» نام بهتر exclude() خواهد بود.
فرض کنید تابعی را که محتوای یک پاراگراف را Clip میکند به صورت زیر دارید:
# Cuts off the end of the text, and appends "..."
def Clip(text, length):
...
به دو روش میتوانید تصور کنید که Clip() چگونه رفتار میکند:
- این تابع Length را از انتهای متن حذف میکند.
- این تابع متن را با حداکثر اندازه length کوتاه میکند.
هرچند روش دوم(کوتاه سازی[4]) محتملتر است اما شما هیچ وقت مطمئن نخواهید بود، پس به جای آن که خواننده خود را با تردیدهای ناخوشایند رها کنید، بهتر است تابع را به شکل Truncate(text, length) نامگذاری نمایید.
اگرچه هنوز هم نام پارامتر Length قابل سرزنش است و اگر max_length جایگزین آن میشد، موضوع را واضحتر مینمود.
اما هنوز کار ما تمام نشده است. max_length تعبیرهای متعددی را ایجاد میکند:
- تعدادی از بایتها[5]
- تعدادی از کاراکترها[6]
- تعدادی از کلمات[7]
همان گونه که در فصل قبل مشاهده نمودید، این از مواردی است که باید واحد آن، به نام اضافه شود و از آنجا که در این مورد منظور تعداد کاراکترها میباشد بنابراین به جای max_length باید از max_chars استفاده نماییم.
اجازه دهید این گونه بگوییم که برنامه سبد خرید باید از خرید بیش از ده آیتم در یک لحظه توسط افراد جلوگیری کند:
CART_TOO_BIG_LIMIT = 10
if shopping_cart.num_items() >= CART_TOO_BIG_LIMIT:
Error("Too many items in cart.")
این کد یک اشکال کلاسیک off-by-one دارد و ما به راحتی میتوانیم آن را با تغییر >= به > برطرف نماییم:
if shopping_cart.num_items() > CART_TOO_BIG_LIMIT:
و یا با تعریف مجدد CART_TOO_BIG_LIMIT به عدد 11 این اشکال را از بین ببریم. اما مشکل اصلی مبهم بودن نام CART_TOO_BIG_LIMIT میباشد، چرا که واضح نیست منظور شما «تا آن[9]» یعنی کمتر از آن است یا «تا آن و شامل[10]» یعنی همان کمتر و مساوی آن.
کلید طلایی: شفافترین راه برای نام گذاری یک محدودیت این است که mx_ یا min_ را قبل از نام آنچه که قرار است محدود شود، قرار دهید.
در مثال بالا باید MAX_ITEMS_IN_CART به عنوان نام انتخاب شود. کد جدید ساده و شفاف است:
MAX_ITEMS_IN_CART = 10
if shopping_cart.num_items() > MAX_ITEMS_IN_CART:
Error("Too many items in cart.")
در اینجا مثال دیگری داریم که شما نمیتوانید بگویید منظورش از «۲ تا ۴» یعنی فقط ۳ یا ۲ و ۳ یا اینکه شامل ۲ و ۴ نیز میشود:
print integer_range(start=2, stop=4)
# Does this print [2,3] or [2,3,4] (or something else)?
اگرچه نام پارامتر start منطقی به نظر میرسد، اما stop میتواند به چندین صورت معنی شود. برای محدودههای جامع همچون این مثال(یعنی مواردی که در آن محدوده باید شامل دو نقطه آغاز و پایان باشد) گزینه مناسب first/last است:
set.PrintKeys(first="Bart", last="Maggie")
برخلاف کلمه stop، کلمه last به طور واضح معنی جامعی دارد. علاوه بر first/last، نامهای min/max نیز احتمالا برای دامنههای جامع[13] مفید باشند البته با این فرض که در این زمینه درست به نظر میرسند.
معمولا استفاده از محدودههای جامع/اختصاصی در عمل راحتتر است. به عنوان مثال، اگر شما بخواهید تمام وقایع اتفاق افتاده در ۱۶ اکتُبر را چاپ کنید، این سادهتر است:
PrintEventsInRange("OCT 16 12:00am", "OCT 17 12:00am")
از اینکه بخواهیم بنویسیم:
PrintEventsInRange("OCT 16 12:00am", "OCT 16 11:59:59.9999pm")
حال این سوال مطرح میشود که یک جفت نام خوب برای این پارامترها چیست؟
ظاهرا قرارداد برنامهنویسی معمول برای نام گذاری یک محدوده جامع/اختصاصی، کلمات begin/end میباشد.
اما اگر بخواهیم دقیقتر نگاه کنیم، کلمه end کمی مبهم بوده و به عنوان مثال در جمله « من آخر کتاب هستم[14]» کلمه end (در زبان انگلیسی) جامع است. متاسفانه زبان انگلیسی کلمهای مختصر برای عبارت «آخرین مقدار قبلی[15]» ندارد.
چرا که begin/end خیلی مصطلح هستند و حداقل، از این روش در کتابخانه استاندارد C++ و نیز اغلب مواردی که به تکه تکه[16] شدن یک ارایه نیاز است استفاده میشوند. پس ظاهرا این کلمات بهترین گزینه موجود هستند.
وقتی نامی را برای یک متغیر Boolean انتخاب میکنید و یا زمانی که تابعی یک Boolean را بر میگرداند، مطمئن شوید که true و false به طور واضح چه معنایی میدهند.
یک مثال چالش برانگیز را ببینید:
bool read_password = true;
بسته به این که شما آن را چگونه میخوانید، میتوان دو معنای کاملا متفاوت برداشت کرد:
- ما نیاز داریم که پسورد را بخوانیم
- پسورد همواره خوانده شده است
در این مورد، بهتر است از کلمه read اجتناب نموده و به جای آن از need_password یا user_is_authenticated استفاده کنید.
به طور کلی، اضافه کردن کلماتی مانند، is، has، can، یا should can معنای نامهای متغیرهای Boolean را شفافتر میکند. به عنوان مثال به نظر میرسد تابعی با نام SpaceLeft() یک مقدار عددی را بر گرداند، ولی اگر قرار باشد که یک مقدار Boolean را برگرداند، بهتر است به شکل HasSpaceLeft() نام گذاری شود.
و نکته پایانی این که از عبارتهای خنثی[17] در انتخاب یک نام اجتناب کنید، به عنوان مثال به جای:
bool disable_ssl = false;
بهتر است برای خوانایی سادهتر و مختصرتر، از این کد استفاده کنیم:
bool use_ssl = true;
بعضی از نامها به این دلیل که کاربر یک ایده از پیش تعبیر شده از معنای نام را در ذهن خود دارد، گمراه کننده میشوند، حتی اگر منظور شما چیز دیگری باشد. در این موارد تسلیم شدن و تغییر نام به چیزی که گمراه کننده نیست، بهترین کار است.
اکثر برنامهنویسان به این قرارداد عادت کردهاند که متدهای شروع شده با get، به شکل «lightweight accessors» هستند و به سادگی یک عضو داخلی را بر میگردانند. مخالفت با این قرارداد به احتمال زیاد باعث گمراهی کاربران میشود. در اینجا مثالی در زبان Java درباره اینکه چه کاری نباید انجام شود داریم:
public class StatisticsCollector {
public void addSample(double x) { ... }
public double getMean() {
// Iterate through all samples and return total / num_samples
}
...
}
در این مورد، پیاده سازی getMean() برای تکرار روی دادههای گذشته و محاسبه میانگین در پرواز است. اگر دادههای زیادی وجود داشته باشد، این مرحله احتمالا پر هزینه خواهد بود! اما یک برنامهنویس نا آشنا ممکن است با این فرض که معنای آن کم هزینه است، نادانسته getMean() فراخوانی کند.
به همین دلیل، متد باید به چیزی شبیه computeMean() تغییر نام داده شود که بیشتر نشان دهنده یک عملیات پر هزینه است. البته باید پیاده سازی مجددی صورت گیرد، تا در واقع یک عملیات سبک[19] محسوب شود.
در اینجا یک مثال از کتابخانه استاندارد C++ داریم. کد زیر دلیل سخت پیدا شدن اشکالی بود که باعث میشد یکی از سرورهای ما هنگام crawl کردن بسیار کند شود:
void ShrinkList(list<Node>& list, int max_size) {
while (list.size() > max_size) {
FreeNode(list.back());
list.pop_back();
}
}
اشکال این است که خواننده نمیداند list.size() یک عملیات O(n) است و به جای اینکه فقط یک تعداد از قبل محاسبه شده را برگرداند، نودهای لیست پیوندی را به صورت نود به نود میشمارد و در نتیجه سبب میشود ShrinkList() یک عملیات با شود.
کد از نظر تکنیکی درست است و همه تستهای واحد[20] ما را پاس میکند، اما زمانی که ShrinkList() روی یک لیست با یک میلیون عنصر صدا زده شده بود، تمام شدنش حدود یک ساعت طول کشید!
شاید شما بگویید این اشکال تقصیر کسی است که این تابع از کد را فراخوان کرده است، چرا که او باید مستندات را با دقت بیشتری بخواند. این درست است اما در این مورد، این واقعیت که list.size() یک عمل ثابت- زمان[21] نیست(به این معنی که مدت زمان اجرای آن ثابت نیست)، تعجب آور است. زیرا دیگر کانتینرها در C++ یک متد size() به شکل ثابت-زمان دارند.
عبارت Size() باید به صورت countSize() یا countElements() نام گذاری شود که در این صورت احتمال رخ دادن اشتباه مشابه کمتر خواهد بود. نویسندههای کتابخانه استاندارد C++ احتمالا میخواستهاند نام متد size() برای همه کانتینرهای دیگر مانند vector و map نیز تطابق داشته باشد. اما به دلیل انجام این کار، برنامهنویسان به سادگی آن را به عنوان یک عمل سریع اشتباه میگیرند، چرا که این روش برای سایر کانتینرها نیز انجام شده است. البته جای نگرانی نبوده و خوشبختانه آخرین استاندارد C++ هم اکنون سرعت اجرای size() را به O(1) کاهش داده است.
خیلی وقت پیش، نویسندهای در حال نصب سیستم عامل OpenBSD بود که در مرحله فرمت کردن دیسک، منوی پیچیدهای در مورد پارامترهای دیسک ظاهر شد. یکی از گزینهها حالت جادوگر یا «Wizard mode» بود. او به نظرش رسید که این گزینه کاربر-پسندی است و آن را انتخاب کرد. در کمال ناراحتی برنامه ادامه کار را برای دریافت دستورات فرمت کردن دیسک به صورت دستی(در حالت خط-فرمان) قرار داد، که روش روشنی برای خارج شدن از آن وجود نداشت. ظاهرا منظور از wizard این بود که شما جادوگر هستید!
هنگام تصمیم گیری در مورد انتخاب یک نام خوب، احتمالا چندین گزینه را در نظر دارید. این طبیعی است که در مورد معیار انتخاب خود در مورد هر کدام از نامها دچار تردید شوید.
مثال زیر پیچیدگی این فرآیند را نشان میدهد:
وبسایتهای پر ترافیک اغلب از آزمایشات(experiments) برای این تست که آیا یک تغییر در سایت موجب بهبود تجارتشان میشود یا خیر استفاده میکنند. در اینجا مثالی از یک فایل پیکربندی که بعضی از experimentsها را کنترل میکند داریم:
experiment_id: 100
description: "increase font size to 14pt"
traffic_fraction: 5%
هر آزمایش با حدود ۱۵ جفت صفت/مقدار[22] تعریف شده است. متاسفانه زمانی که آزمایش دیگری که خیلی شبیه این آزمایش است، تعریف میشود، شما مجبور خواهید بود که اکثر این خطوط را کپی کنید:
experiment_id: 101
description: "increase font size to 13pt"
[other lines identical to experiment_id 100]
فرض کنید ما میخواهیم این شرایط را با معرفی روشی برای داشتن یک آزمایش به گونهای درست کنیم که آن آزمایش از ویژگیهای دیگر آزامایشها مجددا استفاده کند. که در واقع این یک الگوی prototype inheritance خواهد بود. نتیجه نهایی چیزی شبیه این خواهید داشت:
experiment_id: 101
the_other_experiment_id_I_want_to_reuse: 100
[change any properties as needed]
حال سوالی که درباره the_other_experiment_id_I_want_to_reuse مطرح میشود این است که واقعا چه نامی باید برای آن انتخاب شود؟ در اینجا چهار نام در نظر گرفته شده است:
- Template
- Reuse
- Copy
- Inherit
هر کدام از این نامها برای ما دارای معنایی هستند، زیرا این ما هستیم که این ویژگی جدید را به زبان پیکربندی اضافه میکنیم. اما باید تصور کنیم که این نام در نظر کسی که صرفا کد را میبیند و در مورد ویژگی آن چیزی نمیداند چگونه به نظر میرسد. بنابراین بیاید هر نام را بررسی کنیم و در مورد راههایی که کسی بتواند آن را اشتباه تفسیر کند فکر کنیم.
- بیایید تصور کنیم که از نام template استفاده میکنیم:
experiment_id: 101
template: 100
...
کلمه template دو مشکل دارد. اولا این در مورد معنایش «من یک template هستم» یا «من از یک template دیگری استفاده میکنم» شفاف نیست. دوم اینکه یک template غالبا چیزی انتزاعی است و باید قبل از اینکه واقعی باشد، شرح داده شود. ممکن است کسی فکر کند که یک الگوی آزمایش یک آزمایش « واقعی » نیست. به طور کلی template در این شرایط خیلی مبهم است.
- حال فرض کنید reuse را انتخاب کنیم:
experiment_id: 101
reuse: 100
..
کلمه reuse مناسب است اما همان گونه که قبلا گفتیم ممکن است کسی برداشتش این باشد که این آزمایش حداکثر ۱۰۰ مرتبه قابل استفاده مجدد است. تغییر این نام به reuse_id میتواند کمک کننده باشد. اما ممکن است دوباره خواننده گیج شده و فکر کند که عبارت reuse_id: 100 به این معنی است که شناسه من برای استفاده مجدد 100 است.
- اکنون copy را در نظر بگیرید:
experiment_id: 101
copy: 100
...
کلمه copy کلمه خوبی است، اما به تنهایی ممکن است به نظر برسد copy: 100 به این معنی است که این آزمایش ۱۰۰ مرتبه کپی شده است یا این ۱۰۰امین کپی از چیزی است. برای شفافتر شدن اینکه این عبارت به دیگر آزمایشها اشاره میکند، ما باید نام آن را به copy_experiment تغییر دهیم. پس احتمالا copy_experiment بهترین نام تا اینجای کار است.
- اما حالا inherit را در نظر بگیرید:
experiment_id: 101
inherit: 100
...
کلمه inherit برای اکثر برنامهنویسان آشنا است و این قابل درک است که اصلاحات بعدی پس از ارثبری1 انجام میشود. با ارثبری کلاس، شما همه متدها و اعضای کلاسی دیگر را گرفته و آنها را تغییر میدهید و یا چیزی را به کلاس خود اضافه میکنید. حتی در زندگی واقعی نیز چنین است، مثلا وقتی که از کسی چیزی را به ارث میبرید، این قابل درک خواهد بود که ممکن است آنها را بفروشید یا مالک چیزهای دیگری شوید.
اما اجازه دهید که روشن کنیم که از آزمایش دیگری ارثبری کردهایم. برای این منظور میتوانیم نام آن را با تغییر به inherit_from یا حتی inherit_from_experiment_id بهبود دهیم.
درکل، copy_experiment و inherit_from_experiment_id نامهای خوبی هستند، زیرا با وضوح بیشتری شرح میدهند که چه چیزی رخ میدهد و در عین حال احتمال کجفهمی در مورد آنها حداقل است.
بهترین نامها، آنهایی هستند که سبب کجفهمی یا برداشت اشتباه از معنای خود نشود. برای کسی که کد شما را میخواند، این نام باید همان معنایی را بدهد که شما مد نظر داشتید و نه چیز دیگر. متاسفانه بسیاری از کلمات انگلیسی وقتی در برنامهنویسی استفاده میشوند مبهم یا دوپهلو هستند، همچون filter، length و limit.
قبل از این که در مورد یک نام تصمیم گیری کنید، کمی آن را به چالش کشیده و تصور کنید چگونه این نام ممکن است باعث سوء تفاهم شود. بهترین نامها در برابر تفاسیر نادرست، مقاوم هستند.
زمانی که از نامها برای تعریف حد بالا و پایین یک مقدار استفاده میکنید، max_ و min_ پیشوندهای مناسبی برای استفاده هستند. برای محدودههای جامع، first و last نامهای خوبی هستند. برای محدودههای جامع/اختصاصی[23] begin و end بهترین انتخاب هستند زیرا آنها بیشتر مصطلح هستند.
زمانی که یک Boolean را نام گذاری میکنید، برای ایجاد شفافیت بیشتر که اینها Boolean هستند، از کلماتی مانند is و has استفاده کرده و از عبارتهای خنثی اجتناب کنید(همچون disable_ssl).
از انتظارات کاربران درباره معنای کلمات، به شکل دقیق آگاه باشید. برای مثال ممکن است کاربران انتظار داشته باشند که get() یا size() متدهای سبکی4 باشند.
[2]: to pick out
[3]: to get rid of
[4]: Truncation
[5]: Bytes
[6]: Characters
[7]: Words
[8]: Inclusive
[9]: Up to
[10]: Up to and including
[11]: Ranges
[12]: Inclusive
[13]: inclusive ranges
[14]: I’m at the end of the book
[15]: just past the last value
[16]: sliced
[17]: Negate
[18]: Matching Expectations of Users
[19]: lightweight
[20]: Unit test
[21]: constant-time
[22]: attribute/value
[23]: inclusive/exclusive