نوشتن کد تمیز و خوانا، یکی از دغدغه های توسعه دهنده (ارشد یا تازه کار) می باشد. برنامه نویسان ارشد، موفق می شوند با تکیه بر تجربه و همچنین مطالعاتشان، راحت تر این کار را انجام دهند و برنامه نویسان تازه کار، در حین کسب تجربه، تمام تلاششان را می کنند. در سراسر دنیا این موضوع اهمیت بالایی دارد و هر سال مقاله های مختلف توسط حرفه ای ها و حتی تازه کار های پرتلاش، در این مورد نوشته می شود.
در این مقاله نکاتی را برای نوشتن کد تمیزتر در لاراول جمع آوری کردیم و قصد داریم برخی از توصیه های کلی در کد نویسی لاراول را برای علاقه مندان ذکر کنیم. این نکته ها، نقطه شروع عالی برای ایجاد فهم صحیح از کد خوب و کد بد هستند. در نظر داشته باشید که ما بدون در نظر گرفتن ترتیب خاصی، این نکته ها را به همراه نمونه کد ذکر می کنیم و هر کدام در جای خود دارای اهمیت می باشند.
1. استفاده از جداول جستجو (lookup table)
به جای نوشتن عبارت های تکراری else if، بهتر است از یک آرایه استفاده کنیم و بعد مقدار مورد نظر را براساس کلید موجود، جستجو نماییم. در این حالت کد تمیزتر و خواناتر می شود و در صورت بروز اشتباه، موارد استثنا برای ما قابل فهم می شوند.
// Bad
if ($order->product->option->type === 'pdf') {
$type = 'book';
} else if ($order->product->option->type === 'epub') {
$type = 'book';
} else if ($order->product->option->type === 'license') {
$type = 'license';
} else if ($order->product->option->type === 'artwork') {
$type = 'creative';
} else if $order->product->option->type === 'song') {
$type = 'creative';
} else if ($order->product->option->type === 'physical') {
$type = 'physical';
}
if ($type === 'book') {
$downloadable = true;
} else if ($type === 'license') {
$downloadable = true;
} else if $type === 'creative') {
$downloadable = true;
} else if ($type === 'physical') {
$downloadable = false;
}
// Good
$type = [
'pdf' => 'book',
'epub' => 'book',
'license' => 'license',
'artwork' => 'creative',
'song' => 'creative',
'physical' => 'physical',
][$order->product->option->type];
$downloadable = [
'book' => true,
'license' => true,
'creative' => true,
'physical' => false,
][$type];
در لاراول 8 عبارت match معرفی شد که شما می توانید مقدار را از طریق تخصیص و یا بازگشت بدون نیاز به اختصاص به یک متغیر محلی دریافت کنید:
// Before
switch ($this->lexer->lookahead['type']) {
case Lexer::T_SELECT:
$statement = $this->SelectStatement();
break;
case Lexer::T_UPDATE:
$statement = $this->UpdateStatement();
break;
case Lexer::T_DELETE:
$statement = $this->DeleteStatement();
break;
default:
$this->syntaxError('SELECT, UPDATE or DELETE');
break;
}
// After
$statement = match ($this->lexer->lookahead['type']) {
Lexer::T_SELECT => $this->SelectStatement(),
Lexer::T_UPDATE => $this->UpdateStatement(),
Lexer::T_DELETE => $this->DeleteStatement(),
default => $this->syntaxError('SELECT, UPDATE or DELETE'),
};
همچنین می توانید چند match را با استفاده از کاما، با هم ترکیب کنید:
echo match ($x) {
1, 2 => 'Same for 1 and 2',
3, 4 => 'Same for 3 and 4',
};
2. بازگشت زود هنگام (Return early)
سعی کنید با بازگرداندن زود هنگام مقدار، از تو در تو شدن های غیر ضروری جلوگیری کنید. عبارت های بیش از حد تو در تو مانند if و else های زیاد باعث می شوند کد سخت تر خوانده شود.
// Bad
if ($notificationSent) {
$notify = false;
} else if ($isActive) {
if ($total > 100) {
$notify = true;
} else {
$notify = false;
} else {
if ($canceled) {
$notify = true;
} else {
$notify = false;
}
}
}
// Good
if ($notificationSent) {
return false;
}
if ($isActive && $total > 100) {
return true;
}
if (! $isActive && $canceled) {
return true;
}
return false;
3. جدا کردن خط ها به درستی
خط های کد را به طور تصادفی جدا نکنید، از طرفی باید دقت کنیم که بیش از حد هم طولانی نشوند. برای مثال باز کردن یک آرایه با کاراکتر [ و تورفتگی مقادیر داخل آن مکان بسیار مناسبی برای جدا کردن خط ها است. مقدار پارامتر های یک تابع طولانی نیز به همین صورت است و باید به خط بعد منتقل شوند. مکان های خوب دیگر برای جدا کردن خط ها، فراخوانی ها و closure ها می باشد.
// Bad
// No line split
return $this->request->session()->get($this->config->get('analytics.campaign_session_key'));
// Meaningless line split
return $this->request
->session()->get($this->config->get('analytics.campaign_session_key'));
// Good
return $this->request->session()->get(
$this->config->get('analytics.campaign_session_key')
);
// Closure
new EventCollection($this->events->map(function (Event $event) {
return new Entries\Event($event->code, $event->pivot->data);
}));
// Array
$this->validate($request, [
'code' => 'string|required',
'name' => 'string|required',
]);
4. ایجاد نکردن متغیرهای بی فایده
وقتی می توانید مقدار را به صورت مستقیم پاس دهید، متغیر ایجاد نکنید.
// Bad
public function create()
{
$data = [
'resource' => 'campaign',
'generatedCode' => Str::random(8),
];
return $this->inertia('Resource/Create', $data);
}
// Good
public function create()
{
return $this->inertia('Resource/Create', [
'resource' => 'campaign',
'generatedCode' => Str::random(8),
]);
}
5. ایجاد متغیرها زمانی که خوانایی کد را بهبود می دهند.
این مورد بر عکس نکته ی قبلی است. گاهی یک مقدار از یک فراخوانی پیچیده حاصل می شود به این صورت که ایجاد یک متغیر باعث بهبود خوانایی و از بین رفتن نیاز به کامنت می شود. به یاد داشته باشید که ایجاد متغیر در جای درست، موضوع مهمی است و هدف نهایی شما بالا بردن قابلیت خواندن کد است.
// Bad
Visit::create([
'url' => $visit->url,
'referer' => $visit->referer,
'user_id' => $visit->userId,
'ip' => $visit->ip,
'timestamp' => $visit->timestamp,
])->conversion_goals()->attach($conversionData);
// Good
$visit = Visit::create([
'url' => $visit->url,
'referer' => $visit->referer,
'user_id' => $visit->userId,
'ip' => $visit->ip,
'timestamp' => $visit->timestamp,
]);
$visit->conversion_goals()->attach($conversionData);
6. ایجاد متد های مدل برای منطق کسب و کار (business logic)
کنترلر های شما باید ساده باشند. آن ها باید مواردی مانند "ایجاد فاکتور برای سفارش" را بیان کنند. نباید نگران جزئیات ساختار پایگاه داده شما باشند. این کار را به مدل بسپارید.
// Create invoice for order
DB::transaction(function () use ($order) {
Sinvoice = $order->invoice()->create();
$order—>pushStatus(new AwaitingShipping);
return $invoice;
});
// Good
$order->createInvoice();
7. ایجاد کلاس های Action
بیایید به مثال قبلی بپردازیم. گاهی اوقات، ایجاد یک کلاس برای یک عمل می تواند همه چیز را تمیز کند. مدل ها باید منطق کسب و کار مربوط به آن ها را در بر بگیرند، اما نباید خیلی بزرگ باشند.
// Bad
public function createInvoice(): Invoice
{
if ($this->invoice()->exists()) {
throw new OrderAlreadyHasAnInvoice('Order already has an invoice.');
}
return DB::transaction(function () use ($order) {
$invoice = $order->invoice()->create();
$order->pushStatus(new AwaitingShipping);
return $invoice;
});
}
// Good
// Order model
public function createInvoice(): Invoice {
if ($this->invoice()->exists()) {
throw new OrderAlreadyHasAnInvoice('Order already has an invoice.');
}
return app(CreateInvoiceForOrder::class)($this);
}
// Action class
class CreatelnvoiceForOrder
{
public function _invoke(Order $order): Invoice
{
return DB::transaction(function () use ($order) {
$invoice = $order->invoice()->create();
$order->pushStatus(new AwaitingShipping);
return $invoice;
});
}
}
8. استفاده از Form request ها
استفاده از form request ها را در نظر بگیرید. آن ها مکانی عالی برای پنهان کردن منطق پیچیده ی اعتبار سنجی هستند. البته گفته می شود وقتی منطق اعتبار سنجی شما ساده باشد، انجام این کار در کنترلر مشکلی ندارد و انتقال آن به یک form request، پیچیدگی آن را بیشتر می کند. (ولی خودم ترجیح می دهم در یک پروژه، از یک ساختار استفاده کنم.)
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'title' => 'required|unique:posts|max:255',
'body' => 'required',
];
}
9. استفاده از event ها
برخی از منطق ها را از کنترلر ها به رویدادها منتقل کنید. به عنوان مثال، مزیت آن در هنگام ایجاد مدل ها این است که ایجاد این مدل ها در همه جا به یک شکل کار می کند (کنترلر ها، job ها و ...) و کنترلر نگرانی کمتری در مورد جزئیات شمای دیتابیس دارد.
// Bad
// Only works in this place & concerns it with
// details that the model should care about.
if (! isset($data['name'])) {
$data['name'] = $data['code'];
}
$conversion = Conversion::create($data);
// Good
$conversion = Conversion::create($data);
// Model
class ConversionGoal extends Model
{
public static function booted()
{
static::creating(function (self $model) {
$model->name ??= $model->code;
});
}
}
10. استخراج متد از کد های پیچیده
اگر متدی بیش از حد طولانی یا پیچیده است و درک این که دقیق چه اتفاقی می افتد دشوار است، در این حالت منطق موجود در متد را به چند متد (یا بهتر است بگوییم روش) تقسیم کنید.
public function handle(Request $request, Closure $next)
{
// We extracted 3 tong methods into separate methods.
$this->trackVisitor();
$this->trackCampaign();
$this->trackTrafficSource($request);
$response = $next($request);
$this->analytics->log($request);
return $response;
}
11. ایجاد توابع کمکی (helper functions)
اگر بخش هایی از کد را زیاد تکرار میکنید، در نظر بگیرید که آیا استخراج کردن آن در یک تابع کمکی باعث تمیز تر شدن کد میشود یا خیر.
// app/helpers.php, autoloaded in composer.json
function money(int $amount, string $currency = null): Money
{
return new Money($amount, $currency ?? config('shop.base_currency'));
}
function html2text($html = ''): string
{
return str_replace(' ', ' ', strip_tags($html));
}
12. خودداری کردن از کلاس های کمکی (helper classes)
گاهی اوقات افراد تابع های کمکی را در یک کلاس قرار می دهند. انجام این کار می تواند باعث کثیف شدن کد شود. این کلاسی است که فقط از متدهای استاتیک به عنوان توابع کمکی استفاده می کند. بهتر است این تابع ها را در کلاس هایی با منطق مرتبط، قرار دهید یا فقط آن ها را به عنوان توابع Global نگه داری کنید.
// Bad
class Helper
{
public function convertCurrency(Money $money, string $currency): self
{
$currencyConfig = config("shop.currencies.$currency");
$decimalDiff = ...
return new static(
(int) round($money->baseValue() * $currencyConfig[value] * 10**$decimalDiff, 0),
$currency
);
}
}
// Usage
use App\Helper;
Helper::convertCurrency($total, 'EUR');
// Good
class Money
{
// the other money/currency logic
public function convertTo(string $currency): self
{
$currencyConfig = config("shop.currencies.$currency");
$decimalDiff = ...
return new static(
(int) round($this->baseValue() * $currencyConfig[value * 10**$decimalDiff, 0),
$currency
);
}
}
// Usage
$EURtotal = $total->convertTo('EUR');
13. از پارامترهای بیش از حد در توابع اجتناب کنید.
وقتی تابعی را با مقدار زیادی پارامتر می بینید، می تواند به این معنی باشد:
- متد مسئولیت های بیش از اندازه دارد و باید جدا شوند.
- مسئولیت ها خوب است، اما باید موارد طولانی را اصلاح کنید.
در زیر دو روش برای رفع مورد دوم آورده شده است.
14. استفاده از Data Transfer Object (DTOs)
به جای ارسال مقدار زیادی از آرگومان ها در یک ترتیب خاص، ایجاد یک شی با ویژگی ها برای ذخیره ی این داده ها را در نظر بگیرید. اگر متوجه شوید که چه رفتارهایی را می توان به این شیء منتقل کرد، برای شما امتیاز به حساب می آید.
// Bad
public function log($url, $route_name, $route_data, $campaign_code, $traffic_source, $referer, $user_id, $visitor_id, $ip, $timestamp)
{
// ...
}
// Good
public function log(Visit $visit)
{
// ...
}
class Visit
{
public string $url;
public ?string $routeName;
public array $routeData;
public ?string $campaign;
public array $trafficSource[];
public ?string $referer;
public ?string $userId;
public string $visitorId;
public ?string $ip;
public Carbon $timestamp;
// ...
}
15. ایجاد اشیاء Fluent (fluent objects)
شما می توانید اشیاء را با API های fluent ایجاد کنید. به تدریج داده ها را با فراخوانی های جداگانه اضافه کنید و فقط به حداقل داده در constructor نیاز دارید. هر متد، $this را برمی گرداند، بنابراین می توانید در هر فراخوانی، توقف کنید.
Visit::make($url, $routeName, $routeData)
->withCampaign($campaign)
->withTrafficSource($trafficSource)
->withReferer($referer)
// ... etc
16. ایجاد کلاس های شخصی سازی شده برای collection ها
ایجاد مجموعه های سفارشی می تواند راهی عالی برای دستیابی به syntax خواناتر باشد. این مثال را با product collection در نظر بگیرید.
// Bad
$total = $order->products->sum(function (OrderProduct $product) {
return $product->price * $product->quantity * (1 + $product->vat_rate);
});
// Good
$order->products->total();
class OrderProductCollection extends Collection
{
public function total()
{
$this->sum(function (OrderProduct $product) {
return $product->price * $product->quantity * (1 + $product->vat_rate);
});
}
}
17. سعی کنید فقط از اکشن های CRUD استفاده کنید.
اگر می توانید، فقط از 7 عمل CRUD در کنترلرهای خود استفاده کنید. حتی کمتر. یک کنترلر با 20 متد ایجاد نکنید. کنترلرهای کوتاه تر بهتر است.
18. یک آخر هفته را به یادگیری اصول برنامه نویسی شی گرا اختصاص دهید.
تفاوت بین متد ها و متغیرهای static/instance و قابلیت private/protected/public را یاد بگیرید. همچنین یاد بگیرید که لاراول چگونه از متد های جادویی (Magic) استفاده می کند. یک توسعه دهنده ی مبتدی به این نیاز ندارد، اما همان طور که کد رشد می کند، این نکته بسیار مهم خواهد بود.
19. در کلاس ها فقط کد procedural ننویسید.
OOP برای خوانایی بیشتر کد شما وجود دارد، از آن استفاده کنید. در اکشن های کنترلر فقط کد رویه ای 400 خطی ننویسید.
20. مواردی مانند Single Responsibility Principle (SRP) را بخوانید و آن ها را تا حد معقول دنبال کنید.
از داشتن کلاس هایی که به بسیاری از موارد نامرتبط می پردازند خودداری کنید. برای هر چیز یک کلاس ایجاد نکنید. به بیان ساده، با توجه به اصل SRP، بهتر است برای متد هایی که به یک موضوع می پردازند و به هم مرتبط هستند، کلاس جداگانه در نظر بگیریم و از طرف دیگر، فراموش نکنیم که نباید برای هر متد، کلاس جداگانه بسازیم.
21. استفاده از نام های پر معنی و اشاره کننده برای متد ها
به جای اینکه فکر کنید «این شی چه کاری می تواند انجام دهد»، به این فکر کنید که «با این شی چه کاری می توان انجام داد». استثنائاتی مانند کلاس های اکشن اعمال می شود، اما این یک قانون کلی خوب است.
// Bad
$gardener->water($plant);
$orderManager->lock($order);
// Good
$plant->water();
$order->lock();
22. استفاده از trait
اضافه کردن متدها به کلاس هایی که به آن ها تعلق دارند تمیزتر از ایجاد کلاس های action برای همه چیز است، اما میتواند کلاس ها را بزرگ کند. استفاده از trait ها را در نظر بگیرید. آن ها در درجه ی اول برای استفاده ی مجدد از کد هستند، و هیچ مشکلی با ویژگی های single-user وجود ندارد.
class Order extends Model
{
use HasStatuses;
// ...
}
trait HasStatuses
{
public static function bootHasStatuses() { ... }
public static $statusMap = [ ... ];
public static $paymentStatusMap = [ ... ];
public static function getStatusId($status) { ... }
public static function getPaymentStatusId($status): string { ... }
public function statuses() { ... }
public function payment_statuses() { ... }
public function getStatusAttribute(): OrderStatusModel { ... }
public function getPaymentStatusAttribute(): OrderPaymentStatus { ... }
public function pushStatus($status, string $message = null, bool $notification = null) { ... }
public function pushPaymentStatus($status, string $note = null) { ... }
public function status(): OrderStatus { ... }
public function paymentStatus(): PaymentStatus { ... }
}
23. پیروی از قانون های نام گذاری
- نام کنترلر باید با یک اسم (به شکل مفرد) و سپس کلمه “Controller” شروع شود.
- نام مدل ها باید به صورت مفرد و حرف اول آن بزرگ باشد.
- نام فایل های migration باید از الگوی های زیر پیروی کنند:
- نام Property های مدل باید به صورت snake_case باشد.
- نام ستون های جدول باید snake_case بدون نام مدل باشد.
- متدهای رابطه های hasOne یا belongsTo باید به صورت مفرد باشند.
- متد ها باید camelCase باشد.
- مسیرها باید به شکل جمع از منبعی باشند که میخواهد دستکاری کند و همه باید با حروف کوچک (lower-case) باشند.
24. استفاده از Resource Controller ها
شما باید از Resource Controller ها استفاده کنید مگر این که دلیل خاصی برای این کار نداشته باشید. Resource Controller های لاراول، مسیرهای CRUD را در یک خط کد به کنترلر ارائه می کنند. یک resource controller برای ایجاد یک کنترلر استفاده می شود که تمام درخواست های http ذخیره شده توسط برنامه ی شما را مدیریت می کند.
25. به جای استفاده از نام مستعار، namespace ها را وارد کنید.
گاهی ممکن است چند کلاس با یک نام داشته باشید. به جای وارد کردن آن ها با نام مستعار، namespace ها را وارد کنید.
// Bad
use App\Types\Entries\Visit as VisitEntry;
use App\Storage\Database\Models\Visit as VisitModel;
class DatabaseStorage
{
public function log(VisitEntry $visit)
{
$visitModel = VisitModel::create([
// ...
]);
}
}
// Good
use App\Types\Entries;
use App\Storage\Database\Models;
class DatabaseStorage
{
public function log(Entries\Visit $visit)
{
$visitModel = Models\Visit::create([
// ...
]);
}
}
هر کدام از نکته های گفته شده، اگر در جای مناسب استفاده شود، باعث خواهد شد کدی تمیزتر و خواناتر داشته باشیم. همچنین درک کامل و به کارگیری آن ها، شما را به یک توسعه دهنده ی بهتر تبدیل میکند. اما نکته های کد نویسی تمیز، فقط به همین موارد محدود نمی شود و نکته های بیشتر را در بخش دوم این مقاله، برایتان شرح دادهایم.