အခန်း ၈ :: Principles

Developer တစ်ယောက် အဖြစ် စတော့မယ် ဆိုရင် Principles တွေကို လိုက်နာခြင်းအားဖြင့် code တွေကို ပိုမို သပ်ရပ်စေပါတယ်။ လူသုံးများသည့် Principles တွေကတော့

  • KISS (Keep It Simple, Stupid)
  • DRY (Don’t Repeat Yourself)
  • YAGNI (You Aren’t Gonna Need It)
  • SOLID Principles

ဒီအထဲမှာ အရေးပါဆုံးကတော့ SOLID Princples ပါ။

KISS (Keep It Simple, Stupid)

Software တစ်ခုကို ဖန်တီးရေးသားသည့် အခါမှာ ရိုးရှင်းဖို့ လိုပါတယ်။ ကျွန်တော် junior developer ဘဝ တုန်းက ကိုယ်ရေးထားသည့် code တွေ အခြားသူတွေ နားမလည်ရင် တော်တော်ကောင်းသည့် code လို့ ထင်ဖူးပါတယ်။ ဒါဟာ တကယ်တော့ လုံးဝ မှားယွင်းနေတာပါ။ ကိုယ့် code ကို ဘယ် developer မဆို ဖတ်နိုင်ဖို့ နဲ့ နားလည်လွယ်ကူအောင် အရိုးရှင်းဆုံး ရေးထားမှ ဖြစ်မှာပါ။ သို့ပေမယ့် Code တွေဟာ SOLID principles ကိုတော့ အနည်းဆုံး လိုက်နာ ထားဖို့ လိုပါတယ်။ ကိုယ့်ရဲ့ code တွေဟာ လွယ်ကူစွာ နားလည်ဖို့ လိုတယ်​ ၊ မလိုအပ်သည့် ရှုပ်ထွေးမှုတွေကို ရှောင်ရှားဖို့ လိုတယ် ၊​ လွယ်လွယ်ကူကူ extend လုပ်နိုင်ဖို့ လိုပါတယ်။

DRY (Don’t Repeat Yourself)

ခေါင်းစဥ် ဖတ်လိုက်တာနဲ့ ရှင်းပါတယ်။ ကိုယ်ဟာ junior level မဟုတ်တော့ဘူးလား junior level လား ဆိုတာ ကို စစ်ဖို့ ကိုယ်ရေးထားသည့် project တွေမှာ duplicate code တွေ ရှိနေလား ဆိုပြီး စစ်ကြည့်လိုက်ပါ။​ တူညီသည့် code တွေကို ထပ်ခါ ထပ်ခါ မရေးပဲ သက်ဆိုင်ရာ function ဖြစ်စေ class တွေ ဖြစ်စေ ခွဲထုတ်ပြီး ရေးသားထားဖို့ လိုပါတယ်။ The Pragmatic Programmer စာအုပ်ထဲမှာ အောက်ကလို ဖော်ပြထားပါတယ်။

Every piece of knowledge must have a single, unambiguous, authoritative representation within a system ‐ The Pragmatic Programmer

DRY principle ဟာ code တွေ ကို reusability ဖြစ်ပြီး ထိန်းသိမ်းပြုပြင်ရတာ ပိုမို လွယ်ကူစေပါတယ်။

YAGNI (You Aren’t Gonna Need It)

KISS လိုပါပဲ။ Program တစ်ခုမှာ မလိုအပ်သည့် function ကို မထည့်ပါနဲ့။ တကယ်လိုအပ်တယ် ဆိုမှသာ ထည့်ပါ။ feature တစ်ခု သို့မဟုတ် function တစ်ခု ထည့်ဖို့ တကယ်လိုအပ်ပြီဆိုမှ ထည့်သွင်းဖို့ပါပဲ။

do the simplest thing that could possibly work

SOLID

Programmer တိုင်း မဖြစ်နေ သိကို သိရမည့် principle ပါ။ အရမ်းကို အသုံးဝင်ပြီး အတတ်နိုင်ဆုံး programmer တိုင်း လိုက်နာကြပါတယ်။ SOLID အရှည်ကောက်ကတော့

  • Single Responbility Principle
  • Open Close Principle
  • Liskov Substitution Principle
  • Interface Segregation Principle
  • Dependency Inversion Principle

Single Responbility Principle (SRP)

A class should have one, and only one, reason to change.

Class တစ်ခုဟာ အလုပ်တစ်ခု ကို ပဲ လုပ်သင့်ပါတယ်။ ဥပမာ ပစ္စည်းတစ်ခုမှာ တူ နဲ့ ဝက်အူလှည့် အတူတူ တွဲထားတာ ထက် သီးသန့်ခွဲထားတာ ပို ပြီး အလုပ်ဖြစ်ပါတယ်။ တူ က တူ အလုပ်လုပ်ပြီး ဝက်အူလှည့် က သူ့ အလုပ်သူလုပ်ဖို့ပါပဲ။

ဥပမာ ကြည့်ရအောင်

class Book {
	public function save() {
	}

	public function update() {
	}
	
	public function share() {
	}
}

ဒီ code မှာ ဆိုရင် share ဆိုသည့် function ဟာ Book class နဲ့ တိုက်ရိုက် တိုက်ဆိုင် ခြင်း မရှိပါဘူး။ Book Class မှာ share function မလိုအပ်ပါဘူး။

class ShareService {
	public function share() {
	}
}

share function ကို သီးသန့် ခွဲထုတ် ရေးဖို့ ShareService ကို ခွဲရေးလိုက်ပါမယ်။ ဒါဆိုရင် Book class မှာ စာအုပ် နဲ့ ဆိုင်သည့် responsibility ပဲ ရှိပါတော့တယ်။

Open Close Principle (OCP)

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification

Object ဟာ extension လုပ်ဖို့ အတွက် ဖွင့်ထားပြီးတော့ class ထဲမှာ ရေးထားသည့် code တွေကိုတော့ ပြင်ဆင်ခွင့်မရှိပါဘူး။

Class ထဲမှာ function အသစ်ထပ်ဖြည့်မယ်ဆိုရင် ရေးသားပြီးသား class ကို မပြင်ပဲ class ကို extend လုပ်ပြီး ထည့်ပါ။

 ဥပမာ ကြည့်ရအောင်။

class Login
{
    public function googleLogin()
    {
        echo "Login with google";
    }
 
    public function FacebookLogin()
    {
        echo "Login with Facebook";
    }
}
class LoginController
{
    public function login($type)
    {
        $login = new Login();
        if ($type == "google") {
           $login->googleLogin();           
        }
        else if ($type == "facebook") {
            $login->facebookLogin();
        }
    }
}

ဒီ code ကို ကြည့်ကြည့်ပါ။ နောက်ထပ် Apple Login ထပ်ဖြည့်ချင်ရင် Login Class ကော LoginController ကော ပြင်ရမှာ ကို တွေ့ရမှာပါ။ ဒါဟာ ကောင်းမွန်သည့် code မဟုတ်ပါဘူး။ Open Close Principle အရ Open for Extension, Close for Modification ဖြစ်ရမှာပါ။ ဒါကြောင့် ကျွန်တော်တို့ ဟာ interface နဲ့ ခွဲထုတ် ပြီး extendable ဖြစ်အောင် လုပ်ပါမယ်။

interface LoginInterface
{
    public function login()
}
class GoogleLogin implements LoginInterface
{
    public function login() {
        echo "Google Login";
    }
}

class FacebookLogin implements LoginInterface
{
    public function login() {
        echo "Facebook Login";
    }
}

ဒါဆိုရင် LoginInterface ကို LoginController က သိဖို့ပဲ လိုတော့တယ်။ ဘာ Login type လည်း ဆိုတာကို သိဖို့ မလိုတော့ သလို နောက်ပိုင်း Apple Login ရှိလည်း AppleLogin class ကို LoginInterface သုံးပြီး ပြန်ပြင်နိုင်ပါတယ်။

class LoginController
{
     public function login(LoginInterface $type) {
          $type->login();
     }
}

ဒါဆိုရင် LoginController ကို လှမ်းခေါ်သည့် အခါမှာ

$conroller = new LoginController();
$controller->login(new GoogleLogin());

ဆိုပြီး ခေါ်နိုင်ပါတယ်။

Liskov substitution principle (LSP)

MIT က Professor Barbara Liskov က စပြီး အဆိုပြုခဲ့သည့် အတွက် LSP ဆိုပြီး ဖြစ်လာတာပါ။

Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program

Class တစ်ခု က အခြား class တစ်ခု ရဲ့ အမွေ ဆက်ခံသည့် အခါမှာ အလုပ်လုပ်သည့် ပုံစံ ကို မပြောင်းလဲ စေဖို့လိုပါတယ်။

class Square extends Rectangle
{
    public function setWidth(int $width): void { 
        $this->width = $width;
        $this->height = $width;
    }
 
    public function setHeight(int $height): void {
        $this->width = $height;
        $this->height = $height;
    }
}

Square class က rectangle class ကို extends လုပ်ထားပါတယ်။ setWidth/setHeight ထည့်လိုက်သည့် အခါမှာ width ကော height ကော အတူတူထည့်ထားပါတယ်။

ဒီ code က Liskov substitution principl ကို မလိုက်နာထားတာကို တွေ့ရပါတယ်။ Rectangle မှာ setHeight က height ကို ပဲ​ပြောင်းလဲ သလို setWidth က width ကိုပဲ ပြောင်းလဲတာပါ။ ဒါပေမယ့် လက်ရှိ code မှာ ၂ ခု လုံးကို ပြောင်းလဲ ထားတာ တွေ့နိုင်ပါတယ်။

public function testCalculateArea()
{
    $shape = new Rectangle();
    $shape->setWidth(10);
    $shape->setHeight(2);
 
    $this->assertEquals($shape->calculateArea(), 20);
 
    $shape->setWidth(5);
    $this->assertEquals($shape->calculateArea(), 10);
}

ဒီလို test case မှာ ကြည့်လိုက်ရင် ရှင်းပါမယ်။ Width ပြောင်းသည့် အခါမှာ area ပြောင်းသွားပါတယ်။ ဒါပေမယ့် Square က မပြောင်းနိုင်ပါဘူး။​ ဒါကြောင့် code က LSP ကို မလိုက်နာ ထားဘူးလို့ ပြောနိုင်ပါတယ်။

နောက်ထပ် ဥပမာ ကြည့်ရအောင်။

<?php

interface PaymentMethod {
    public function processPayment();
}

class CreditCardPayment implements PaymentMethod {
    public function processPayment() {
        // Implement the payment processing logic for credit cards
    }
}

class DebitCardPayment implements PaymentMethod {
    public function processPayment() {
        // Implement the payment processing logic for debit cards
    }
}

class CashPayment implements PaymentMethod {
    public function processPayment() {
        throw new Exception("Cash payments are not supported");
    }
}

class Checkout {
    public function processPayment(PaymentMethod $paymentMethod) {
        try {
            $paymentMethod->processPayment();
        } catch (Exception $e) {
            // Handle the exception for unsupported payment methods
            echo "Error: " . $e->getMessage();
        }
    }
}

// Example usage
$checkout = new Checkout();
$paymentMethod = new CashPayment(); // Try changing to CreditCardPayment or DebitCardPayment

$checkout->processPayment($paymentMethod);

ဒီ code က LSP ကို လိုက်မနာထားဘူး။ ဘာဖြစ်လို့လည်းဆိုတော့ Cash Payment ကို အသုံးပြုသည့် အခါမှာ Cash Payment မှာ processPayment ဆိုသည့် function က အလုပ်မလုပ်လို့ပဲ။ LSP သဘောတရားက ဘယ် class ပဲ ပြောင်းပြောင်း အလုပ်လုပ်သည့် သဘောတရား တူညီ နေရမယ်။ ဒီ class မှာတော့ မလုပ်ဘူး။ ဒီ class ကို သုံးချင်ရင်တော့ ဒီလို ရေးဆိုပြီး လုပ်လို့မရပါဘူး။ Class အကုန်လုံးက အလုပ်လုပ်ပုံ တူညီနေရမယ်။

Interface segregation principle (ISP)

Clients should not be forced to depend upon interfaces that they do not use

ရေးထားတာကတော့ ရှင်းပါတယ်။ Client မသုံးသည့် function ကို အတင်းသုံးခိုင်းထားတာ မျိုး မဖြစ်စေရပါဘူး။

interface Exportable
{
    public function getPDF();
    public function getCSV();
}

Exportable interface မှာ getPDF , getCSV ဆိုပြီး abstract function ရေးထားတာ တွေ့နိုင်ပါတယ်။​

class Invoice implements Exportable
{
    public function getPDF() {
        // ...
    }
    public function getCSV() {
        // ...
    }
}

class CreditNote implements Exportable
{
    public function getPDF() {
        throw new \NotUsedFeatureException();
    }
    public function getCSV() {
        // ...
    }
}

Invoice အတွက် function ၂ ခု လုံးက အလုပ်လုပ်ပေမယ့် CreditNote အတွက် PDF ထုတ်ဖို့ မလိုဘူး။ ဒါပေမယ့် Exportable interface က getPDF ကို မဖြစ်မနေ function ထည့်ရေးခိုင်းထားသည့် သဘောမျိုး ဖြစ်နေပါတယ်။ ဒါဟာ LSP ကို မလိုက်နာတာပါ။

LSP ကို ဖြေရှင်းဖို့ Interface segration ကို အသုံးပြုနိုင်တယ်။ လိုသည့် function ပဲ interface မှာ ကြေငြာပါ ဆိုသည့် သဘောပဲ။ မကြေငြာထားသည့် function ကို class ထဲမှာ အတင်း မသုံးခိုင်းနဲ့။ ဒါကြောင့် လိုအပ်တာတွေကို interface ခွဲထုတ်ဖြစ်ဖို့ပါ။

interface ExportablePdf
{
    public function getPDF();
}

interface ExportableCSV
{
    public function getCSV();
}

class Invoice implements ExportablePdf, ExportableCSV
{
    public function getPDF() {
        //
    }
    public function getCSV() {
        //
    }
}

class CreditNote implements ExportableCSV
{
    public function getCSV() {
        //
    }
}

ဒါဆိုရင် မလိုအပ်သည့် function ပါနေဖို့ မလိုတော့ပါဘူး။

Dependency inversion principle (DIP)

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
  2. Abstractions should not depend upon details. Details should depend upon abstractions.

High-level moduels တွေဟာ low-level modules တွေ အပေါ် depend မလုပ်သင့်ပါဘူး။​ အဲဒီ အစား abstractions ကို depend လုပ်ဖို့ လိုပါတယ်။

Abstractions တွေဟာ details ပေါ်မှာ depend မလုပ်သင့်ပါဘူး။​ Details က သာ abstractions ပေါ်မှာ depend လုပ်ဖို့ လိုအပ်ပါတယ်။

class DatabaseLogger
{
    public function logError(string $message)
    {
        // ..
    }
}
class MailerService
{
    private DatabaseLogger $logger;
 
    public function __construct(DatabaseLogger $logger)
    {
        $this->logger = $logger;
    }
 
    public function sendEmail()
    {
        try {
            // ..
        } catch (SomeException $exception) {
            $this->logger->logError($exception->getMessage());
        }
    }
}

ဒီ class မှာ mail service ဟာ DatabaseLogger ကို တိုက်ရိုက် သုံးထားတယ်။ အကယ်၍ ကျွန်တော်တို့ Database မသုံးပဲ File သုံးမယ် ဒါမှမဟုတ် အခြား thrid party log service သုံးမယ် ဆိုရင် ဘယ်လိုလုပ်မလဲ။ ဒါကြောင့် MailService က DIP ကို ချိုးဖောက်နေပါတယ်။ Log အတွက် သီးသန့် interface တစ်ခု သုံးသင့်ပါတယ်။

interface LoggerInterface
{
    public function logError(string $message): void;
}

class DatabaseLogger implements LoggerInterface
{
   public function logError(string $message): void
   {
       // ..
   }
}

class MailerService
{
    private LoggerInterface $logger;
 
    public function sendEmail()
    {
        try {
            // ..
        } catch (SomeException $exception) {
            $this->logger->logError($exception->getMessage());
        }
    }
}

ဒါဆိုရင် LoggerInterface ဟာ database လည်း ဖြစ်နိုင်သလို File လည်း ဖြစ်နိုင်သလို third party တွေလည်း ဖြစ်လို့ရသွားပါပြီ။

အခု ဆိုရင် SOLID ကို နားလည်သဘောပေါက်ပြီး လက်ရှိ ရေးထားသည့် code တွေကို ပြန်ပြီး review လုပ်ကြည့်ပါ။ အသစ်ရေးမယ့် code တွေကို အတတ်နိုင်ဆုံး follow လုပ်ကြည့်ပါ။​