فریدون سالمی
فریدون سالمی
خواندن ۲ دقیقه·۲ سال پیش

اصول سالید در php


سالید چیست ؟

به عنوان یک مهندس نرم افزار،ما همیشه به تغییرات یا اضافه کردن ویژگی های جدید به نرم افزار در میانه فاز توسعه، یا نگهداری نیازمندیم.

بسیار مهم است که کدی بنویسیم که گسترش و نگهداری آن آسان باشد. SOLIDمجموعه قوانینی است که در مورد چگونگی رسیدن به این مهم در برنامه نویسی شی گرا چراغ راه ماست. با استفاده از دستورالعمل های SOLIDمی توانید از طراحی بد در کد خود جلوگیری کنید و یک معماری ساختار یافته به طراحی خود بدهید. طراحی بد منجر به ایجاد کدی غیرقابل انعطاف می شود، به طوری که یک تغییر ساده و کوچک می تواند کل سیستم را تحت تأثیر قرار دهد.

SOLID مخفف شده پنج قانون از مجموعه قوانینی است که در سال 2000 توسط رابرت سی مارتین با نام مستعار "عمو باب" برای طراحی سیستم های نرم افزاری منسجم و با قابلیت توسعه پذیری و نگداری بالا پیشنهاد شد.

در زیر پنج مفهومی که اصول SOLID را تشکیل می‌دهند، آمده است:

1. Single Responsibility principle

2. Open/Closed principle

3. Liskov Substitution principle

4. Interface Segregation principle

5. Dependency Inversion principle.

خب بیاین هر کدام از این اصول را با جزئیات در کد ببینیم

اصل اول : اصل مسئولیت واحد همانطور که از نام آن پیداست، هدف این اصل داشتن یک مسئولیت واحد برای یک کلاس/ماژول است. به عبارت دیگر، می توان گفت که کلاس یا ماژول باید تنها یک مشکل را حل کند، بنابراین باید یک دلیل واحد برای تغییر داشته باشد. این کار کد ما را منسجم تر و قابلیت نگهداری و تست آن را آسان تر می کند.

"یک کلاس فقط باید یک مسئولیت داشته باشد، یعنی فقط تغییرات در یک قسمت از مشخصات نرم افزار باید بتواند روی مشخصات کلاس تاثیر بگذارد."

بیایید با یک مثال ساده درک کنیم:

class Logger{
private $logs = [];

public function add($log){
$now = new DateTime();
$date = $now->format("Y-m-d h:i:s.u");
$this->logs[] = $date." : ".$log;
}
public function toString($dimiliter=", "){
if(empty($this->logs)){
return "No logs";
}
return implode($this->logs,$dimiliter);

}
public function reset(){
$this->logger=[];
}public function save($fileName){
$fp = fopen($fileName,"w");
fwrite($fp,$this->toString("\n"));
fclose($fp);
}

}
$logger = new Logger();
$logger->add("First log");
$logger->add("Second log");
$logger->add("Third log");

$logger->save("logs.txt");

در کد مثال بالا، ما یک کلاس Logger ساده داریم که لاگ ها را جمع آوری کرده و در یک فایل به اسم logs.txt ذخیره می کند. همه چیز خوب به نظر می رسد اما کلاس فوق اصل مسئولیت واحد را زیر پا می گذارد زیرا کلاس Logger دو مسئولیت دارد: جمع آوری لاگ های مربوطه و ذخیره سازی آنها

خب بیاید روش پیاده سازی درست را ببینیم

<?php class Logger{ private $logs = []; public function add($log){ $now = new DateTime(); $date = $now->format(&quotY-m-d h:i:s.u&quot); $this->logs[] = $date.&quot : &quot.$log; } public function toString($dimiliter=&quot, &quot){ if(empty($this->logs)){ return &quotNo logs" } return implode($this->logs,$dimiliter); } public function reset(){ $this->logger=[]; }public function save($fileName){ $fp = fopen($fileName,&quotw&quot); fwrite($fp,$this->toString(&quot\n&quot)); fclose($fp); } }class LogStorage{ private $fileName; public function __construct($fileName){ $this->fileName = $fileName; }public function save($text){ $fp = fopen($this->fileName,&quotw&quot); fwrite($fp,$text); fclose($fp); } }$logger = new Logger(); $logger->add(&quotFirst log&quot); $logger->add(&quotSecond log&quot); $logger->add(&quotThird log&quot);$logStorage = new LogStorage(&quotpfile.txt&quot); $logStorage->save($logger->toString(&quot\n&quot));

بنابراین ما کلاس لاگر خود را به دو کلاس مختلف تقسیم کردیم که یکی Logger و دیگری LogStorage است و اکنون هر کلاس فقط یک مسئولیت دارد. کلاس Logger فقط مسئول جمع آوری گزارش ها و کلاس LogStorage مسئول ذخیره گزارش ها در فایل است.

اصل باز/بستن : این اصل می گوید کلاسی که تست شده و به درستی کار می کند نباید با اضافه کردن ویژگی جدید دچار اخلال شود. تغییر کلاس موجود برای ایجاد یک ویژگی جدید ، ممکن است یک باگ جدید ایجاد کند. بنابراین به جای تغییر یک کلاس/اینترفیس موجود، باید آن کلاس/اینترفیس برای ایجاد ویژگی های جدید از یک کلاس/اینترفیس جدید ارث بری کند.

" کلاس / اینترفیس ... باید برای توسعه باز باشند، اما برای اصلاح بسته باشد."

بیایید با یک مثال ساده درک کنیم:

<?php class SavingAccount { private $balance; public function setBalance($balance){} public function getBalance(){} public function withdrawal(){}} class FixedDipositAccount() { private $balance; private $maturityPeriod; public function setBalance($balance){} public function getBalance(){} } class IntrestCalculator { public function calculate($account) { if ($account instanceof SavingAccount) { return $account->getBalance*3.0; } elseif ($member instanceof FixDipositAccount) { return $account->getBalance*9.5; } throw new Exception('Invalid input member'); } }$savingAccount = new SavingAccount(); $savingAccount->setBalance(15000); $fdAccount = new FixedDipositAccount(); $fdAccount->setBalance(25000);$intrestCalculator = new IntrestCalculator(); echo $intrestCalculator->calculate($savingAccount); echo $intrestCalculator->calculate($fdAccount);

در مثال بالا، ما دو کلاس ساده SavingAccount و FixedDepositAccount و یک کلاس IntrestCalculator داریم که سود را بر اساس نوع شی ارائه شده محاسبه می کند. اما در اینجا ما یک مشکل داریم. کلاس IntrestCalculator ما برای تغییرات بسته نیست. هر گاه که ما نیاز به افزودن نوع جدیدی از حساب داشته باشیم، باید کلاس IntrestCalculator خود را تغییر دهیم تا از نوع حساب جدید معرفی شده در سیستم پشتیبانی کند. بنابر این مثال بالا Open/Closed principle را نقض می کند.

خب بیاید روش پیاده سازی درست را ببینیم

<?php interface Account { public function calculateInterest(); } class SavingAccount implements Account { private $balance; private $rate=3.0; private $maturityPeriod;public function setBalance($balance){} public function getBalance(){} public function withdrawal(){} public function calculateIntrest(){ $this->$rate*$this->balance; } } class FixedDipositAccount implements Account { private $balance; private $rate =9.5; public function setBalance($balance){} public function getBalance(){} public function calculateIntrest(){ $this->$rate*$this->balance; } }class IntrestCalculator { public function calculate(Account $account) { return $account->calculateIntrest(); } }$savingAccount = new SavingAccount(); $savingAccount->setBalance(15000); $fdAccount = new FixedDipositAccount(); $fdAccount->setBalance(25000);$intrestCalculator = new IntrestCalculator(); echo $intrestCalculator->calculate($savingAccount); echo $intrestCalculator->calculate($fdAccount);

اصل جایگزینی لیسکوف : این اصل از نام باربارا لیسکوف گرفته شده است. او این اصل را در سال 1987 معرفی کرد.

این اصل بیان می کند که یک کلاس فرزند باید بدون تغییر رفتار قابل جایگزینی با کلاس پدر (کلاسی که از آن ارث بری شده است) باشد. این یک تعریف خاص از رابطه زیرمجموعه است که به آن زیرگروه سازی رفتاری گفته می شود.

"یک شی از کلاس اصلی در یک برنامه باید بتواند با نمونه هایی از کلاس هایی که از کلاس اصلی ارث بری کرده اند بدون تغییر در صحت آن برنامه قابل تعویض باشند."

بیایید با یک مثال بسیار ابتدایی درک کنیم.

Class A { public function doSomething(){ }} Class B extends A { }

در مثال بالا دو کلاس ساده A و B داریم که کلاس B از کلاس A ارث بری می کند. بنابراین طبق اصل جایگزینی Liskov هر جا که از کلاس A در برنامه خود استفاده می کنیم، باید بتوانیم آن را با کلاس B جایگزین کنیم.

حالا بیایید به مسئله مربع مستطیل کلاسیک نگاه کنیم:

<?php // The Rectangle Square problem class Rectangle { protected $width; protected $height; public function setHeight($height) { $this->height = $height; } public function getHeight() { return $this->height; } public function setWidth($width) { $this->width = $width; } public function getWidth() { return $this->width; } public function area() { return $this->height * $this->width; } } class Square extends Rectangle { public function setHeight($value) { $this->width = $value; $this->height = $value; } public function setWidth($value) { $this->width = $value; $this->height = $value; } } class AreaTester { private $rectangle; public function __construct(Rectangle $rectangle) { $this->rectangle = $rectangle; } public function testArea($width,$height) { $this->rectangle->setHeight($width); $this->rectangle->setWidth($height); return $this->rectangle->area(); } }$rectangle = new Rectangle(); $rectangleTest = new AreaTester($rectangle);$rectangleTest->testArea(2,3); // gives 6 as expecated$squre = new Square(); $rectangleTest = new AreaTester($squre);$rectangleTest->testArea(2,3); // gives 9 expecated is 6

در مثال بالا، کلاس Rectangle از کلاس Square ارث بری می کند و متد های آن override می شوند. در نگاه اول، اینطور به نظر می رسد که کلاس Square ما دیگر قابل جایگزینی با کلاس Rectangle نیست. و انتظار می رود متد area تعریف شده در کلاس Rectangle دیگر در کلاس Square کار نکند. بنابراین اصل لیسکو را نقض می کند. بدون پیروی از اصل لیسکو، تغییرات در یک کلاس ممکن است عواقب غیرمنتظره ای داشته باشد و/یا نیاز به باز کردن یک کلاس قبلاً بسته داشته باشد. در حالی که پیروی از اصل لیسکو امکان ارث بری آسان رفتار کلاس را بدون ایجاد نتایج نامطلوب در برنامه با جایگزین کردن کلاس فرعی را فراهم می کند. این اصل فقط توسعه ای از اصل باز/بسته است و به این معنی است که ما باید مطمئن شویم که کلاس های مشتق شده جدید، کلاس های پایه را بدون تغییر رفتارشان گسترش می دهند.

برای جلوگیری از نقض قانون لیسکو باید از موارد زیر اجتناب کرد.

· متد ها باید مطابقت داشته باشند و پارامتر غیر مساوی از پارامتر را به عنوان نوع پایه قبول نکنند.

· نوع برگشتی متد باید با نوع پایه مطابقت داشته باشد.

· موارد استثنا باید با کلاس پایه مطابقت داشته باشند.

به مثال زیر که اصل لیسکو در آن رعایت شده است توجه کنید:

interface LogRepositoryInterface { /** * Gets all logs. * * @return array */ public function getAll(); }class FileLogRepository implements LogRepositoryInterface { public function getAll() { // Fetch the logs from the file and return an array return $logsArray; } }class DatabaseLogRepository implements LogRepositoryInterface { public function getAll() { // fetch Logs from model Log and call toArray() function to match the return type. return Log::all()->toArray();} }

همانطور که در کد بالا مشاهده می کنید، هر دو کلاسی که از اینترفیس LogRepositoryInterface ارثبری می کنند متد getAll پیاده سازی می کنند. FileRepository گزارش‌ها را از آرایه بازگشت فایل می‌خواند و DatabaseLogRepository لاگ ها را را با استفاده از متد Eloquent model all می‌خواند که نوع Collection را برمی‌گرداند، بنابراین ما متد toAarry را در مجموعه فراخوانی می‌کنیم تا آن را به آرایه تبدیل کنیم. اگر متد ()toArry را فراخوانی نکنیم و مجموعه ای را برگردانیم، لیسکو را نقض می کند که منجر به بررسی نوع در کلاس کلاینت می شود.

اصل تفکیک اینترفیس ها: این اصل بیان می‌کند که یک interface نباید متدهای ناخواسته را برای یک کلاس اعمال کند. ایده آن اینگونه است که به جای داشتن یک interface بزرگ، باید interface های کوچکتری داشته باشیم، بنابراین نباید یک interface با متدهای های زیادی ایجاد کنیم، اگر چنین interface ی داریم، باید به interface های کوچکتر تبدیل شود.

"تعریف interface های خاص به تعداد زیاد با کارکرد مشخص بهتر از تعریف یک interface بزرک با کارکردهای عمومی است"

<?phpinterface IPrintMachine { public function print(Document $d); public function scan(Document $d); public function xerox(Document $d); }class Document { // some attributes and methods }class AdvancePrinter implements IPrintMachine { public function print(Document $d){ echo &quotPrint document" } public function scan(Document $d){ echo &quotScan document" } public function xerox(Document $d){ echo &quotTake xerox copy of document" } } class SimplePrinter implements IPrintMachine { public function print(Document $d){ echo &quotPrint document" } public function scan(Document $d){ echo &quotNot supported" } public function xerox(Document $d){ echo &quotNot supported" } }<?php interface IPrinter { public function print(Document $d); }interface IScanner { public function scan(Document $d); }interface IXerox { public function xerox(Document $d); }class Document { // some attributes and methods }class AdvancePrinter implements IPrinter,IScanner,IXerox { public function print(Document $d){ echo &quotPrint document" } public function scan(Document $d){ echo &quotSacn document" } public function xerox(Document $d){ echo &quotTake xerox copy of document" } }class SimplePrinter implements IPrinter public function print(Document $d){ echo &quotPrint document" } }

اصل وارونگی وابستگی : این اصل به عبارتی ساده بیان میکند که کلاس های سطح بالا نباید به کلاس سطح پایین وابستگی داشته باشن بلکه هردوی آنها باید به abstractions وابسته باشند.

<?php
class MySqlConnection {
public function connect() {}
}

class Post{
private $dbConnection;
public function __construct(MySqlConnection $dbConnection) {
$this->dbConnection = $dbConnection;
$this->dbConnection->connect();
}
}

در مثال بالا یک کلاس MySqlConnection تعریف کرده ایم که دارای متد connect برای برقراری ارتباط با db است و این کلاس MySqlConnection را به سازنده کلاس Post پاس می کنیم. مشکل اینجاست که کلاس پست ما به کلاس MySqlConnection وابستگی دارد. اکنون در آینده، اگر نیاز به پشتیبانی از DB دیگری داشته باشیم، باید کلاس Post خود را تغییر دهیم.

مثال بالا را به شکل درست زیر تغییر می دهیم

interface DbConnectionInterface {
public function connect();
}

class MySqlConnection implements DbConnectionInterface {
public function connect() {}
}

class Post {
private $dbConnection;
public function __construct(DbConnectionInterface $dbConnection) {
$this->dbConnection = $dbConnection;
$this->dbConnection->connect();
}
}

سالیدsolidphp
شاید از این پست‌ها خوشتان بیاید