بیشتر وقت برنامه نویسان به debug کردن برنامه ها می گذرد. متوسط تعداد خط کد نوشته شده توسط بیشتر برنامه نویسان حدود 20 خط در روز است. دلیل این امر صرف شدن بیشتر وقت برای debugging می باشد. دلایل مختلفی باعث به وجود آمدن ایراد در برنامه می شود و روش های مختلفی برای رفع کردن این ایراد ها وجود دارد.
یکی از مهمترین دلایل ایراد در برنامه ها که در این پست راجع به آن صحبت می کنم وضعیت جاری نادرست در برنامه است. یعنی مثلا یک تابع فرض می کند که مقدار متغیری عمومی یا متغیری در یک نمونه از یک کلاس (instance)، باید عدد خاصی باشد ولی در شرایط فعلی به دلیلی مقدار اشتباهی در این متغیر قرار دارد. برای جلوگیری از این مشکل باید ابتدا مفهوم side-effect را بدانید. side-effect به معنای آن است که گاهی اوقات یک تابع علاوه بر گرفتن ورودی و دادن خروجی اثری بر روی متغیر های دیگر برنامه می گذارد. مثلا خواندن ورودی یک عمل دارای side-effect می باشد. اگر تابعی داشته باشید که برای شما زمان سیستم را بخواند و برگرداند و به علاوه مقدار متغیر latestReadTime را به روز کند که در توابع مختلف قابل دسترسی است، این تابع دارای side-effect است. اگر بخشی دیگر از برنامه با این فرض اجرا شود که مقدار درون lastReadTime حد اکثر 10 ثانیه کوچکتر از زمان حقیقی فعلی است و این زمان بیش از 10 ثانیه قبل به روز شده باشد، برنامه شما دچار مشکل خواهد شد.
همچنین اگر برای انجام کاری از متغیر های عمومی استفاده کنیم مشکل بالا رخ خواهد داد. مثلا همان lastReadTime یک متغیر عمومی است. اگر به جای استفاده از آن هر تابعی که به زمان نیاز دارد، مقدار زمان سیستم را بخواند، دیگر این مشکل پیش نخواهد آمد و دیگر هم نیازی نیست تابع خواندن زمان side-effect داشته باشد. به طور کل به توابعی که مقدار خروجیشان فقط و فقط از روی داده های ورودی مشخص می شود توابع محض و یا pure گفته می شود. این توابع نه متغیری عمومی در برنامه یا کلاس را عوض می کنند و نه با استفاده از متغیر های عمومی و کلاس خروجی خود را مشخص می کنند. مثلا تابع توان یا سینوس کاملا pure هستند. یافتن ایراد در توابع محض بسیار ساده است زیرا مقدار ورودی بررسی می شود و روش محاسبه خروجی نیز مشخص است. اگر تابع از state کلاس یا عمومی استفاده کند آن وقت باید اول متوجه شوید اثر متغیر عمومی در تابع چیست و مقدار متغیر عمومی به چه دلیل مقدار خاصی را گرفته است. یافتن رشته توابعی که باعث پیش آمدن حالت فعلی شده اند همیشه کار ساده ای نیست. این کار به خصوص در برنامه های شبیه سازی و بازی ها و هر برنامه concurrent دیگری بسیار مشکل است.
خوب است سعی کنیم از توابع محض برای بیشتر محاسباتمان استفاده کنیم، آبجکت هایی که به شکل مرجع (pass by reference) به ما داده می شوند را تا حد ممکن تغییر ندهیم و کاری کنیم هر تابع به خودی خود با ورودی هایش کار خود را انجام دهد. البته نوشتن کل برنامه به شکل تابع های محض نیاز به تکنیک های زبان های functional دارد که سرعت اجرا را پایین می آورند و سختی های خاص خود را نیز دارند ولی خیلی خوب است سعی کنیم تا جای ممکن از این توابع استفاده کنیم. من از زمانی که به این موضوع توجه کرده ام بسیار ایراد های کمتری به خاطر state اشتباه در برنامه هایم داشته ام. در زیر مثالی را می آورم که شاید مساله را روشنتر کند.
مثلا اگر می خواهیم آسیب ناشی از حمله به یک دشمن را حاسبه کنیم می توانیم تابع را درون دشمن نوشته و مقدار آخرین تیر خورده به دشمن و میزان healthش را در کلاس دشمن ذخیره کنیم و این تابع با استفاده از این متغیر ها میزان آسیب را حساب کند. در این شرایط شما نمی توایند دقیقا بفهمید چرا مقدار متغیر تیر خورده به دشمن مقدار خاصی دارد و چرا میزان health به حد کافی کم نشده اما اگر این مقدار ها به عنوان ورودی به تابع مربوط به محاسبه آسیب داده شوند آن گاه این تابع بدون توجه به وضعیت فعلی هر آبجکت دیگری می تواند مقدارش را حساب کند و درستی آن را بدون در نظر گرفتن وضعیت کل برنامه می توان امتحان کرد. بدتر از این مثال های بسیار پیچیده ای هستند که مقدار زیادی از state در متغیر های مختلف عمومی قرار داشته باشند که توابع آن ها را می خوانند و با آن ها کار می کنند. این کار باعث مشکل شدن debug برنامه می شود. باید سعی کنیم کلاس هایی کوچک با تعداد متغیر کلاس member variable بسیار کم داشته باشیم و همه توابع مقدارهای لازمشان را از ورودی بگیرند تا پس از یافتن جریان داده ها در برنامه (data flow) ، (که برای درک برنامه الزامی است) بتوانیم بفهمیم کجای سیستم آن طور که باید کار نمی کند. برای جلوگیری از مشکلات مربوط به performance بهتر است آبجکت های بزرگ را به شکل reference به توابع پاس دهیم ولی مقدار آن ها را تغییر ندهیم. در بعضی زبان ها مثل سی پلاس پلاس به علت امکان قرار دادن کلمه کلیدی const در آرگومان های تابع، می توانیم تغییرات را در زمان کامپایل تشخیص دهیم ولی در برخی زبان های دیگر یا باید دیسیپلین داشته باشیم و یا از ساختار های کتابخانه ها استفاده کنیم (مثل Immutable در سی شارپ).
در پست های آینده در باره روش های دیگر نوشتن کد کم خطا و مدیریت state بیشتر خواهم گفت.