در این مقاله در رابطه با درک مفاهیم SOLID مینویسیم و مفاهیمی که برای آن وجود دارد را به ترتیب بررسی میکنیم. در واقع اصول SOLID یک استاندارد کدنویسی است که به برنامه نویس کمک میکند درک واضحتری از برنامه نویسی داشته باشد تا بتواند برنامهای تمیز با قابلیت توسعه زیاد پیاده سازی کند. این اصول توسط Robert C Martin در حوزه object-oriented design مطرح شد.
هر کدام از حروف SOLID بیانگر یک مفهوم هستند که به آنها میپردازیم.
وقتی برنامهای با ساختار و طراحی نامناسب پیادهسازی میشود کدهای برنامه غیرقابل انعطاف و شکننده خواهند بود به طوریکه با تغییر بخشی از برنامه احتمال بروز خطا و ایجاد باگ وجود دارد با توجه به این موارد ما باید اصول SOLID را یاد بگیریم و در برنامهها از آنها استفاده کنیم.
به طور کاملا خلاصه هر کلاس یک وظیفه را انجام میدهد.
A class should have one and only one reason to change, meaning that a class should have only one job.
یک کلاس تنها یک هدف و مسئولیت دارد، این به این معنا نیست که کلاس تنها یک متد داشته باشد بلکه یک کلاس میتواند متدهای مختلفی داشته باشد ولی همه آنها برای یک هدف خاص کار میکنند. هر زمان که یک کلاس چندین هدف و مسئولیت مختلف را داشت آن زمان است که آنها در قالب کلاس جدید باید قرار دهیم.
فرض کنید که یک پروژه داریم که API Base است، حالا یک Request را ارسال میکنیم و در کلاس مربوطه این اقدامات صورت میگیرد: لاگین کاربر/ اعتبارسنجی دادههای ارسال شده/ Query به دیتابیس و دریافت دادهها/ مرتبسازی دادهها طبق فرمت استاندارد و ارسال پاسخ.
همه کارهایی که انجام دادیم به صورت کاملا درست و بدون خطا کار میکند ولی اصل Single Responsibility به ما میگوید که هر کدام از کارها را در یک کلاس مجزا انجام دهیم، در واقع یک کلاس تنها و تنها یک وظیفه دارد! کلاسی که گزارشات را انجام میدهد تنها با گزارشاتی که وجود دارد کار میکند و ارتباطی به کاربر، فرمتدهی پاسخ و query به دیتابیس ندارد.
namespace Demo; use DB; class OrdersReport { public function getOrdersInfo($startDate, $endDate) { $orders = $this--->queryDBForOrders($startDate, $endDate); return $this->format($orders); } protected function queryDBForOrders($startDate, $endDate) { // If we would update our persistence layer in the future, // we would have to do changes here too. <=> reason to change! return DB::table('orders')->whereBetween('created_at', [$startDate, $endDate])->get(); } protected function format($orders) { // If we changed the way we want to format the output, // we would have to make changes here. <=> reason to change! return "<\h1>Orders: $orders <\/h1>" } }
در کلاس بالا اصل Single Responsibility نقض شده است، این کلاس تنها باید وظیفه و هدف گزارشگیری سفارشات را داشته باشد و همه متدهای آن همین هدف را دنبال کنند. ارتباط با دیتابیس و فرمتدهی ریسپانس وظیفه این کلاس نیست. از طرف دیگر در صورتیکه در آینده بخواهیم سایر فرمتهای json,xml و... را ایجاد کنیم باید برای هر کدام متدی ایجاد کنیم که کلاس را کامل از هدف خود دور میکند.
در نهایت به صورت زیر کد فوق را ریفکتور میکنیم.
namespace Report; use Report\Repositories\OrdersRepository; class OrdersReport { protected $repo; protected $formatter; public function __construct(OrdersRepository $repo, OrdersOutPutInterface $formatter) { $this->repo = $repo; $this->formatter = $formatter; } public function getOrdersInfo($startDate, $endDate) { $orders = $this->repo->getOrdersWithDate($startDate, $endDate); return $this->formatter->output($orders); } } namespace Report; interface OrdersOutPutInterface { public function output($orders); } namespace Report; class HtmlOutput implements OrdersOutPutInterface { public function output($orders) { return '< h1>Orders: ' . $orders . '< /h1>'; } } namespace Report\Repositories; use DB; class OrdersRepository { public function getOrdersWithDate($startDate, $endDate) { return DB::table('orders')->whereBetween('created_at', [$startDate, $endDate])->get(); } }
کلاس موجودیتها (entities) باید به نحوی پیاده سازی شود که برای توسعه دادن باز و برای تغییر دادن بسته باشد.
Objects or entities should be open for extension but closed for modification. A class should be easily extendable without modifying the class itself.
موجودیتهای برنامه (classes, modules, functions, etc.) باید به نحوی پیاده سازی شوند که بتوانیم آنها را توسعه دهیم و ویژگیهای جدید را اضافه کنیم بدون آنکه محتوای کدهای هر موجودیت را تغییر دهیم.
کدهای زیر را مشاهده کنید:
class Rectangle { public $width; public $height; public function __construct($width, $height) { $this->width = $width; $this->height = $height; } } class Circle { public $radius; public function __construct($radius) { $this->radius = $radius; } } class CostManager { public function calculate($shape) { $costPerUnit = 1.5; if ($shape instanceof Rectangle) { $area = $shape->width * $shape->height; } else { $area = $shape->radius * $shape->radius * pi(); } return $costPerUnit * $area; } } $circle = new Circle(5); $rect = new Rectangle(8,5); $obj = new CostManager(); echo $obj->calculate($circle);
در نمونه کد فوق در صورتیکه بخواهیم مساحت یک مربع یا شکل جدید را در متد CostManager محاسبه کنیم ابتدا باید آن متد را تغییر دهیم و این اصل Open-Closed را نقض میکند، بر اساس این اصل ما توسعه میدهیم و بر اساس ویژگی تغییر ایجاد نمیکنیم.
interface AreaInterface { public function calculateArea(); } class Rectangle implements AreaInterface { public $width; public $height; public function __construct($width, $height) { $this->width = $width; $this->height = $height; } public function calculateArea(){ $area = $this->height * $this->width; return $area; } } class Circle implements AreaInterface { public $radius; public function __construct($radius) { $this->radius = $radius; } public function calculateArea(){ $area = $this->radius * $this->radius * pi(); return $area; } } class CostManager { public function calculate(AreaInterface $shape) { $costPerUnit = 1.5; $totalCost = $costPerUnit * $shape->calculateArea(); return $totalCost; } } $circle = new Circle(5); $obj = new CostManager(); echo $obj->calculate($circle);
این اصل توسط Barbara Liskov و Jeannette Wing ابتدا در یک کنفرانس و در نهایت در مقاله ای در سال ۱۹۹۴ منتشر شد. مقالهای که منتشر شد به صورت قوانین ریاضی است. در نهایت Robert C Martin آن را به زبان سادهتری وارد اصول SOLID کرد.
Functions that use pointers of references to base classes must be able to use objects of derived classes without knowing it. (Robert C Martin)
Subclass/derived class should be substitutable for their base/parent class.
هر کلاسی که implement کرده باشد از یک abstraction (interface) باید نوع و متد آن قابل استفاده در آن کلاس باشد.(هر کلاسی که از کلاس دیگری ارثبری میکند نباید رفتار والد را تغییر دهد.) وقتی در یک interface یک متد تعریف میشود هدف تنها تعریف متد و دریافت ورودی نیست بلکه باید نوع خروجی متد هم در همه کلاسها یکسان باشد. اگر خروجی آرایه است باید در همه جایی که استفاده شده است خروجی از نوع آرایه باشد.
( در نسخه جدید php میتوانیم به صورت type hint نوع پارامتر دریافتی و نوع خروجی را مشخص کنیم که از این قانون پیروی کنیم.)
interface LessonRepositoryInterface { /** * Fetch all records. * * @return array */ public function getAll(); } class FileLessonRepository implements LessonRepositoryInterface { public function getAll() { // return through file system return []; } } class DbLessonRepository implements LessonRepositoryInterface { public function getAll() { /* Violates LSP because: - the return type is different - the consumer of this subclass and FileLessonRepository won't work identically */ // return Lesson::all(); // to fix this return Lesson::all()->toArray(); } }
New derived classes just extend without replacing the functionality of old classes.
No new exceptions can be thrown by the subtype.
Clients should not know which specific subtype they are calling.
با توجه به قوانین ذکر شده در بالا، مثال زیر این قانون را نقض میکند.
class VideoPlayer() { public function play($file) { // play the video } } class AviVideoPlayer extends VideoPlayer() { public function play($file) { if(phpinfo($file, PATHINFO_EXTENSION) != 'avi') { throw new Exception; // violates the LSP } } }
The pre-conditions enforced by the subclass must not be more restrictive than the pre-conditions enforced by the superclass.
An example of violation of pre-condition rule is when a superclass method can accept null
as an argument, but subclass method can’t. In this situation, clients of a superclass method that expect that it can handle null
value, can pass this value to the subclass method which can’t handle this input.
The post-conditions enforced by the subclass must not be more permissive than the post-conditions enforced by the superclass.
An example of violation of post-condition rule is when a superclass method can’t return null
, but subclass method can. In this situation, clients of a superclass that do not expect to get null
return value can actually get this value if the subclass is used.
این اصل به این اشاره میکند که یک کلاس نباید از قراردادی پیروی کند که حداقل یکی از متدهای آن را نمیتواند پیاده سازی کند.
A Client should not be forced to implement an interface that it doesn’t use.
همانند اصل SRP، هدف اصل Interface Segregation Principle این است که بخشهای مختلف نرمافزار را تا حد امکان به بخشهای کوچک، مستقل و بدون تکرار تقسیم کند.
مثال زیر این اصل را نقض میکند:
interface workerInterface { public function work(); public function sleep(); } class HumanWorker implements workerInterface { public function work() { var_dump('works'); } public function sleep() { var_dump('sleep'); } } class RobotWorker implements workerInterface { public function work() { var_dump('works'); } public function sleep() { // No need } }
برای رفع نقض شدن این اصل ISP به صورت زیر کد فوق را ویرایش میکنیم:
interface WorkAbleInterface { public function work(); } interface SleepAbleInterface { public function sleep(); } class HumanWorker implements WorkAbleInterface, SleepAbleInterface { public function work() { var_dump('works'); } public function sleep() { var_dump('sleep'); } } class RobotWorker implements WorkAbleInterface { public function work() { var_dump('works'); } }
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Abstractions should not depend on details. Details should depend on abstractions.
برای پیادهسازی این اصل در پروژه باید ساختار ماژولها را به گونهای بنویسیم که کدهای high-level به کدهای low-level وابسته نباشند و کدهای high-level نباید تحت تاثیر کدهای low-level قرار بگیرند.
مثال زیر را مشاهده کنید:
class MySQLConnection { /** * db connection */ public function connect() { var_dump('MYSQL Connection'); } } class PasswordReminder { /** * @var MySQLConnection */ private $dbConnection; public function __construct(MySQLConnection $dbConnection) { $this->dbConnection = $dbConnection; } }
همانطور که در نمونه کد بالا میبینید MySQLConnection در کلاس PasswordReminder برای ایجاد کانکشن inject شده است اما این کلاس به کلاس MySQLConnection وابسته است. high-level module در این مثال PasswordReminder است که به low-level module یعنی MySQLConnection وابسته است.
اگر ما بخواهیم کانکشن را از MySQLConnection به MongoDBConnection تغییر دهیم، باید در کلاس PasswordReminder به صورت hard-code تغییر ایجاد کنیم و این اصل را نقض میکند. PasswordReminder باید به یک Abstractions وابسته باشد. به صورت زیر کدها را ویرایش میکنیم.
interface ConnectionInterface { public function connect(); } class DbConnection implements ConnectionInterface { /** * db connection */ public function connect() { var_dump('MYSQL Connection'); } } class PasswordReminder { /** * @var MySQLConnection */ private $dbConnection; public function __construct(ConnectionInterface $dbConnection) { $this->dbConnection = $dbConnection; } }
در مثال بالا اگر ما بخواهیم کانکشن را از MySQLConnection به MongoDBConnection تغییر دهیم احتیاجی نیست که کدهای inject شده در constructor کلاس PasswordReminder را تغییر دهیم چونکه کلاس PasswordReminder به Abstractions وابستهست. درنهایت برای اعمال تغییرات از container برای resolve کردن آن استفاده میکنیم.