Principles of Software Design

สรุป design software principle สำหรับเป็น guidelines เพื่อใช้ในการ design software ซึ่งโดยทั่วไปแล้ว pattern และ practices ก็เหมือนเครื่องเมื่อ (tools) เพื่อที่จะทำให้สามารถสร้าง software ที่มีคุณภาพ ตัวอย่างเช่น

  • KISS – Keep it simple, stupid ใช้หลักการออกแบบที่ง่าย ไม่ซับซ้อน จะทำให้ software ทำงานได้ดีกว่า
  • DRY – Don’t repeat yourself หลีกเลี่ยงการทำอะไรซ้ำๆ หรือมีส่ิงเดียวกันหลายๆ ที่ ทำให้แก้ไขลำบาก
  • YAGNI – You aren’t gonna need it ไม่ควรพัฒนาหรือเพิ่ม function ใดๆ จนกว่าจะเห็นว่าจำเป็นจริงๆ
  • SoC – Separation of concerns หลักการออกแบบ software เป็นลักษณะ modular ที่ทำงานใดงานหนึ่งสมบูรณ์ในตัวเอง
  • SOLID – Single responsibility, Open/Close, Liskov substitution, Interface segregation, Dependency inversion เป็นหลักการในการเขียน object-oriented programming (OOP) ที่จะทำให้ software มีโครงสร้างที่ดี และแก้ไขได้ง่าย

Single Responsibility Principle – หลักการออกแบบ Class

หลักการที่เรามีแค่เหตุผลเดียวในการสร้าง class ขึ้นมาเพื่อทำงานใดงานหนึ่ง ไม่ควรให้ class ที่สร้างขึ้นมาทำงานหลายหน้าที่ (responsibility) เช่นออกแบบ class เพื่อหาพื้นที่รวมของรูปทรงต่างๆ ก็ไม่ควรให้ class นี้จะต้องทำเรื่องการแสดงผล (output format) ที่ได้ออกมาในรูป html หรือ json เพราะจะทำให้ class นี้ถูกสร้างขึ้นมาด้วยเหตุผลมากกว่าหนึ่งเหตุผล หรือถูกใช้หรือ support เฉพาะบางกลุ่มหรือบาง role

Open Closed Principle – หลักการออกแบบ Interface

Object หรือ entities ต้องสามารถ extend ได้ แต่ต้องแก้ไขไม่ได้ เป็นหลักการที่ทำให้โครงสร้างของ code เดิมไม่กระทบเมื่อมี type หรือ object ที่แตกต่างออกไป เช่น การที่เรามี class สำหรับรวมพื้นรวมของรูปทรงสี่เหลี่ยม กับวงกลม ด้วย method sum() ถ้าเรามีสามเหลี่ยมเพิ่มขึ้นมาก็จะหลีกเลี่ยงการแก้ไข method เดิมไม่ได้ เราสามารถแก้ไขได้ด้วยการสร้าง interface shape โดยให้ type object ใดๆ สามารถ extend ไปเพื่อ implement logic หา area ตัวเอง ก็จะทำให้ mothod sum() ของเราก็ไม่ต้องแก้ไขอะไร เพื่อที่จะ support รูปทรงใหม่ๆ

interface ShapeInterface
{
    public function area();
}
class Square implements ShapeInterface
{
    // ...
}
class Circle implements ShapeInterface
{
    // ...
} 
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 Substitution – หลักการออกแบบ Inheritance

เป็นหลักการที่ object ของ supper class จะต้องสามารถแทนทีด้วย object ของ subclass ได้โดยที่ต้องไม่ส่งผลต่อ program และ object ของ subclass จะต้องสามารถ access ทุก method และ property ของ super class

public interface Bird{
    public void fly();
    public void walk();
}

public class Parrot implements Bird{
    public void fly(){ // to do}
    public void walk(){ // to do }
}// ok 

public class Penguin implements Bird{
    public void fly(){ // to do }
    public void walk(){ // to do }
}  // it's break the principle of LSP. Penguin can not fly.

จากตัวอย่าง code จะเห็นว่า subclass สามารถ access superclass ได้ แต่จะเห็นว่า penguin ที่เป็น subclass เข้ากันไม่ได้กับ superclass เพราะ penguin ไม่สามารถบินได้จึงผิดหลัก Liskov substitution

public interface Bird{
    // to do;
}

public interface FlyingBird extends Bird{
    public void fly(){}
}

public interface WalkingBird extends Bird{
    public void work(){}
}

public class Parrot  implements FlyingBird, WalkingBird {
    public void fly(){ // to do}
    public void walk(){ // to do }
}

public class Penguin implements WalkingBird{
    public void walk(){ // to do }

ถ้าเปลี่ยนใหม่ให้ penguin สือทอดจาก walkingbird ก็จะทำให้ถูกต้องตามหลักการ และไม่ส่งผมต่อ program ทำให้เกิด bug

Interface segregation – หลักการออกแบบ Polymorphism

code จะต้องไม่ถูกบังคับให้ implement หรือเกี่ยวพันกับ method ที่ไม่ได้ใช้ เช่นกรณีที่เรามี class interface ที่มี method คำนวณพื้นที่ และปริมาตรสามมิติ ซึ่งถ้า type ของสี่เหลี่ยมเป็น 2 มิติก็จะไม่สามารถ implement method คำนวณปริมาตรได้เป็นต้น

Dependency Inversion – หลักการออกแบบ Decoupling and Abstraction

หลักการในการจัดการ dependency ของสอง object ที่เมื่อถ้าต้องเปลี่ยนไปใช้ อีก object จะทำได้ง่ายๆ โดยไม่กระทบกับ code ด้วยวิธีการใช้ intermediate object เพื่อเชื่มระหว่างทั้งสอง object เข้าด้วยกัน แทนที่จะให้ทั้งสอง object มีการเรียกใช้กันตรงๆ

public class Book {

    void seeReviews() {
         ...
    }

    void readSample() {
         ...
    }
}


public class Shelf {

     Book book;

     void addBook(Book book) {
          ...
     }

     void customizeShelf() {
          ...
     }
}

ตัวอย่างนี้ถ้าเราสร้าง shelf ไว้เก็บหนังสือ (book) อนาคตถ้าต้องการเก็บ DVD ด้วยก็เลี่ยงไม่ได้ที่ต้องแก้ class shelf เพราะความสัมพันธ์ที่ขั้นตรงต่อกันของทั้งสอง object

public interface Product {

    void seeReviews();

    void getSample();

}

public class Book implements Product {

    @Override
    public void seeReviews() { 
          ...
    }

    @Override
    public void getSample() {
          ...
    }
}

public class DVD implements Product {

    @Override
    public void seeReviews() { 
         ...
    }

    @Override  
    public void getSample() {
          ...
    }
}


public class Shelf {

    Product product;

    void addProduct(Product product) {
          ...
    }

    void customizeShelf() {
          ...
    }
}

ถ้าไม่ต้องการให้ object มี dependency ต่อกันก็ต้องสร้าง intermediate object ขึ้นมา ตัวอย่างนี้คือ object product ซึ่งเป็น abstraction ของทั้งสอง object ทำให้อนาคตถ้ามี object ใหม่ๆ เกิดขึ้น ก็สามารถเพิ่มเข้า หรือลบออกได้ง่าย

บทความที่น่าสนใจ และศึกษาเพิ่มเติมเพื่อให้เข้าใจมากขึ้นสำหรับ architecture และ software pattern อื่นๆ

Architecture

Design Patterns

Chaos Engineering