مزرعه

مزرعه

تمرین یادگیری

مقدمه

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

ایجاد یک خطا، اگر مدیریت نشود، برنامه را متوقف می‌کند و یک پیام خطا بیرون می‌دهد. در بیشتر موارد، نمی‌خواهید برنامه‌تان هنگام بروز خطا متوقف شود؛ در عوض می‌خواهید خطا را مدیریت کنید و اجرای برنامه را ادامه دهید.

ایجاد یک استثنا

در Crystal، استثناها با «کلیدواژه»ی raise ایجاد می‌شوند و می‌توان به آن‌ها یک String یا یک شیء Exception داد. اگر مدیریت نشود، برنامه متوقف می‌شود و پیام خطا را چاپ می‌کند.

raise "This is an error"

در Crystal چندین استثنای داخلی وجود دارد، مانند ArgumentError، IndexError، KeyError، IOError، SystemCallError، TypeError، ZeroDivisionError و بسیاری دیگر. برای این‌ها باید پیامی به استثنا بدهید.

raise ArgumentError.new("This is an argument error")

مدیریت استثناها

نمی‌خواهیم برنامه‌مان وقتی استثنایی ایجاد می‌شود از کار بیفتد. بنابراین، وقتی می‌دانیم بخشی از کد مستعد خطاست، می‌توانیم آن را در یک «بلوک» begin بپیچیم و استثنا را با یک بلوک rescue بگیریم. بلوک begin آغاز کدی را مشخص می‌کند که ممکن است استثنا ایجاد کند، و بلوک rescue استثنا را مدیریت می‌کند.

begin
 raise "This is an error"
rescue
 puts "An error occurred!"
end

بلوک rescue را می‌توان با یک متغیر هم مشخص کرد تا شیء استثنا را دریافت کنید.

begin
 raise "This is an error"
rescue ex
 puts "An error occurred: #{ex.message}"
end

بلوک rescue را می‌توان تنها با یک نوع استثنای مشخص هم تعیین کرد تا فقط همان استثنا گرفته شود.

begin
 raise ArgumentError.new("This is an argument error")
rescue ArgumentError
 puts "An argument error occurred!"
end

# or

begin
 raise ArgumentError.new("This is an argument error")
rescue ex : ArgumentError
 puts "An argument error occurred: #{ex.message}"
end

می‌توان از چند بلوک rescue برای مدیریت انواع مختلف استثناها استفاده کرد. در مثال زیر، بلوک rescue اول یک ArgumentError را می‌گیرد و بلوک rescue دوم هر استثنای دیگری را می‌گیرد.

begin
 raise ArgumentError.new("This is an argument error")
rescue ArgumentError
 puts "An argument error occurred!"
rescue
 puts "An error occurred!"
end

بلوک begin می‌تواند یک بلوک else هم داشته باشد که در صورت ایجاد نشدن استثنا اجرا می‌شود.

begin
 puts "No error occurred"
rescue
 puts "An error occurred!"
else
 puts "No error occurred"
end

در پایان، یک بلوک ensure هست که همیشه اجرا می‌شود، فارغ از اینکه استثنایی ایجاد شده باشد یا نه.

begin
 raise "This is an error"
rescue
 puts "An error occurred!"
ensure
 puts "This is always executed"
end

قرارداد متدها

برخی متدها دو نسخه دارند، یکی با ! و دیگری بدون آن. این می‌تواند دو معنای متفاوت داشته باشد. یکی اینکه متد شیء را تغییر می‌دهد، و دیگری اینکه متد می‌تواند استثنا ایجاد کند.

اما قرارداد دیگری هم هست که مربوط به پایان یافتن نام متد با ? است و در مفهوم «منطقی» به آن اشاره شده است. برخی متدها به‌طور پیش‌فرض استثنا ایجاد می‌کنند، اما نسخه‌ای هم دارند که با ? تمام می‌شود و به‌جای ایجاد استثنا nil برمی‌گرداند.

این زمانی ایده‌آل است که می‌خواهید از ایجاد یک خطا جلوگیری کنید. این می‌تواند به کارایی کمک کند، چون نیازی نیست «استک‌تریس» ساخته شود و اگر درست تنظیم شود، می‌تواند کد را امن‌تر کند.

استثناهای سفارشی

همچنین می‌توانید استثناهای خودتان را با ارث‌بری از کلاس Exception بسازید. با این کار، می‌توانید به‌صورت اختیاری متد initialize را بازنویسی کنید تا پیام استثنا را تعیین کنید. این کار را می‌توان با اختصاص دادن پیام به یک متغیر نمونه به اسم @message انجام داد.

class MyException < Exception
 def initialize
   @message = "This is my exception"
 end
end

raise MyException.new

دستورالعمل‌ها

روزی که مدت‌ها منتظرش بودید بالاخره از راه رسید و شما اکنون صاحب مفتخر یک مزرعه‌ی زیبا در آلپ هستید.

هنوز هم دوست ندارید صبح زود برای غذا دادن به گاوهایتان از خواب بیدار شوید. چون مهندس زبردستی هستید، یک غذارسان می‌سازید به اسم FEED-M-ALL.

آخرین موردی که برای تمام کردن پروژه‌تان لازم است، تکه‌کدی است که مقدار علوفه‌ای را محاسبه می‌کند که هر گاو باید دریافت کند. مهم است که هر گاو مقدار یکسانی دریافت کند؛ باید از بروز درگیری جلوگیری کنید. گاوها بسیار حساس هستند.

خوشبختانه لازم نیست خودتان همه‌ی فرمول‌های محاسبه‌ی مقدار علوفه را به دست آورید. شما از یک کتابخانه‌ی بیرونی مرموز استفاده می‌کنید که آن را در اینترنت پیدا کرده‌اید. قرار است به شادترین گاوها منجر شود. این کتابخانه کلاسی را در دسترس قرار می‌دهد که «رابط» زیر را برآورده می‌کند. در کدی که خودتان می‌نویسید به این تکیه خواهید کرد.

class FodderCalculator
  def fodder_amount! : Number 
  end

  def fattening_factor! : Number
  end
end

همان‌طور که روی کدتان کار می‌کنید، مدیریت خطا را بهبود می‌دهید تا مقاوم‌تر باشد و بعدها که در زندگی روزمره‌ی مزرعه از آن استفاده می‌کنید، دیباگ کردنش آسان‌تر شود.

1. تقسیم یکنواخت غذا

پیش از هر چیز، روی نوشتن کدی تمرکز می‌کنید که برای محاسبه‌ی مقدار علوفه‌ی هر گاو لازم است.

متدی به اسم TheFarm.divide_food پیاده‌سازی کنید که یک شیء FodderCalculator و تعداد گاوها به‌صورت یک عدد صحیح را به عنوان آرگومان می‌پذیرد. برای این کار، فرض می‌کنید تعداد گاوها همیشه بزرگ‌تر از صفر است. این متد باید مقدار غذا به ازای هر گاو را برگرداند.

برای انجام محاسبه، ابتدا باید مقدار کل علوفه‌ی همه‌ی گاوها را به دست آورید. این کار با فراخوانی متد fodder_amount! و پاس دادن تعداد گاوها انجام می‌شود. به‌علاوه، به ضریبی نیاز دارید که این مقدار باید در آن ضرب شود. این ضریب را با فراخوانی متد fattening_factor! به دست می‌آورید. با این دو مقدار و تعداد گاوها، اکنون می‌توانید مقدار غذا به ازای هر گاو را محاسبه کنید (به‌صورت float64).

اگر یکی از متدهایی که فراخوانی می‌کنید خطا برگرداند، اجرا باید متوقف شود و 0 برگردانده شود.

fodder_calculator = FodderCalculator.new(50, 1.5)
TheFarm.divide_food(fodder_calculator, 5)
# => 15.0

fodder_calculator = FodderCalculator.new(50, nil)
TheFarm.divide_food(fodder_calculator, 5)
# => 0

2. بررسی تعداد گاوها

در حین کار روی تمرین اول، متوجه شدید که کتابخانه‌ی بیرونی‌ای که استفاده می‌کنید آن‌قدر که فکر می‌کردید باکیفیت نیست. برای نمونه، نمی‌تواند ورودی‌های نامعتبر را به‌درستی مدیریت کند. می‌خواهید این محدودیت را با افزودن یک بررسی برای مقدار ورودی در کد خودتان دور بزنید.

تابعی به اسم TheFarm.validate_input_and_divide_food بنویسید که امضای آن با TheFarm.divide_food بالا یکسان است.

  • اگر تعداد گاوهای داده‌شده بزرگ‌تر از ۰ باشد، تابع باید TheFarm.divide_food را فراخوانی کند و نتیجه‌ی آن فراخوانی را برگرداند.
  • اگر تعداد گاوها ۰ یا کمتر باشد، تابع باید خطایی با پیام "Number of cows must be greater than 0" ایجاد کند.
TheFarm.validate_input_and_divide_food(fodderCalculator, 5)
# => 15

TheFarm.validate_input_and_divide_food(fodderCalculator, -2)
# Error: Number of cows must be greater than 0

3. بهبود مدیریت خطا

بررسی تعداد گاوها پیش از فرستادنش کار خوبی بود، اما از پیام خطای نامشخص چندان راضی نیستید. تصمیم می‌گیرید بهتر عمل کنید و یک کلاس خطای سفارشی به اسم InvalidCowsError بسازید.

خطای سفارشی باید تعداد گاوها (Int32) و یک پیام سفارشی (String) را نگه دارد و متد Expection باید داده‌ها را با قالب زیر سریالایز کند:

{number of cows} cows are invalid: {custom message}

با مجهز بودن به خطای سفارشی خود، تابعی به اسم TheFarm.validate_number_of_cows پیاده‌سازی کنید که تعداد گاوها را به‌صورت یک عدد صحیح می‌گیرد و یک خطا (یا nil) برمی‌گرداند.

  • اگر تعداد گاوها کمتر از ۰ باشد، تابع یک InvalidCowsError برمی‌گرداند که پیام سفارشی آن "there are no negative cows" تنظیم شده است.
  • اگر تعداد گاوها ۰ باشد، تابع یک InvalidCowsError برمی‌گرداند که پیام سفارشی آن "no cows don't need food" تنظیم شده است.
  • در غیر این صورت، تابع nil را برمی‌گرداند تا نشان دهد اعتبارسنجی موفق بوده است.
TheFarm.validate_number_of_cows(5)
# => nil

TheFarm.validate_number_of_cows(-5)
# => InvalidCowsError: -5 cows are invalid: there are no negative cows

پس از زحمت راه‌اندازی این تابع اعتبارسنجی، متوجه می‌شوید که دیگر غروب شده است و میزتان را ترک می‌کنید تا از تماشای غروب خورشید بر فراز کوه‌ها لذت ببرید. کار افزودن واقعی تابع اعتبارسنجی جدید به کدتان را به روز دیگری موکول می‌کنید.

ویرایش از طریق GitHub این لینک در پنجره یا زبانه‌ی جدیدی باز می‌شود
Crystal Exercism

آماده‌اید مزرعه را شروع کنید؟

در Exercism ثبت‌نام کنید تا Crystal را همراه با 26 مفهوم142 تمرین و مربی‌گری انسانی واقعی یاد بگیرید و در آن استاد شوید، همه‌ی این‌ها رایگان.