#สร้าง Object ทีละขั้นอย่างเป็นระบบ

info-builder-pattern-with-java-object

เมื่อคลาสมีตัวเลือกจำนวนมาก การสร้าง object ด้วย constructor เพียงบรรทัดเดียวอาจทำให้ผู้อ่านไม่รู้ว่าค่าแต่ละตำแหน่งหมายถึงอะไร Builder Pattern ช่วยย้ายรายละเอียดการกำหนดค่าไปยังเมธอดที่สื่อความหมาย แล้วสร้าง object ผลลัพธ์เมื่อเรียก build()

Computer workstation = new Computer.Builder("Ryzen 7", 32)
        .storageGB(1024)
        .gpu("Dedicated GPU")
        .addPort("USB-C")
        .addPort("HDMI")
        .build();

จากโค้ดนี้ ผู้อ่านเห็นทันทีว่าเครื่องใช้ RAM 32 GB, Storage 1,024 GB และมีพอร์ตอะไรบ้าง โดยไม่ต้องเปิดดู constructor เพื่อไล่ความหมายของทุกตำแหน่ง

บทความนี้ใช้ Java 17 เป็นฐาน ไม่ใช้ไลบรารีภายนอก และประกอบด้วย workshop ที่คัดลอกโค้ดไปรันได้

#1. Builder Pattern คืออะไร

Builder เป็น Creational Design Pattern หรือรูปแบบการออกแบบที่เกี่ยวข้องกับการสร้าง object แนวคิดสำคัญคือแยกกระบวนการประกอบ object ออกจาก object ผลลัพธ์

Builder สะสมค่าหรือทำขั้นตอนการสร้าง ส่วน Product คือ object ที่ผู้ใช้ได้รับเมื่อการสร้างเสร็จสมบูรณ์

ในรูปแบบดั้งเดิมของ GoF การแยกกระบวนการสร้างยังเปิดให้ใช้ขั้นตอนการสร้างชุดเดียวกันกับ Concrete Builder ที่สร้างผลลัพธ์ต่างรูปแบบได้ ส่วนรูปแบบที่ใช้บ่อยใน Java คือ nested fluent builder ซึ่งเน้นการกำหนดพารามิเตอร์ให้อ่านง่าย และจัดการตัวเลือกจำนวนมาก บทความนี้ลงมือทำรูปแบบหลัง แล้วเชื่อมโยงกลับไปยังบทบาทของรูปแบบดั้งเดิม

การต่อเมธอดหลายครั้งเป็นเพียงรูปแบบการเรียก API ที่เรียกว่า method chaining ไม่ได้ทำให้ทุก API กลายเป็น Builder Pattern โดยอัตโนมัติ

#2. ปัญหาก่อนใช้ Builder

#Constructor มีพารามิเตอร์จำนวนมาก

สมมติว่าคลาสเดิมมี constructor ดังนี้

// ตัวอย่าง API เดิมเพื่อแสดงปัญหา ไม่ใช้กับ Computer ใน workshop
Computer computer = new Computer(
        "Ryzen 7", 32, 1024, "Dedicated GPU", true, false
);

ค่า true และ false อ่านยาก เพราะชื่อของพารามิเตอร์ไม่ปรากฏที่จุดเรียกใช้งาน แม้ IDE บางตัวจะแสดง parameter hints ก็ยังเกิดความสับสนเมื่ออ่าน diff หรือเอกสาร

#เพิ่ม constructor หลายรูปแบบ

Computer(String cpu, int ramGB)
Computer(String cpu, int ramGB, int storageGB)
Computer(String cpu, int ramGB, int storageGB, String gpu)

วิธีนี้มักเรียกว่า telescoping constructors เมื่อ optional fields เพิ่มขึ้น จำนวนชุด constructor และภาระการดูแลก็อาจเพิ่มตามไปด้วย

#ใช้ setter หลังสร้าง object

// ตัวอย่างแนวทาง JavaBeans เพื่อเปรียบเทียบ
Computer computer = new Computer();
computer.setCpu("Ryzen 7");
computer.setRamGB(32);
computer.setStorageGB(1024);

แนวทาง setter เหมาะกับ object ที่ต้องแก้ไขค่าได้ แต่ถ้าระบบต้องการ object ที่สมบูรณ์ตั้งแต่พร้อมใช้งาน ก็ต้องจัดการช่วงเวลาที่ object ยังได้รับค่าไม่ครบ รวมถึงการป้องกันการเปลี่ยนค่าภายหลัง

Builder ช่วยให้เก็บสถานะระหว่างเตรียมค่าไว้ใน Builder และส่งออกเฉพาะ Product ที่ผ่านเงื่อนไขแล้ว

#3. บทบาทสำคัญ

บทบาท หน้าที่ ตัวอย่างในบทความ
Product เก็บผลลัพธ์ที่สร้างเสร็จแล้ว Computer
Builder กำหนดขั้นตอนหรือค่าที่ใช้สร้าง Product Computer.Builder ซึ่งรวมบทบาทของ builder และ implementation ไว้ในคลาสเดียว
Client เลือกค่าหรือสั่งขั้นตอนการสร้าง Main
Director จัดลำดับขั้นตอนหรือกำหนดสูตรสร้างที่ใช้ซ้ำ ComputerDirector ในตัวอย่างเสริม

ในตัวอย่างหลัก Client เรียก Builder โดยตรง จึงไม่ต้องมี Director ส่วนแบบ GoF ที่มี builder หลายชนิดมักแยก abstraction ของ Builder ออกจาก Concrete Builder

classDiagram
    class Main {
        +main(String[] args)
    }
    class Builder {
        +Builder(String cpu, int ramGB)
        +storageGB(int value) Builder
        +gpu(String value) Builder
        +addPort(String value) Builder
        +build() Computer
    }
    class Computer {
        -String cpu
        -int ramGB
        -int storageGB
        -String gpu
        -List~String~ ports
        +getPorts() List~String~
    }
    Main ..> Builder : configures
    Builder ..> Computer : creates

Builder ในแผนภาพหมายถึง nested class Computer.Builder ไม่ใช่คลาสแยกไฟล์

#4. เป้าหมายของ Workshop

ใช้เวลาประมาณ 45–60 นาที ผู้เรียนจะได้สร้างคลาสที่มีคุณสมบัติดังนี้

  • บังคับให้ผู้เรียกส่ง CPU และ RAM เข้ามาเมื่อเริ่ม Builder
  • กำหนดค่าเริ่มต้นให้ Storage, GPU และรายการพอร์ต
  • ต่อเมธอดเพื่อเลือกค่าได้
  • ปฏิเสธข้อมูลที่ไม่ถูกต้องก่อนคืน Product
  • สร้าง Product ที่แก้ไขค่าผ่าน API ไม่ได้
  • ใช้ Builder เดิมสร้าง Product ใหม่ โดย Product เดิมยังคงค่าเดิม

#เครื่องมือที่ต้องมี

ติดตั้ง JDK 17 หรือใหม่กว่า และตรวจสอบคำสั่ง

java -version
javac -version

ต้องมี JDK ที่มี compiler เพื่อใช้คำสั่ง javac

#ไฟล์ที่ใช้

ไฟล์ เนื้อหา
Computer.java Product และ nested Builder
Main.java ตัวอย่างการใช้งาน
ComputerDirector.java ตัวอย่างเสริมสำหรับสูตรสร้างที่ใช้ซ้ำ

วางไฟล์ไว้ในโฟลเดอร์เดียวกัน ตัวอย่างนี้ไม่ประกาศ package เพื่อให้เริ่มรันได้ง่าย

#5. ออกแบบข้อมูลก่อนเขียนโค้ด

Field ชนิดข้อมูล บังคับส่งตอนเริ่ม Builder ค่าเริ่มต้น เงื่อนไข
cpu String ใช่ ไม่มี ต้องไม่เป็น null หรือข้อความว่าง
ramGB int ใช่ ไม่มี มากกว่า 0
storageGB int ไม่ 256 มากกว่า 0
gpu String ไม่ Integrated ต้องไม่เป็น null หรือข้อความว่าง
ports List<String> ไม่ รายการว่าง ชื่อพอร์ตต้องไม่เป็น null หรือข้อความว่าง

การบังคับรับค่าผ่าน constructor ของ Builder ทำให้ผู้เรียกต้องระบุอาร์กิวเมนต์ แต่ยังไม่รับประกันว่าค่านั้นถูกต้อง ตัวอย่างเช่น RAM เท่ากับ 0 ยังต้องถูกตรวจสอบตอนสร้าง Product

#6. สร้าง Product และ Builder

สร้างไฟล์ Computer.java

import java.util.ArrayList;
import java.util.List;

public final class Computer {
    private final String cpu;
    private final int ramGB;
    private final int storageGB;
    private final String gpu;
    private final List<String> ports;

    private Computer(Builder builder) {
        this.cpu = requireText(builder.cpu, "cpu");
        this.gpu = requireText(builder.gpu, "gpu");

        if (builder.ramGB <= 0) {
            throw new IllegalArgumentException("ramGB must be > 0");
        }
        if (builder.storageGB <= 0) {
            throw new IllegalArgumentException("storageGB must be > 0");
        }

        this.ramGB = builder.ramGB;
        this.storageGB = builder.storageGB;
        this.ports = List.copyOf(builder.ports);
    }

    private static String requireText(String value, String name) {
        if (value == null || value.isBlank()) {
            throw new IllegalArgumentException(name + " must not be blank");
        }
        return value.trim();
    }

    public String getCpu() {
        return cpu;
    }

    public int getRamGB() {
        return ramGB;
    }

    public int getStorageGB() {
        return storageGB;
    }

    public String getGpu() {
        return gpu;
    }

    public List<String> getPorts() {
        return ports;
    }

    @Override
    public String toString() {
        return "Computer{cpu='" + cpu + "', ramGB=" + ramGB
                + ", storageGB=" + storageGB + ", gpu='" + gpu
                + "', ports=" + ports + "}";
    }

    public static final class Builder {
        private final String cpu;
        private final int ramGB;
        private int storageGB = 256;
        private String gpu = "Integrated";
        private final List<String> ports = new ArrayList<>();

        public Builder(String cpu, int ramGB) {
            this.cpu = cpu;
            this.ramGB = ramGB;
        }

        public Builder storageGB(int storageGB) {
            this.storageGB = storageGB;
            return this;
        }

        public Builder gpu(String gpu) {
            this.gpu = gpu;
            return this;
        }

        public Builder addPort(String port) {
            ports.add(requireText(port, "port"));
            return this;
        }

        public Computer build() {
            return new Computer(this);
        }
    }
}

#ทำไมต้องใช้ private Computer(Builder builder)

constructor ของ Product เป็น private เพื่อให้เส้นทางการสร้างที่เปิดเผยต่อผู้ใช้เป็น Builder ภายใน constructor มีการตรวจสอบเงื่อนไข หากค่าไม่ถูกต้องจะโยน IllegalArgumentException และไม่คืน Product ให้ผู้เรียก

การให้ constructor ตรวจสอบข้อมูลยังช่วยรักษาเงื่อนไขของ Product หากเพิ่มเส้นทางการสร้างภายในคลาสในภายหลัง

#ทำไม Builder เป็น static

Builder เป็น static nested class จึงสร้างได้โดยไม่ต้องมี Computer ก่อน เช่น new Computer.Builder(...) ถ้าเป็น non-static inner class การสร้าง Builder จะต้องผูกกับ outer instance ซึ่งไม่เหมาะกับขั้นตอนที่ Product ยังไม่ถูกสร้าง

#ทำไมเมธอดคืน this

public Builder storageGB(int storageGB) {
    this.storageGB = storageGB;
    return this;
}

เมธอดนี้แก้สถานะของ Builder แล้วคืน Builder ตัวเดิม เพื่อให้เรียกเมธอดถัดไปต่อได้ ผลลัพธ์ของ storageGB(...) จึงยังไม่ใช่ Computer

#build() ทำอะไร

public Computer build() {
    return new Computer(this);
}

ทุกครั้งที่เรียก build() จะพยายามสร้าง Computer ตัวใหม่ โดย constructor ของ Computer ตรวจสอบค่าและคัดลอกข้อมูลจาก Builder

ในตัวอย่างนี้ CPU, RAM, Storage และ GPU ตรวจสอบตอน build() ส่วน addPort() ตรวจสอบชื่อพอร์ตทันทีที่เพิ่ม ทั้งสองแนวทางใช้ร่วมกันได้ แต่ควรระบุพฤติกรรมให้ผู้ใช้ API ทราบ

#7. ทดลองใช้งาน

สร้างไฟล์ Main.java

public class Main {
    public static void main(String[] args) {
        Computer office = new Computer.Builder("Core i5", 16)
                .build();

        Computer workstation = new Computer.Builder("Ryzen 7", 32)
                .storageGB(1024)
                .gpu("Dedicated GPU")
                .addPort("USB-C")
                .addPort("HDMI")
                .build();

        System.out.println(office);
        System.out.println(workstation);
    }
}

คอมไพล์และรันจากโฟลเดอร์ที่มีไฟล์ทั้งสอง

mkdir out
javac --release 17 -d out Computer.java Main.java
java -cp out Main

ถ้ามีโฟลเดอร์ out อยู่แล้ว ให้ข้ามคำสั่ง mkdir out

ผลลัพธ์

Computer{cpu='Core i5', ramGB=16, storageGB=256, gpu='Integrated', ports=[]}
Computer{cpu='Ryzen 7', ramGB=32, storageGB=1024, gpu='Dedicated GPU', ports=[USB-C, HDMI]}

ตัวแปร office ใช้ค่าเริ่มต้นทั้งหมดสำหรับ optional fields ส่วน workstation เลือกค่าเพิ่มเติมก่อนสร้าง Product

#8. ตรวจสอบข้อมูลก่อนส่งออก Product

ทดลองแทนที่เนื้อหาใน main() ด้วยโค้ดต่อไปนี้

try {
    Computer invalid = new Computer.Builder("Core i5", 0)
            .build();
    System.out.println(invalid);
} catch (IllegalArgumentException exception) {
    System.out.println(exception.getMessage());
}

ผลลัพธ์

ramGB must be > 0

ในกรณีนี้ Builder ถูกสร้างได้ แต่ build() ไม่คืน Computer เพราะ RAM ไม่ผ่านเงื่อนไข

สำหรับเงื่อนไขที่เกี่ยวข้องกับหลาย field เช่น “ถ้าเปิดใช้ dedicated GPU ต้องมี power supply ขั้นต่ำตามที่กำหนด” ควรตรวจสอบความสัมพันธ์ของ field ก่อนคืน Product เช่นเดียวกัน

#9. ทำให้ Product เป็น Immutable

Builder Pattern ไม่ได้ทำให้ object เป็น immutable โดยอัตโนมัติ ตัวอย่างนี้ออกแบบเพิ่มให้ Computer แก้ไขค่าหลังสร้างผ่าน API ไม่ได้ โดยใช้แนวทางต่อไปนี้

  1. ประกาศ Computer เป็น final เพื่อไม่ให้ subclass เพิ่มพฤติกรรมที่ขัดกับข้อกำหนดนี้
  2. ให้ทุก field ของ Product เป็น private final
  3. ไม่มี setter ใน Product
  4. ไม่เปิดเผย mutable collection ภายในออกไป
  5. เก็บรายการพอร์ตด้วย List.copyOf(builder.ports)

ตามเอกสาร Java วิธี List.copyOf() คืน list ที่แก้ไขโครงสร้างไม่ได้ และการแก้ collection ต้นทางภายหลังจะไม่เปลี่ยน list ผลลัพธ์ [1]

Computer.Builder builder = new Computer.Builder("Core i5", 16)
        .addPort("USB-C");

Computer first = builder.build();

builder.addPort("HDMI");
Computer second = builder.build();

System.out.println(first.getPorts());
System.out.println(second.getPorts());

ผลลัพธ์

[USB-C]
[USB-C, HDMI]

first ไม่เปลี่ยนตาม Builder เพราะมี snapshot ของรายการพอร์ตตอนสร้าง

ทดลองแก้รายการผ่าน getter

first.getPorts().add("LAN");

คำสั่งนี้จะเกิด UnsupportedOperationException

ข้อจำกัดของ List.copyOf() คือไม่ได้คัดลอก object สมาชิกแบบ deep copy ถ้า list เก็บ mutable objects ก็ยังต้องออกแบบการคัดลอกหรือป้องกันสมาชิกเหล่านั้นด้วย [1] ตัวอย่างนี้ใช้ String ซึ่งเป็น immutable จึงไม่มีสมาชิกที่ผู้เรียกเปลี่ยนค่าในภายหลังได้

#10. ใช้ Director เมื่อมีสูตรสร้างที่ทำซ้ำ

ถ้ามีขั้นตอนสร้างที่หลายจุดใช้เหมือนกัน สามารถย้ายสูตรนั้นไปยัง Director ตัวอย่างต่อไปนี้กำหนด Storage, GPU และพอร์ตสำหรับเครื่องสำนักงาน

สร้างไฟล์ ComputerDirector.java

public final class ComputerDirector {
    private ComputerDirector() {
    }

    public static Computer office(Computer.Builder builder) {
        return builder.storageGB(512)
                .gpu("Integrated")
                .addPort("HDMI")
                .build();
    }
}

ตัวอย่างเรียกใช้ภายใน main()

Computer office = ComputerDirector.office(
        new Computer.Builder("Core i5", 16)
);

System.out.println(office);

คอมไพล์โดยเพิ่มไฟล์ใหม่

javac --release 17 -d out Computer.java ComputerDirector.java Main.java
java -cp out Main

ตัวอย่างนี้เป็น Director แบบง่ายที่ทำงานกับ concrete builder เพียงชนิดเดียว หากต้องการรูปแบบ GoF ที่สร้างผลลัพธ์ต่าง representation ให้แยก interface ของขั้นตอนสร้าง แล้วมี Concrete Builder หลายชนิดให้ Director เรียกผ่าน interface นั้น

ควรส่ง Builder ใหม่ให้สูตรนี้แต่ละครั้ง เพราะ addPort() เพิ่มรายการสะสม การส่ง Builder เดิมซ้ำจะทำให้ HDMI ถูกเพิ่มซ้ำ ตัวอย่างไม่ได้ตั้งกฎว่าชื่อพอร์ตต้องไม่ซ้ำ

ถ้าต้องการเพียงฟังก์ชันสะดวกสำหรับสร้างค่ามาตรฐาน การใช้ named factory หรือคลาส preset ก็เป็นอีกทางเลือกหนึ่ง ไม่จำเป็นต้องเพิ่ม Director ให้ทุกระบบ

#11. ตัวอย่าง Builder ใน Java Standard Library

Java มี API ที่ใช้แนวทางนี้อยู่แล้ว เช่น HttpRequest.newBuilder() ซึ่งรับค่าของ HTTP request แล้วคืน HttpRequest เมื่อเรียก build() [2]

import java.net.URI;
import java.net.http.HttpRequest;
import java.time.Duration;

// วางคำสั่งนี้ภายใน main() หรือเมธอดอื่น
HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create("https://example.com/api/products"))
        .timeout(Duration.ofSeconds(10))
        .header("Accept", "application/json")
        .GET()
        .build();

โค้ดนี้สร้าง request object เท่านั้น การส่ง request ต้องเรียกผ่าน HttpClient เพิ่มต่างหาก

#12. เลือก Builder หรือวิธีอื่น

วิธี เหมาะกับกรณี สิ่งที่ต้องพิจารณา
Constructor ข้อมูลน้อยและส่วนใหญ่บังคับ รายการพารามิเตอร์ยาวทำให้อ่านยาก
Static factory ต้องการชื่อที่สื่อความหมายหรือควบคุมชนิด instance ที่คืน ถ้ารับตัวเลือกมากก็ยังพบปัญหาพารามิเตอร์ยาวได้
JavaBeans/setter Object ต้องแก้ไขค่าหลังสร้าง หรือ framework ต้องใช้รูปแบบนี้ ต้องจัดการสถานะที่ข้อมูลอาจยังไม่ครบ
Builder มี optional fields หลายค่า ต้องการ defaults และ validation ก่อนคืน Product ต้องเขียนคลาสและเมธอดเพิ่ม
Java record ต้องการ data carrier แบบกระชับ ไม่ได้มี named optional arguments หรือ Builder ให้โดยอัตโนมัติ และ component ที่เป็น mutable object ยังต้องดูแล

#Builder ต่างจาก Factory อย่างไร

Factory มักเป็นจุดเรียกเพื่อรับ object ที่สร้างตามเงื่อนไขหรือชื่อที่กำหนด ส่วน Builder เปิดให้ผู้เรียกกำหนดค่าหรือประกอบ object หลายขั้นก่อนขอผลลัพธ์ ทั้งสองแนวคิดใช้ร่วมกันได้ เช่น factory method อาจคืน Builder ที่ตั้งค่าพื้นฐานไว้แล้ว

#13. ข้อผิดพลาดที่ควรหลีกเลี่ยง

#คืน Product ตัวเดิมที่ยังเปลี่ยนตาม Builder

ถ้า Builder เก็บ Product ตัวเดียวแล้วแก้ Product นั้นทุกครั้ง การเปลี่ยน Builder หลัง build() อาจกระทบ object ที่ส่งออกไปแล้ว ตัวอย่างนี้จึงสร้าง new Computer(this) ทุกครั้ง

#แชร์ mutable collection โดยตรง

// อย่าทำแบบนี้ใน Product ที่ต้องการให้ immutable
this.ports = builder.ports;

การเก็บ reference เดียวกันทำให้การแก้ list ผ่าน Builder กระทบ Product เดิมได้

#เข้าใจว่า final ทำให้ข้อมูลข้างในแก้ไม่ได้ทั้งหมด

final List<String> ป้องกันการเปลี่ยน reference ของ field แต่ไม่ได้ห้าม add() หรือ remove() บน list นั้น ต้องจัดการ collection เพิ่มเติมด้วย

#เข้าใจว่า Builder ปลอดภัยต่อหลาย thread เสมอ

Computer.Builder ในบทความมี mutable state และไม่ได้ออกแบบให้แชร์ระหว่าง thread ควรใช้ Builder แยกต่อการสร้างแต่ละงาน หากต้องแชร์ ต้องออกแบบ synchronization หรือรูปแบบข้อมูลให้เหมาะสม

#ใช้ Builder กับคลาสที่เรียบง่ายเกินไป

คลาสที่มีเพียงสอง field และทั้งสองเป็นข้อมูลบังคับ อาจใช้ constructor ได้ชัดเจนอยู่แล้ว Builder ควรเพิ่มความชัดเจนให้การใช้งานมากพอกับโค้ดที่ต้องดูแลเพิ่ม

#เข้าใจว่า Builder ป้องกันการขาด field ตอน compile ได้ทุกแบบ

Builder ที่มีเมธอดตั้งค่าทุก field อาจยอมให้ลืม required fields ได้ ตัวอย่างนี้บังคับ CPU และ RAM ผ่าน constructor ของ Builder แต่ความถูกต้องของค่าต้องตรวจตอนรัน หากต้องการกำหนดลำดับและขั้นตอนที่ขาดไม่ได้ด้วยชนิดข้อมูล สามารถศึกษา Step Builder เพิ่มเติม

#14. กิจกรรมฝึกปฏิบัติ

#กิจกรรมที่ 1: เพิ่มระบบปฏิบัติการ

เพิ่ม field operatingSystem โดยมีค่าเริ่มต้น Linux และเพิ่มเมธอด operatingSystem(String value) ใน Builder ตรวจสอบว่าค่าไม่เป็น null หรือข้อความว่างก่อนคืน Product

#กิจกรรมที่ 2: กำหนดข้อมูลบังคับใหม่

เพิ่ม modelName เป็นข้อมูลบังคับ โดยให้ผู้เรียกระบุเมื่อสร้าง Builder แล้วปรับตัวอย่างเรียกใช้งานให้ครบ

#กิจกรรมที่ 3: เพิ่มสูตรสร้าง

เพิ่มสูตร workstation() ที่กำหนด Storage และ GPU มาตรฐานสำหรับงานประมวลผล แล้วอภิปรายว่าจะวางสูตรนี้ใน Director, preset class หรือ static factory เพราะเหตุใด

#กิจกรรมที่ 4: เปลี่ยนชนิดสมาชิกในรายการ

สมมติเปลี่ยนจาก List<String> เป็น List<Port> และ Port มี setter ให้เสนอวิธีรักษา immutability ของ Computer พร้อมอธิบายว่าทำไม List.copyOf() เพียงอย่างเดียวจึงยังไม่พอ

#15. รายการตรวจสอบผลลัพธ์

สถานการณ์ ผลที่คาดหวัง
ส่ง CPU และ RAM ที่ถูกต้องโดยไม่กำหนดตัวเลือก Storage เป็น 256, GPU เป็น Integrated, พอร์ตว่าง
กำหนด Storage, GPU และพอร์ต Product เก็บค่าตามที่เลือก
CPU เป็น null หรือข้อความว่าง build() โยน IllegalArgumentException
RAM เป็น 0 build() โยน IllegalArgumentException
Storage น้อยกว่าหรือเท่ากับ 0 build() โยน IllegalArgumentException
GPU เป็นข้อความว่าง build() โยน IllegalArgumentException
เพิ่มชื่อพอร์ตเป็น null addPort() โยน IllegalArgumentException
เพิ่มพอร์ตผ่าน Builder หลังสร้าง Product แรก รายการของ Product แรกไม่เปลี่ยน
เพิ่มพอร์ตผ่าน getter ของ Product โยน UnsupportedOperationException
เรียก build() สองครั้งบน Builder เดิม ได้ Product คนละ instance โดย Builder ยังเก็บค่าที่ตั้งไว้

ตัวอย่างหลักและตัวอย่าง Director ผ่านการคอมไพล์โดยกำหนดเป้าหมาย Java 17 และตรวจพฤติกรรมค่าเริ่มต้น, validation, การแก้ list, การใช้ Builder ซ้ำ และการสร้าง HttpRequest แล้ว

#เอกสารอ้างอิงและอ่านเพิ่มเติม

  1. Oracle Java SE 17 API: List — Unmodifiable Lists และ copyOf
  2. Oracle Java SE 17 API: HttpRequest
  3. Gamma, E., Helm, R., Johnson, R., & Vlissides, J. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, 1994. — แนวคิด Builder ในกลุ่ม Creational Patterns