شی گرایی و برنامهنویسی شیءگرا اصول و مفاهیمی را در خود جای داده تا فرآیند توسعه نرمافزار را سادهتر و قابلتحملتر کند. SOLID، مجموعهای از اصول مهم در این زمینه هستند که توسعه دهندگان را در نوشتن کدهای بهتر و قابلتوسعهتر یاری میسازند. در این مقاله، با پنج اصل SOLID در شی گرایی آشنا می شود و می بیند که چگونه این اصول می توانند شما را به یک توسعه دهند بهتر تبدیل کنند.
SOLID مخفف پنج اصل اول طراحی شی گرا (OOD) توسط رابرت سی مارتین (همچنین با نام عمو باب) است.
این اصول شیوههایی را ایجاد میکنند که به توسعه نرمافزار با ملاحظاتی برای حفظ و گسترش همزمان با رشد پروژه کمک میکند. اتخاذ این شیوهها همچنین میتواند به جلوگیری از کد های نامناسب، بازآفرینی کد و توسعه نرمافزار بسیار سریع یا تطبیقی کمک کند.
در اصل می شود گفت که، هدف از این اصول این است که به توسعه دهندگان اجازه دهند نرم افزار بهتری بنویسند. نگهداری این نرمافزار آسانتر و ارزانتر، درک آسانتر، توسعه سریعتر در یک تیم و تست آسانتر است. پیروی از این اصول موفقیت را تضمین نمی کند، اما اجتناب از آنها در بیشتر موارد منجر به حداقل نتایج کمتر از حد مطلوب از نظر عملکرد، هزینه یا هر دو می شود.
SOLID مخفف:
S - اصل تک مسئولیت Single Responsibility Principle (SRP)
O - اصل باز-بسته Open-Closed Principle (OCP)
L - اصل جایگزینی لیسکوف Liskov Substitution Principle (LSP)
I - اصل جداسازی رابط Interface Segregation Principle (ISP)
D - اصل وارونگی وابستگی Dependency Inversion Principle (DIP)
در این مقاله، شما با هر یک از اصول به صورت جداگانه آشنا می شوید تا بفهمید که چگونه SOLID می تواند به شما کمک کند تا یک توسعه دهنده بهتر شوید.
ما در این مقاله برای درک این اصول از زبان PHP استفاده کرده ایم و شما نیز می توانید این اصول را در هر زبان برنانه نویسی پیاده سازی و استفاده کنید.
اصل تک مسئولیتی
اصل مسئولیت تکی (SRP) بیان می کند:
یک کلاس باید یک و تنها یک دلیل برای تغییر داشته باشد، به این معنی که یک کلاس باید فقط یک شغل داشته باشد.
این اصل بیان می کند که شما زمانی که دارید یک کلاس ایجاد می کنید این کلاس شما باید یک اساس یونیک یا همان یکتا داشته باشد. و تنها یک کلاس یک تابع یا ماژول نیز باید این اصل را پیروی کند.
به عنوان مثال، برنامه ای را در نظر بگیرید که مجموعه ای از اشکال - دایره ها و مربع ها را می گیرد و مجموع مساحت تمام اشکال موجود در مجموعه را محاسبه می کند.
ابتدا کلاس های شکل را ایجاد کنید و از سازنده ها بخواهید پارامترهای مورد نیاز را تنظیم کنند.
برای مربع ها، باید length
یک ضلع را بدانید:
class Square
{
public $length;
public function construct($length)
{
$this->length = $length;
}
}
برای دایره ها، باید radius
را بدانید:
class Circle
{
public $radius;
public function construct($radius)
{
$this->radius = $radius;
}
}
در مرحله بعد، کلاس AreaCalculator
را ایجاد کنید و سپس منطق کار را بنویسید تا ناحیه تمام اشکال ارائه شده جمع شود. مساحت مربع با مجذور طول محاسبه می شود. مساحت دایره با پی ضربدر شعاع مجذور محاسبه می شود.
class AreaCalculator
{
protected $shapes;
public function __construct($shapes = [])
{
$this->shapes = $shapes;
}
public function sum()
{
foreach ($this->shapes as $shape) {
if (is_a($shape, 'Square')) {
$area[] = pow($shape->length, 2);
} elseif (is_a($shape, 'Circle')) {
$area[] = pi() * pow($shape->radius, 2);
}
}
return array_sum($area);
}
public function output()
{
return implode('', [
'',
'Sum of the areas of provided shapes: ',
$this->sum(),
'',
]);
}
}
برای استفاده از کلاس AreaCalculator
، باید کلاس را نمونه سازی کنید و آرایه ای از اشکال را ارسال کنید و خروجی را در پایین صفحه نمایش دهید.
در اینجا یک مثال با مجموعه ای از سه شکل آورده شده است:
دایره ای با شعاع 2
مربعی به طول 5
مربع دوم به طول 6
$shapes = [
new Circle(2),
new Square(5),
new Square(6),
];
$areas = new AreaCalculator($shapes);
echo $areas->output();
مشکل متد خروجی این است که AreaCalculator
منطق خروجی داده ها را کنترل می کند.
سناریویی را در نظر بگیرید که در آن خروجی باید به فرمت دیگری مانند JSON تبدیل شود.
تمام منطق کار توسط کلاس AreaCalculator
مدیریت می شود. این امر اصل مسئولیت واحد را نقض می کند. کلاس AreaCalculator
فقط باید با مجموع مساحت اشکال ارائه شده سروکار داشته باشد. نباید اهمیتی بدهد که کاربر JSON می خواهد یا HTML.
برای رفع این مشکل، میتوانید یک کلاس SumCalculatorOutputter
جداگانه ایجاد کنید و از آن کلاس جدید برای مدیریت منطقی که برای خروجی دادهها به کاربر نیاز دارید، استفاده کنید:
class SumCalculatorOutputter
{
protected $calculator;
public function __constructor(AreaCalculator $calculator)
{
$this->calculator = $calculator;
}
public function JSON()
{
$data = [
'sum' => $this->calculator->sum(),
];
return json_encode($data);
}
public function HTML()
{
return implode('', [
'',
'Sum of the areas of provided shapes: ',
$this->calculator->sum(),
'',
]);
}
}
کلاس SumCalculatorOutputter
به صورت زیر عمل می کند:
$shapes = [
new Circle(2),
new Square(5),
new Square(6),
];
$areas = new AreaCalculator($shapes);
$output = new SumCalculatorOutputter($areas);
echo $output->JSON();
echo $output->HTML();
اکنون منطقی که برای خروجی دادهها به کاربر نیاز دارید توسط کلاس SumCalculatorOutputter
مدیریت میشود.
که اصل مسئولیت تکی را برآورده می کند.
اصل باز-بسته
اصل بسته باز (OCP) بیان می کند:
اشیا یا موجودیت ها باید برای گسترش باز باشند اما برای اصلاح بسته باشند.
این بدان معنی است که یک کلاس باید بدون تغییر خود کلاس قابل گسترش باشد. نه تنها کلاسها، ماژولها و توابع باید برای توسعه باز باشند اما برای اصلاح بسته باشند.
ممکن است به نظر برسد که این اصل با خودش تناقض داشته باشد، اما همچنان میتوانید آن را در کد معنا کنید. این بدان معناست که شما باید بتوانید عملکرد یک کلاس، ماژول یا تابع را با افزودن کد بیشتر بدون تغییر کد موجود گسترش دهید.
بیایید دوباره کلاس AreaCalculator
را بررسی کنیم و روی متد جمع تمرکز کنیم:
class AreaCalculator
{
protected $shapes;
public function __construct($shapes = [])
{
$this->shapes = $shapes;
}
public function sum()
{
foreach ($this->shapes as $shape) {
if (is_a($shape, 'Square')) {
$area[] = pow($shape->length, 2);
} elseif (is_a($shape, 'Circle')) {
$area[] = pi() * pow($shape->radius, 2);
}
}
return array_sum($area);
}
}
سناریویی را در نظر بگیرید که در آن کاربر مجموع اشکال اضافی مانند مثلث، پنج ضلعی، شش ضلعی و غیره را میخواهد. شما باید دائماً این فایل را ویرایش کنید و بلوکهای if/else
بیشتری اضافه کنید. این امر اصل بسته بودن باز را نقض می کند.
راهی که می توانید این متد جمع را بهتر کنید این است که منطق محاسبه مساحت هر شکل را از روش کلاس AreaCalculator
حذف کنید و آن را به کلاس هر شکل وصل کنید.
در اینجا روش مساحت تعریف شده در Square است:
class Square
{
public $length;
public function __construct($length)
{
$this->length = $length;
}
public function area()
{
return pow($this->length, 2);
}
}
و در اینجا متد area
تعریف شده در Circle است:
class Circle
{
public $radius;
public function construct($radius)
{
$this->radius = $radius;
}
public function area()
{
return pi() * pow($shape->radius, 2);
}
}
سپس متد sum
برای AreaCalculator
می تواند به صورت زیر بازنویسی شود:
class AreaCalculator
{
// ...
public function sum()
{
foreach ($this->shapes as $shape) {
$area[] = $shape->area();
}
return array_sum($area);
}
}
اکنون، میتوانید کلاس شکل دیگری ایجاد کنید و آن را هنگام محاسبه مجموع بدون شکستن کد ارسال کنید.
با این حال، مشکل دیگری به وجود می آید. چگونه می دانید که شیء ارسال شده به AreaCalculator
در واقع یک شکل است یا اینکه شکل دارای متدی به نام area
است؟
کدگذاری به یک رابط بخشی جدایی ناپذیر از SOLID است.
یک ShapeInterface
ایجاد کنید که از area
پشتیبانی می کند:
interface ShapeInterface
{
public function area();
}
کلاس های شکل خود را برای پیاده سازی ShapeInterface
تغییر دهید.
در اینجا به روز رسانی برای Square
است:
class Square implements ShapeInterface
{
// ...
}
و در اینجا به روز رسانی برای Circle
است:
class Circle implements ShapeInterface
{
// ...
}
در روش جمع برای Area Calculator
، می توانید بررسی کنید که آیا اشکال ارائه شده در واقع نمونه هایی از Shape Interface
هستند یا خیر. در غیر این صورت، یک استثنا قرار دهید:
class AreaCalculator
{
// ...
public function sum()
{
foreach ($this->shapes as $shape) {
if (is_a($shape, 'ShapeInterface')) {
$area[] = $shape->area();
continue;
}
throw new AreaCalculatorInvalidShapeException();
}
return array_sum($area);
}
}
که اصل باز-بسته را برآورده می کند.
اصل جایگزینی لیسکوف
اصل جایگزینی Liskov یکی از مهمترین اصولی است که در برنامه نویسی شی گرا (OOP) باید به آن پایبند بود. این توسط دانشمند کامپیوتر باربارا لیسکوف در سال 1987 در مقاله ای که او با ژانت وینگ نوشته بود معرفی شد.
اصل جایگزینی لیسکوف می گوید:
فرض کنید
q(x)
یک ویژگی قابل اثبات در مورد اشیاءx
از نوعT
باشد. سپسq(y)
باید برای اشیاءy
از نوعS
قابل اثبات باشد که در آنS
زیرگروهT
است.
این بدان معنی است که هر زیر کلاس یا کلاس مشتق شده باید برای پایه یا کلاس والد خود جایگزین شود.
این اصل بیان میکند که کلاسها یا زیر کلاسهای فرزند باید جایگزین کلاسهای والد یا کلاسهای super خود شوند. به عبارت دیگر، کلاس فرزند باید بتواند جایگزین کلاس والد شود. این مزیت را دارد که به شما امکان می دهد بدانید از کد خود چه انتظاری دارید.
با ایجاد کلاس AreaCalculator
مثال، یک کلاس VolumeCalculator
جدید را در نظر بگیرید که کلاس AreaCalculator
را گسترش می دهد:
class VolumeCalculator extends AreaCalculator
{
public function construct($shapes = [])
{
parent::construct($shapes);
}
public function sum()
{
// logic to calculate the volumes and then return an array of output
return [$summedData];
}
}
به یاد بیاورید که کلاس SumCalculatorOutputter
شبیه این است:
class SumCalculatorOutputter {
protected $calculator;
public function __constructor(AreaCalculator $calculator) {
$this->calculator = $calculator;
}
public function JSON() {
$data = array(
'sum' => $this->calculator->sum();
);
return json_encode($data);
}
public function HTML() {
return implode('', array(
'',
'Sum of the areas of provided shapes: ',
$this->calculator->sum(),
''
));
}
}
اگر سعی کردید مثالی مانند این اجرا کنید:
$areas = new AreaCalculator($shapes);
$volumes = new VolumeCalculator($solidShapes);
$output = new SumCalculatorOutputter($areas);
$output2 = new SumCalculatorOutputter($volumes);
وقتی متد HTML
را روی شی output2$
فراخوانی میکنید، یک خطای E_NOTICE
دریافت میکنید که شما را از تبدیل آرایه به رشته مطلع میکند.
برای رفع این مشکل، به جای برگرداندن یک آرایه از روش مجموع کلاس VolumeCalculator
، $summedData
را برگردانید:
class VolumeCalculator extends AreaCalculator
{
public function construct($shapes = [])
{
parent::construct($shapes);
}
public function sum()
{
// logic to calculate the volumes and then return a value of output
return $summedData;
}
}
summedData$
می تواند یک float ، دو یا عدد صحیح باشد.
این اصل جایگزینی لیسکوف را برآورده می کند.
اصل جداسازی رابط
اصل تفکیک رابط بیان می کند:
یک کلاینت هرگز نباید مجبور به پیاده سازی رابطی شود که از آن استفاده نمی کند، یا کلاینت ها نباید مجبور شوند به متد هایی که استفاده نمی کنند وابسته شوند.
اصل تفکیک واسط بیان می کند که کلاینت ها نباید مجبور به پیاده سازی رابط ها یا متد هایی شوند که استفاده نمی کنند.به طور خاص، ISP پیشنهاد میکند که توسعهدهندگان نرمافزار باید رابطهای بزرگ را به واسطهای کوچکتر و خاصتر تقسیم کنند، بهطوریکه کلاینتها فقط باید به رابطهایی وابسته باشند که به آنها مرتبط است. این میتواند حفظ پایگاه کد را آسانتر کند.
این اصل تقریباً مشابه اصل مسئولیت واحد (SRP) است. اما این فقط در مورد این نیست که یک رابط تنها یک کار را انجام دهد - بلکه در مورد شکستن کل پایگاه کد به چندین رابط یا کامپوننت است.
در مورد این همان کاری فکر کنید که هنگام کار با فریمورکها و کتابخانههای frontend مانند React، Svelte و Vue انجام میدهید. شما معمولاً پایگاه کد را به کامپوننت هایی تقسیم می کنید که فقط در صورت نیاز وارد می شوید.
این بدان معنی است که شما کامپوننت های جداگانه ای را ایجاد می کنید که عملکردی خاص برای آنها دارد. برای مثال، کامپوننتی که مسئول پیادهسازی اسکرول به بالا است، چیزی نیست که بین روشن و تاریک و غیره جابهجا شود.
همچنان که از مثال قبلی ShapeInterface
ساخته می شود، باید از اشکال سه بعدی جدید Cuboid
و Spheroid
پشتیبانی کنید و این اشکال باید حجم را نیز محاسبه کنند.
بیایید در نظر بگیریم که اگر بخواهید ShapeInterface
را برای اضافه کردن قرارداد دیگری تغییر دهید، چه اتفاقی میافتد:
interface ShapeInterface
{
public function area();
public function volume();
}
حال، هر شکلی که ایجاد می کنید باید متد volume
را پیاده سازی کند، اما می دانید که مربع ها شکل های مسطح هستند و حجم ندارند، بنابراین این رابط کلاس Square
را مجبور می کند تا متدی را اجرا کند که هیچ استفاده ای از آن ندارد.
این امر اصل جداسازی رابط را نقض می کند. در عوض، می توانید یک رابط دیگر به نام ThreeDimensionalShapeInterface
ایجاد کنید که دارای قرارداد حجم است و اشکال سه بعدی می توانند این رابط را پیاده سازی کنند:
interface ShapeInterface
{
public function area();
}
interface ThreeDimensionalShapeInterface
{
public function volume();
}
class Cuboid implements ShapeInterface, ThreeDimensionalShapeInterface
{
public function area()
{
// calculate the surface area of the cuboid
}
public function volume()
{
// calculate the volume of the cuboid
}
}
این رویکرد بسیار بهتری است، اما مشکلی که باید مراقب آن بود هنگام تایپ کردن این رابطها است. به جای استفاده از ShapeInterface
یا ThreeDimensionalShapeInterface
، میتوانید رابط دیگری ایجاد کنید، شاید ManageShapeInterface
، و آن را روی هر دو شکل تخت و سه بعدی پیادهسازی کنید.
به این ترتیب، می توانید یک API واحد برای مدیریت اشکال داشته باشید:
interface ManageShapeInterface
{
public function calculate();
}
class Square implements ShapeInterface, ManageShapeInterface
{
public function area()
{
// calculate the area of the square
}
public function calculate()
{
return $this->area();
}
}
class Cuboid implements ShapeInterface, ThreeDimensionalShapeInterface, ManageShapeInterface
{
public function area()
{
// calculate the surface area of the cuboid
}
public function volume()
{
// calculate the volume of the cuboid
}
public function calculate()
{
return $this->area();
}
}
اکنون در کلاس AreaCalculator می توانید فراخوانی متد ناحیه را با محاسبه جایگزین کنید و همچنین بررسی کنید که آیا شی نمونه ای از ManageShapeInterface است نه ShapeInterface.
که اصل تفکیک رابط را برآورده می کند.
اصل وارونگی وابستگی
اصل وارونگی وابستگی بیان می کند:
موجودیت ها باید به abstractions وابسته باشند، نه به ادغام. بیان می کند که ماژول سطح بالا نباید به ماژول سطح پایین بستگی داشته باشد، اما آنها باید به انتزاعات وابسته باشند.
اصل وارونگی وابستگی مربوط به جداسازی ماژول های نرم افزاری است. یعنی تا حد امکان آنها را از یکدیگر جدا کنید.
این اصل بیان می کند که ماژول های سطح بالا نباید به ماژول های سطح پایین وابسته باشند. در عوض، هر دو باید به abstractions وابسته باشند. علاوه بر این، abstraction ها نباید به جزئیات بستگی داشته باشند، بلکه جزئیات باید به abstraction ها بستگی داشته باشند.
به عبارت ساده تر، این بدان معناست که به جای نوشتن کدی که بر جزئیات خاصی از نحوه عملکرد کدهای سطح پایین متکی است، باید کدی بنویسید که به abstraction کلی تری بستگی دارد که می توانند به روش های مختلف پیاده سازی شوند.
این کار تغییر کدهای سطح پایین را بدون نیاز به تغییر کدهای سطح بالاتر آسان تر می کند.
این اصل امکان جداسازی را فراهم می کند.
در اینجا نمونه ای از یادآوری رمز عبور است که به پایگاه داده MySQL متصل می شود:
class MySQLConnection
{
public function connect()
{
// handle the database connection
return 'Database connection';
}
}
class PasswordReminder
{
private $dbConnection;
public function __construct(MySQLConnection $dbConnection)
{
$this->dbConnection = $dbConnection;
}
}
اول، MySQLConnection
یک ماژول سطح پایین است در حالی که PasswordReminder
سطح بالایی است، اما طبق تعریف D
در SOLID، که می گوید به abstraction بستگی دارد، نه به concretions. این قطعه بالا این اصل را نقض می کند زیرا کلاس PasswordReminder
مجبور است به کلاس MySQLConnection
وابسته باشد.
بعداً، اگر میخواهید موتور پایگاه داده را تغییر دهید، باید کلاس PasswordReminder
را نیز ویرایش کنید و این اصل باز کردن بسته را نقض میکند.
کلاس PasswordReminder
نباید اهمیتی بدهد که برنامه شما از چه پایگاه داده ای استفاده می کند. برای رسیدگی به این مسائل، میتوانید به یک رابط کدنویسی کنید زیرا ماژولهای سطح بالا و سطح پایین باید به انتزاع بستگی داشته باشند:
interface DBConnectionInterface
{
public function connect();
}
رابط دارای یک متد اتصال است و کلاس MySQLConnection
این رابط را پیاده سازی می کند. همچنین، به جای تایپ مستقیم کلاس MySQLConnection
در سازنده PasswordReminder
، به جای آن DBConnectionInterface
را تایپ کنید و مهم نیست که برنامه شما از چه نوع پایگاه داده ای استفاده می کند، کلاس PasswordReminder
می تواند بدون هیچ مشکلی به پایگاه داده متصل شود و باز و بسته شود. اصل نقض نمی شود
class MySQLConnection implements DBConnectionInterface
{
public function connect()
{
// handle the database connection
return 'Database connection';
}
}
class PasswordReminder
{
private $dbConnection;
public function __construct(DBConnectionInterface $dbConnection)
{
$this->dbConnection = $dbConnection;
}
}
این کد مشخص می کند که هر دو ماژول های سطح بالا و سطح پایین به انتزاع بستگی دارند.
نتیجه
در این مقاله پنج اصل SOLID Code به شما ارائه شد. پروژه هایی که به اصول SOLID پایبند هستند را می توان با همکاران به اشتراک گذاشت، توسعه داد، اصلاح کرد، آزمایش کرد و با مشکلات کمتری بازسازی کرد.
با مطالعه سایر روش های توسعه نرم افزار Agile و Adaptive به یادگیری خود ادامه دهید.
اگر می خواهید PHP را به صورت پیشرفته یادبگری، دوره آموزش مقدماتی تا پیشرفته PHP وبسایت آنوفل را ببینید.