مقدمه
در دو درس گذشته، ما یک برنامه خط فرمان ساده ساختیم که آرگومانها را خوانده و محتوای یک فایل را
چاپ میکند. کد ما کار میکند، اما ساختار آن برای یک پروژه واقعی ایدهآل نیست. در حال حاضر، تمام
منطق برنامه در تابع main متمرکز شده است. با رشد برنامه، این کار باعث میشود که تابع
main بسیار بزرگ و درک آن دشوار شود.
مشکلات اصلی کد فعلی عبارتند از:
- متغیرهای پیکربندی (query و file_path) به صورت مجزا تعریف شدهاند و ارتباط
منطقی آنها مشخص نیست.
- تابع main چندین مسئولیت دارد: هم آرگومانها را پردازش میکند و هم منطق اصلی برنامه را
اجرا میکند.
- مدیریت خطا ضعیف است. ما از expect استفاده کردهایم که در صورت بروز خطا، برنامه را
panic میکند. این یک تجربه کاربری خوب برای یک ابزار CLI نیست.
در این درس، ما با «بازسازی» یا refactoring کد، این مشکلات را قدم به قدم حل خواهیم کرد.
گروهبندی متغیرهای پیکربندی با struct
اولین قدم، گروهبندی متغیرهای پیکربندی در یک ساختار واحد است. این کار باعث میشود که کد ما
گویاتر شود. ما یک struct به نام Config میسازیم که این دو مقدار را در خود نگه دارد.
یک سازنده برای Config
به جای پردازش آرگومانها در main، ما یک تابع سازنده برای Config خود میسازیم. این تابع،
آرگومانها را گرفته و یک نمونه از Config را برمیگرداند. مهمتر از آن، به جای panic کردن،
این تابع یک Result برمیگرداند تا مدیریت خطا به صورت کنترلشده انجام شود.
src/main.rs
struct Config {
query: String,
file_path: String,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
تابع build() یک اسلایس از آرگومانها را دریافت میکند. ابتدا بررسی
میکند که آیا تعداد آرگومانها کافی است یا خیر. اگر نباشد، یک Err با یک پیام خطا برمیگرداند.
در غیر این صورت، مقادیر را clone کرده (تا مالکیت آنها را به دست آورد) و یک نمونه Ok(Config)
را برمیگرداند.
اصلاح تابع main
اکنون که منطق پردازش آرگومانها را جدا کردهایم، تابع main بسیار سادهتر و خواناتر
میشود. مسئولیت اصلی آن هماهنگی بین بخشهای مختلف برنامه است.
src/main.rs
use std::{env, process};
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
println!("Searching for '{}'", config.query);
println!("In file '{}'", config.file_path);
}
در اینجا، ما از متد unwrap_or_else() روی Result بازگشتی از Config::build() استفاده میکنیم. این متد به ما اجازه میدهد تا در
صورت بروز خطا (Err)، یک closure را اجرا کنیم. در این closure، ما یک پیام خطای کاربرپسند
چاپ کرده و با استفاده از process::exit()، برنامه را با یک کد وضعیت
خطا متوقف میکنیم. این روش بسیار بهتر از panic کردن است.
استخراج منطق اصلی به تابع run
آخرین مرحله، استخراج منطق اصلی برنامه (خواندن فایل) به یک تابع جداگانه به نام run است.
این تابع، Config را به عنوان ورودی دریافت کرده و مسئول اجرای وظیفه اصلی برنامه است. این تابع
نیز باید یک Result برگرداند تا خطاهای مربوط به خواندن فایل را مدیریت کند.
src/main.rs
use std::error::Error;
fn main() {
if let Err(e) = run(config) {
println!("Application error: {e}");
process::exit(1);
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
println!("With text:\n{contents}");
Ok(())
}
اکنون تابع main ما بسیار تمیز است. وظیفه آن تنها هماهنگی است. تابع run منطق اصلی
را در خود دارد. نوع بازگشتی Result<(), Box<dyn Error>> یک الگوی رایج در Rust است. () یک نوع
تاپل خالی است که نشان میدهد در صورت موفقیت، هیچ مقداری برنمیگردانیم. Box<dyn Error> یک
«trait object» است که به ما اجازه میدهد هر نوع خطایی را که Error را پیادهسازی
کرده، برگردانیم. استفاده از اپراتور ? در انتهای fs::read_to_string()، به صورت خودکار خطاها را به کد
فراخواننده (یعنی main) منتشر میکند.
در این درس با بازسازی کد، برنامه خود را به یک ساختار ماژولار، قوی و قابل نگهداری تبدیل کردیم. با
جداسازی مسئولیتها، اکنون هر بخش از کد ما یک وظیفه مشخص دارد و مدیریت خطاها نیز به صورت
کنترلشده انجام میشود. حالا که یک پایه محکم داریم، میتوانیم با اطمینان بیشتری قابلیتهای جدید
را به آن اضافه کنیم. در درس بعدی، با رویکرد «توسعهی تستمحور یا TDD»، یاد میگیریم که چگونه
ابتدا تستهای خود را بنویسیم و سپس کدی بنویسیم که آن تستها را پاس کند، که این کار به نوشتن
کدهای صحیحتر و قابل اعتمادتر کمک میکند.