PHPʼda obyektga yoʻnaltirilgan loyihalashning 5 ta asosiy tamoyillari

Muallif: Sanjarbek Sobirjonov       199       2020-09-29/14:09:31


SOLID nima?

Dasturchi sifatida, dasturiy mahsulot yaratish davomida talablarning o'zgarishi, hamda yangi qo'shimchalarni qo'shish bilan shug'ullanib kelamiz. Hayotimizdagi bosh og'riqlaridan xalos qilish uchun esa, kodlarni to'g'ri yozish juda ham muhim. ****

SOLID - bu obyektga yoʻnaltirilgan dasturlashda amal qilinishi kerak boʻlgan qoidalar toʻplami hisoblanadi. SOLID qoidalaridan foydalanish kodni xunuk yozishni oldini olish hamda loyihangizga yaxshi tuzilgan arxitekturani olib kirishga imkon beradi. Xunuk yozilgan loyihalar moslashuvchan va mustahkam boʻlmagan kodga olib keladi. Bunday loyihalarda kodga kichkina oʻzgartirish kiritish katta xatoliklarni keltirib chiqaradi.

SOLID - bu 2000-yilda yuqori darajadagi bogʻlanuvchan, moslashuvchan va kengayuvchan dasturiy taʼminot yozish imkonini beradigan Robert C Martin (Bob Togʻa) tomonidan taklif etilgan 5ta tamoyil toʻplamining qisqartmasi hisoblanadi.

SOLID tushunchasini tashkil etuvchi beshta tamoyillar:

  1. Yagona javobgarlik tamoyili (Single reponsibility principle)
  2. Ochiqlik/Yopiqlik tamoyili (Open/Closed principle)
  3. Liskov almashtirish tamoyili (Liskov Substitution principle)
  4. Interfeyslarga ajratish tamoyili (Interface Segregation principle)
  5. Aks bogʻliqlik tamoyili (Dependency Inversion principle)

Shunday ekan, har bir tamoyilni misollar bilan birgalikda tushunib olamiz.

Yagona javobgarlik tamoyili (Single reponsibility principle) - nomidan ko'rinib turibdiki, bu tamoyilning asosiy maqsadi, class yoki modul uchun yagona javobgarlikka ega bo'lishidir. Boshqacha qilib aytganda, class yoki modul faqatgina bitta muammoga yechim topishi kerak. Shuning uchun, uning o'zgarishi uchun 1 tagina sabab bo'lishi kerak. Bu bizning kodimizni uyg'unlashtiradi hamda, test qilish va saqlash jarayonini osonlashtiradi.

Keling, yanada yaxshiroq tushunishimiz uchun misolni ko'rib chiqamiz

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");

Yuqoridagi kodda, bizda sodda logger sinfi aks etgan va u log larni to'playdi va ularni ma'lum bir fayl nomiga saqlab qo'yadi. Barchasi yaxshi ketyabganga o'xshaydi, lekin bu Yagona javobgarlik tamoyili ni ****buzadi, chunki, logger sinfining 2 ta javobgarligi bor:

  1. Loglarni bir yerga jamlash
  2. Loglarni saqlash

Keling, to'g'ri tadbiq etilishini ko'rib chiqamiz:

<?php 
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);
  }
  
}class LogStorage{
 private $fileName;
 public function __construct($fileName){
  $this->fileName = $fileName;
 }public function save($text){
  $fp = fopen($this->fileName,"w");
    fwrite($fp,$text);
    fclose($fp);
 }
}$logger = new Logger();
$logger->add("First log");
$logger->add("Second log");
$logger->add("Third log");$logStorage = new LogStorage("pfile.txt");
$logStorage->save($logger->toString("\\n"));

Shunday qilib biz logger sinfimizni ikki xil sinfga ajratdik, biri Logger, ikkinchisi LogStorage. Hozir, har bir sinfda yagona javobgarlik bor.

Logger sinfi - faqatgina log larni bir yerga jamlash vazifasini bajaradi.

Logstorage sinfi - loglarni fayllarga saqlashga javobgardir.

Endi esa, kodimiz uyg'un holatga keldi desak ham bo'ladi.

Ochiqlik/Yopiqlik tamoyili: Bu tamoyilning maqsadi, yangi xususiyat kerak bo'lganda, mavjud bo'lgan va testdan o'tkazilgan sinflarga o'zgartirish kiritilmasligi uchun qurilgan. Biz mavjud bo'lgan sinfga yangi xossalar qo'shayotganimizda, yangi buglar xosil bo'lishi mumkin. Shuning uchun, mavjud sinf/interfeys ni o'zgartirish o'rniga, biz yangi xossalar uchun alohida sinf yoki interfeys qo'shami

"Dasturiy ta'minot subyektlari… kengaytirilishi uchun ochiq bo'lishi kerak, ammo o'zgartirish uchun yopiq bo'lishi kerak."

Yaxshiroq tushunishimiz uchun quyidagi misolni ko'rib chiqamiz:

<?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);

Yuqoridagi misolda, bizda 2 ta sodda sinflar mavjud. Ular, SavingAccount va FixedDepositAccount. Hamda, InterestCalculator sinfi. Ammo, bu yerda bir muammo bor. Bizning InterestCalculator sinfimiz o'zgartirishlar uchun yopiq emas. Bizning IntrestCalculator sinfimiz oʻzgartirishlar uchun yopiq emas. Qachonki biz yangi akkaunt turini qoʻshishimiz kerak boʻlsa, tizimda yangi akkaunt turini qoʻllab-quvvatlash uchun IntrestCalculator sinfiga oʻzgartirish kiritishimiz kerak. Yuqoridagi namunamiz esa Ochiqlik/Yopiqlik tamoyilini buzadi.

Quyida takomillashtirilgan holati:

<?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);

Liskov Almashtirish tamoyili: Bu tamoyilning nomi Barbara Liskovning nomiga atab qoʻyilgan. U bu tamoyilni 1987-yilda ommaga tanishtirgan. Tamoyil, avlod sinf asosiy sinfning ish-faoliyatini buzmasdan uning oʻrnida ishlatilishi kerakligini nazarda tutadi.

“Dasturdagi obyektlarni dasturning ish-faoliyatini oʻzgartirmasdan ularning avlod sinflarning ekzemplyarlari bilan almashitirish imkoni boʻlishi kerak.”

Buni juda sodda namuna asosida koʻrib chiqamiz:

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

Yuqoridagi namunada, 2ta oddiy sinf mavjud: A va B. B sinf A sinfdan meros oladi. Liskov Almashtirish tamoyiliga koʻra dasturimizdagi A sinfini qayerda ishlatsak ham, biz uning B sinfi bilan almashtirish imkoniga ega boʻlishimiz kerak. Endi keling hamma bilgan Toʻgʻri Toʻrtburchakning kvadrati masalasiga diqqatimizni qaratamiz:

<?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

Yuqoridagi namunada, biz Square sinfini yaratish maqsadida Rectangle sinfidan meros oldik. Square sinfi ichida Rectangle sinfining metodlaridan foydalanish imkoniyati mavjud. Birinchi qaraganda, u yaxshi koʻrinishi mumkin lekin Square sinfimiz hozir Rectangle sinfi uchun mos emas. Agar biz Rectangle sinfini Square sinfi bilan almashtirsak, area metodi Rectangle uchun kutilgandek ishlamaydi. Bu esa Liskov Almashtirish Tamoyili(LAT)ni buzadi.

LATga rioya qilmasdan, sinfga oʻzgartirish kiritish kutilmagan oqibatlarga yoki oldingi yopiq sinfni ochishni talab qilinishiga sabab boʻlishi mumkin. LATga amal qilinsa dasturning ishlashini oson kengaytirishga imkon beradi chunki uning avlod sinflari ishlayotgan kodga hech qanday xatolarsiz birikib ketishi mumkin. Bu tamoyil shunchaki Ochiqlik/Yopiqlik Tamoyilining kengaytirilgani va bu yangi hosil bo'lgan sinflar asosiy sinflardan ularning ish faoliyatini o'zgartirmasdan meros olayotganligiga ishonch hosil qilishimiz kerakligini anglatadi.

Quyda LATning buzilishini oldini olish shartlari keltirilgan:

  • Metod imzolari asosiy turdagi parametrlarga teng parametrlarni qabul qilishi va ular bilan mos kelishi kerak.
  • Metoddan qaytayotgan natija turi asosiy turdagi metodning qaytarayotgan natijasining turi bilan mos kelishi kerak.
  • Istisnolar turli asosiy sinfniki bilan mos tushishi kerak.

Keling endi LATga rioya qilib yozilgan namunaviy kodni koʻrib chiqamiz:

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();}
}

Yuqoridagi koddan koʻrib turganingizdek, ikkala sinf ham LogRepositoryInterface getAll metodiga ega boʻlganligi tufayli implement qilmoqda. FileRepository fayldagi loglari oʻqiydi va massiv qaytaradi, DatabaseLogRepository Eloquent modelining all() metodidan foydalangan holda loglarni oʻqiydi va Toʻplam turini qaytaradi, biz uni massiv qilish uchun toʻplamdagi toArray() metodini chaqiramiz. Agar toArray() metodini chaqirmasak va u Toʻplam qaytarsa, mijoz sinfdagi turni tekshirishga sabab boʻladigan LATni buzishga olib keladi.

Interfeyslarga ajratish tamoyili: Bu tamoyil interfeysni sinfga istalmagan metodlarni olib kirmaslikni nazarda tutadi. Bu yerdagi maqsad - bu katta interfeysga ega boʻlgandan koʻra kichikroq sinflarga ega boʻlish. Yaʼni koʻp metodlardan iborat sinf yaratmaymiz, agar bizda shunday interfeys boʻlsa ularni kichikroq interfeyslarga ajratib chiqamiz.

“Koʻplab mijozga-xoslangan interfeyslar yagona umumiy-maqsadli interfeysdan yaxshiroq.”

<?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 "Print document";
  }
  public function scan(Document $d){
    echo "Scan document";
  }
  public function xerox(Document $d){
    echo "Take xerox copy of document";
  }
}
class SimplePrinter implements IPrintMachine
{
  public function print(Document $d){
    echo "Print document";
  }
  public function scan(Document $d){
    echo "Not supported";
  }
  public function xerox(Document $d){
    echo "Not 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 "Print document";
  }
  public function scan(Document $d){
    echo "Sacn document";
  }
  public function xerox(Document $d){
    echo "Take xerox copy of document";
  }
}class SimplePrinter implements IPrinter
  public function print(Document $d){
    echo "Print document";
  }
}

Aks bogʻliqlik tamoyili: Bu tamoyilga koʻra, yuqori darajali sinflar toʻgʻridan-toʻgʻri quyi darajali sinflarga bogʻlangan boʻlmasligi kerak balki mavhum boʻlishi kerak. Bu yuqori darajali sinf quyi darajali sinfning maʼlumotlarinining bajarilishini bilmasligi, quyi darajali sinf esa mavhum boʻlishi kerak. Bu yerda kichik namuna keltirilgan: taassavur qiling bizda blog chop etadigan va maʼlumotlar bazasidan maqolalarni oʻqiydigan Post sinfi mavjud, bunday vazifalarni bajarishi uchun unga baza bilan bogʻlanish muhim sanaladi. Shuning uchun birinchi urunishimiz quyidagicha boʻlishi mumkin:

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

Yuqoridaga amaliyotda biz baza bilan bogʻlanadigan MySqlConnection sinfini belgiladik va uni Post sinfining konstruktoriga uzatdik. Bu yerdagi muammo - bu bizning post-sinfimiz konkret MySqlConnection sinfiga qaram. Endi kelgusida, agar bizga boshqa maʼlumot bazasini ishlatishga toʻgʻri kelib qolsa, Post sinfimizga ham oʻzgartirish kiritishimizga toʻgʻri keladi. Buni oldini olish uchun biz Post sinfimizni oʻzgartirib, konstruktorga konkret sinf turini uzatishdan koʻra biz MySqlConnection sinfi realizatsiya qilayotgan DbConnection interfeysini uzatishimiz kerak:

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();
  }
}

Endi yangi baza qoʻshadigan boʻlsak Post sinfini oʻzgartirishimiz shart emas. Yangi baza qoʻshish uchun qiladigan yagona ishimiz bu DbConnectionInterface interfeysini realizatsiya qiladigan sinf yaratish hisoblanadi.