در طراحی نرمافزار، یکی از چالشهای مهم این است که اجزای مختلف برنامه فقط کار کنند، بلکه رفتارشان قابل پیشبینی، قابل اعتماد و قابل نگهداری باشد. هرچه یک سیستم نرمافزاری بزرگتر میشود، احتمال بروز خطا هم بیشتر میشود. بسیاری از این خطاها به این دلیل رخ میدهند که ورودیها، خروجیها و مسئولیت اجزای برنامه بهصورت دقیق مشخص نشدهاند.
یکی از روشهایی که برای حل این مشکل مطرح شده، Design by Contract یا طراحی بر اساس قرارداد است. این روش که به اختصار DBC نامیده میشود، تلاش میکند رابطه میان اجزای نرمافزار را مانند یک قرارداد شفاف تعریف کند. یعنی مشخص شود هر بخش از برنامه چه شرایطی را باید رعایت کند و در مقابل، بخش دیگر چه نتیجهای را باید تضمین کند.
در DBC، هر تعامل نرمافزاری شبیه رابطه میان دو طرف قرارداد در نظر گرفته میشود. یک طرف، مشتری یا سرویسگیرنده است که از یک متد یا عملیات استفاده میکند. طرف دیگر، سرویسدهنده است که آن متد یا عملیات را اجرا میکند.
مشتری باید قبل از درخواست سرویس، شرایط لازم را فراهم کند. در مقابل، سرویسدهنده هم متعهد میشود اگر آن شرایط برقرار بود، نتیجه درست و مشخصی ارائه دهد. به همین دلیل، DBC کمک میکند مسئولیت هر بخش از برنامه روشنتر شود.
برای مثال، فرض کنید متدی برای برداشت پول از حساب بانکی داریم. مشتری باید مبلغی معتبر وارد کند و موجودی حساب هم باید کافی باشد. اگر این شرایط رعایت شود، سرویسدهنده باید عملیات برداشت را درست انجام دهد و موجودی جدید را بهدرستی تنظیم کند.
طراحی بر اساس قرارداد معمولاً با سه مفهوم اصلی شناخته میشود:
پیششرط یا Precondition شرطی است که باید قبل از اجرای یک متد برقرار باشد. این شرط مشخص میکند متد تحت چه شرایطی اجازه اجرا دارد.
برای مثال، اگر متدی برای تقسیم دو عدد داشته باشیم، یکی از پیششرطها این است که مخرج نباید صفر باشد. اگر مشتری این شرط را رعایت نکند، خطا از سمت فراخواننده است، نه از خود متد.
پسشرط یا Postcondition شرطی است که باید بعد از اجرای موفق متد برقرار باشد. این شرط نشان میدهد اگر متد در شرایط معتبر اجرا شد، چه نتیجهای باید حاصل شود.
برای مثال، در متد واریز پول، پسشرط میتواند این باشد که موجودی جدید حساب برابر با موجودی قبلی بهعلاوه مبلغ واریزی باشد. اگر ورودی معتبر باشد اما این نتیجه حاصل نشود، مشکل از پیادهسازی متد است.
ناوردای کلاس یا Class Invariant شرطی است که باید وضعیت معتبر یک شیء را نشان دهد. این شرط معمولاً قبل و بعد از اجرای متدهای اصلی کلاس باید برقرار باشد.
برای مثال، در کلاس حساب بانکی میتوان گفت موجودی حساب نباید منفی باشد. این شرط به یک متد خاص مربوط نیست، بلکه سلامت کلی شیء را نشان میدهد.
پایتون بهصورت مستقیم مانند زبان ایفل از DBC پشتیبانی نمیکند، اما میتوان با کمک کتابخانههایی مثل icontract این مفهوم را پیادهسازی کرد.
نمونه ساده زیر یک حساب بانکی را نشان میدهد:
import icontract @icontract.invariant(lambda self: self.balance >= 0) class BankAccount: def init(self, initial_balance: int): self.balance = initial_balance @icontract.require(lambda self, amount: amount > 0) def deposit(self, amount: int): self.balance += amount @icontract.require(lambda self, amount: amount > 0) @icontract.require(lambda self, amount: amount <= self.balance) def withdraw(self, amount: int): self.balance -= amount
در این مثال، شرط amount > 0 یک پیششرط است؛ یعنی مبلغ واریز یا برداشت باید مثبت باشد. همچنین شرط amount <= self.balance مشخص میکند برای برداشت، موجودی حساب باید کافی باشد. از طرف دیگر، شرط self.balance >= 0 ناوردای کلاس است و بیان میکند موجودی حساب نباید در وضعیت پایدار منفی شود.
زبان برنامهنویسی Eiffel یکی از مهمترین زبانها در تاریخچه طراحی بر اساس قرارداد است. دلیل اهمیت ایفل این است که DBC در این زبان فقط یک کتابخانه یا ابزار جانبی نیست، بلکه بخشی از ساختار خود زبان محسوب میشود.
در ایفل، برای تعریف پیششرط از require، برای پسشرط از ensure و برای ناوردای کلاس از invariant استفاده میشود. این ویژگی باعث میشود قراردادها در کنار کد اصلی برنامه دیده شوند و فقط به شکل توضیحات متنی باقی نمانند.
نمونه ساده در ایفل میتواند به این شکل باشد:
class BANK_ACCOUNT feature balance: INTEGER deposit (amount: INTEGER) require positive_amount: amount > 0 do balance := balance + amount ensure balance_increased: balance >= amount end invariant balance_non_negative: balance >= 0 end
این مثال نشان میدهد که در ایفل، قراردادها مستقیماً در ساختار کلاس و متد قرار میگیرند. بنابراین خواننده کد میتواند سریعتر بفهمد متد چه ورودیهایی را میپذیرد و پس از اجرا چه نتیجهای را تضمین میکند.
Design by Contract با طراحی شیءگرا ارتباط نزدیکی دارد. در برنامهنویسی شیءگرا، سیستم از مجموعهای از اشیا تشکیل میشود که هرکدام مسئولیت مشخصی دارند و با یکدیگر تعامل میکنند. اگر مسئولیت اشیا و انتظار آنها از یکدیگر روشن نباشد، خطاها پنهانتر و نگهداری سیستم سختتر میشود.
DBC کمک میکند هر شیء مانند موجودیتی با تعهدات مشخص دیده شود. هر متد دقیقاً اعلام میکند چه چیزی از فراخواننده انتظار دارد و در مقابل چه نتیجهای ارائه میدهد. این نگاه باعث میشود طراحی شیءگرا از حالت صرفاً کدنویسی خارج شود و به سمت تعریف دقیق رفتار و مسئولیتها حرکت کند.
یکی از نکات مهم در DBC، ارتباط آن با وراثت و اصل جانشینی لیسکوف است. طبق این اصل، کلاس فرزند باید بتواند جای کلاس پدر استفاده شود، بدون اینکه رفتار مورد انتظار سیستم خراب شود.
در طراحی بر اساس قرارداد، این موضوع با چند قاعده مهم کنترل میشود:
کلاس فرزند نباید پیششرطها را سختگیرانهتر کند، چون در این صورت از مشتری چیزی بیشتر از کلاس پدر انتظار دارد.
کلاس فرزند نباید پسشرطها را ضعیفتر کند، چون در این صورت نتیجهای کمتر از تعهد کلاس پدر ارائه میدهد.
ناوردای کلاس پدر نیز باید در کلاس فرزند حفظ شود.
به زبان ساده، در DBC میتوان گفت: فرزندان نباید کمتر ارائه دهند یا بیشتر انتظار داشته باشند.
یکی از مهمترین مزایای DBC این است که رفتار برنامه را شفافتر میکند. وقتی پیششرط، پسشرط و ناوردا مشخص باشند، برنامهنویس بهتر میفهمد هر متد چه مسئولیتی دارد.
مزیت دیگر، سادهتر شدن اشکالزدایی است. اگر پیششرط نقض شود، مشخص است که مشکل از سمت فراخواننده است. اگر پیششرط برقرار باشد اما پسشرط نقض شود، مشکل از پیادهسازی متد است. اگر ناوردای کلاس نقض شود، وضعیت داخلی شیء دچار مشکل شده است.
DBC همچنین میتواند خوانایی و قابلیت نگهداری کد را افزایش دهد. چون قراردادها نوعی مستندسازی زنده هستند؛ یعنی فقط توضیح نیستند، بلکه میتوانند در زمان اجرا هم بررسی شوند.
با وجود مزایای زیاد، DBC محدودیتهایی هم دارد. اولین محدودیت این است که نوشتن قراردادهای درست نیاز به دقت دارد. اگر پیششرط یا پسشرط اشتباه تعریف شود، خود قرارداد میتواند باعث ابهام شود.
محدودیت دیگر این است که حجم ظاهری کد بیشتر میشود. در پروژههای کوچک ممکن است این موضوع اضافه به نظر برسد، اما در پروژههای بزرگ میتواند از خطاهای جدی جلوگیری کند.
همچنین DBC جایگزین تست نرمافزار نیست. قراردادها میتوانند بسیاری از خطاهای منطقی را آشکار کنند، اما همچنان باید تست، بررسی سناریوهای مختلف و کنترل کیفیت انجام شود.
با وجود مزایای زیاد، طراحی بر اساس قرارداد محدودیتهایی هم دارد. اولین محدودیت این است که نوشتن قراردادهای درست نیاز به دقت دارد. اگر پیششرط یا پسشرط اشتباه تعریف شود، خود قرارداد میتواند باعث ابهام یا خطا شود.
محدودیت دیگر این است که حجم ظاهری کد بیشتر میشود. در پروژههای کوچک ممکن است نوشتن قراردادها اضافه به نظر برسد، اما در پروژههای بزرگ میتواند از خطاهای جدی جلوگیری کند.
همچنین DBC جایگزین تست نرمافزار نیست. قراردادها میتوانند بعضی از خطاهای منطقی را آشکار کنند، اما همچنان باید تست، بررسی سناریوهای مختلف و کنترل کیفیت انجام شود.
طراحی بر اساس قرارداد یکی از روشهای مهم برای افزایش کیفیت و قابلیت اعتماد نرمافزار است. این روش با استفاده از پیششرط، پسشرط و ناوردای کلاس، رفتار صحیح اجزای برنامه را روشن میکند.
DBC باعث میشود رابطه میان مشتری و سرویسدهنده از حالت مبهم خارج شود و به یک رابطه قراردادی دقیق تبدیل گردد. در نتیجه، مسئولیتها بهتر مشخص میشوند، اشکالزدایی سادهتر میشود و طراحی نرمافزار ساختارمندتر خواهد شد.
بهترین کاربرد DBC زمانی است که در کنار طراحی خوب، تست مناسب و مستندسازی دقیق استفاده شود. در چنین حالتی، قراردادها مانند مرزهای روشن در نقشه نرمافزار عمل میکنند و مسیر تعامل میان اجزای سیستم را شفافتر میسازند.
تهیهشده توسط A&N Team
اعضای گروه:
Abbas Azizmohammadi - 40111150013
Negin Baqersad - 40011158213
درس اصول طراحی نرمافزار - نیمسال دوم 1404-1405 - دانشگاه کاشان