အခန်း ၈ :: 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 လုပ်ပြီး ထည့်ပါ။

ဘာနဲ့ တူ သလဲ ဆိုတော့ ကျွန်တော်တို့တွေ အနေနဲ့ ခေါင်းပြောင်းလို့ ရသည့် ဝက်အူလှည့်တွေ နဲ့ တူပါတယ်။ + size အမျိုးမျိုးအတွက် base က အတူတူပါပဲ။ - size အမျိုးမျိုးအတွက် base အတူတူပါပဲ။ base ကို မပြင်ပဲ extend လုပ်ပြီး သုံးသည့် သဘောပါ။

class Dog
{
    public function bark(): string
    {
        return 'woof woof';
    }
}

class Duck
{
    public function quack(): string
    {
        return 'quack quack';
    }
}
class Communication
{
    public function communicate($animal): string
    {
        switch (true) {
            case $animal instanceof Dog:
                return $animal->bark();
            case $animal instanceof Duck:
                return $animal->quack();
            default:
                throw new \InvalidArgumentException('Unknown animal');
        }
    }
}

ကျွန်တော်တို့ ရဲ့ Dog နဲ့ Duck ဟာ exten လုပ်လို့မရသည့် အတွက် Communication class မှာ instance ကို ကြည့်ပြီး ပြန်ရေးထားရပါတယ်။ တနည်းပြောရင် base က exten လုပ်ဖို့ အဆင်မပြေဘူးပေါ့။

အဲဒီ အတွက်

interface Communicative
{
    public function speak(): string;
}

ကျွန်တော်တို့ interface တစ်ခု ဖန်တီးထားပါတယ်။ Speak ဆိုသည့် abstract function ပါတယ်။

class Dog implements Communicative
{
    public function speak(): string
    {
        return 'woof woof';
    }
}

class Duck implements Communicative
{
    public function speak(): string
    {
        return 'quack quack';
    }
}

Dog နဲ့ Duck က သူ့ကိုယ်ပိုင် function တွေ ရှိပေမယ့် Communicative interface ကို သုံးပြီးတော့ အဲဒီ မှာ speak function ကို ပေါင်းထည့်ထားပါတယ်။

class Communication
{
    public function communicate(Communicative $animal): string
    {
        return $animal->speak();
    }
}

အခုဆိုရင် Communication class က ပိုရှင်းပြီး သပ်ရပ်သွားပါပြီ။

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 က recetangle 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 ကို မလိုက်နာ ထားဘူးလို့ ပြောနိုင်ပါတယ်။

LSP ကို လိုက်နာအောင် CalculableArea interface တစ်ခု တည်ဆောက်ပြီး ဖြေရှင်းလို့ ရပါတယ်။

<?php

class Rectangle implements CalculableArea
{
    protected int $width;
    protected int $height;

    public function __construct(int $width, int $height)
    {
        $this->width = $width;
        $this->height = $height;
    }

    public function calculateArea(): int
    {
        return $this->width * $this->height;
    }
}

class Square implements CalculableArea
{
    protected int $edge;

    public function __construct(int $edge)
    {
        $this->edge = $edge;
    }

    public function calculateArea(): int
    {
        return $this->edge ** 2;
    }
}

interface CalculableArea
{
    public function calculateArea();
}

class RectangleTest extends TestCase
{
    public function testCalculateArea()
    {
        $shape = new Rectangle(10, 2);
        $this->assertEquals($shape->calculateArea(), 20);

        $shape = new Rectangle(5, 2);
        $this->assertEquals($shape->calculateArea(), 10);
    }
}


class SquareTest extends TestCase
{
    public function testCalculateArea()
    {
        $shape = new Square(10);
        $this->assertEquals($shape->calculateArea(), 100);

        $shape = new Square(5);
        $this->assertEquals($shape->calculateArea(), 25);
    }
}

Interface segregation principle (ISP)

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

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

ကျွန်တော်တို့ မြန်မာနိုင်ငံ က ဖုန်းအားသွင်းသည့် ကြိုးတွေမှာ တွေ့ရပါတယ်။ ကြိုးတစ်ချောင်းထဲမှာ iPhone, Micro USB , USB C အစုံ ပါနေတာမျိုးပေါ့။​ iPhone အတွက်ပဲ အားသွင်းကြိုးဝယ်သည့် အခါမှာ တစ်ခြားဟာတွေ အတွက်ပါလာသည့် ကြိုးတွေက အားသွင်းချိန်မှာ ရှုပ်နေတာပဲ အဖတ်တင်ပါတယ်။ iPhone အတွက် iPhone အားသွင်းဖို့ တစ်ခုတည်း ပါတာ က ပိုအဆင်ပြေပါတယ်။

အဲဒီ သဘောတရားပါပဲ။

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

Exportable interface မှာ getPDF , getCSV ဆိုပြီး abstrct 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 ထည့်ရေးခိုင်းထားသည့် သဘောမျိုး ဖြစ်နေပါတယ်။ ဒါဟာ ISP ကို မလိုက်နာတာပါ။

တကယ်တန်း ဖြစ်သင့်သည့် code က

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 လုပ်ကြည့်ပါ။​