14 เทคนิคปรับการเรียกเมธอดให้สื่อความหมาย ส่งข้อมูลได้เหมาะสม และดูแลต่อได้ง่าย

info-refactoring-simplifying-method-calls-java

#Simplifying Method Calls คืออะไร

การอ่านโค้ดไม่ได้จบที่การเข้าใจสิ่งที่อยู่ภายในเมธอด ผู้เรียกยังต้องเข้าใจชื่อเมธอด ข้อมูลที่ต้องส่งเข้าไป ผลลัพธ์ที่ได้รับ และผลข้างเคียงที่อาจเกิดขึ้นด้วย กลุ่มเทคนิค Simplifying Method Calls จึงมุ่งปรับจุดติดต่อระหว่างผู้เรียกกับเมธอดให้ชัดเจน

ตัวอย่างเช่น process(true) ทำให้ผู้อ่านต้องเปิดดู implementation เพื่อเข้าใจความหมายของ true ขณะที่ publishAndNotify() บอกเจตนาได้ตั้งแต่จุดเรียก

บทความนี้เรียบเรียงจากรายการเทคนิคที่ผู้ใช้ให้มา โดยอ้างอิงหมวด Simplifying Method Calls ของ Refactoring.Guru ตัวอย่าง Java และข้อพิจารณาประกอบเป็นตัวอย่างที่เขียนขึ้นเพื่อการสอน

ขอบเขตตัวอย่าง: โค้ดแต่ละส่วนเป็นตัวอย่างแยกกัน ไม่ใช่ไฟล์โปรแกรมเดียว บางส่วนละคลาสประกอบเพื่อเน้นสิ่งที่เปลี่ยน ตัวอย่างที่ใช้ record เหมาะกับ Java 17 ขึ้นไป ตัวเลขเงินใช้หน่วยย่อยเป็นจำนวนเต็มเมื่อเกี่ยวข้องกับยอดคงเหลือ ส่วนตัวอย่างอัตราใช้ double เพื่อให้เห็นแนวคิดได้ง่าย

#ภาพรวม 14 เทคนิค

เทคนิค สัญญาณที่ควรพิจารณา ผลที่ต้องการ
Rename Method ชื่อไม่อธิบายหน้าที่ ผู้เรียกเข้าใจเจตนา
Add Parameter ข้อมูลจากผู้เรียกยังไม่เพียงพอ ส่งข้อมูลที่จำเป็นอย่างชัดเจน
Remove Parameter มีพารามิเตอร์ที่ไม่ใช้ ลดภาระผู้เรียก
Separate Query from Modifier อ่านค่าแล้วเปลี่ยนสถานะไปด้วย แยกการอ่านกับการเปลี่ยนสถานะ
Parameterize Method หลายเมธอดต่างกันเพียงค่าที่ใช้ รวมขั้นตอนเดียวกันด้วยพารามิเตอร์
Replace Parameter with Explicit Methods ค่าพารามิเตอร์เลือกคนละพฤติกรรม ใช้ชื่อเมธอดบอกพฤติกรรม
Preserve Whole Object แกะข้อมูลหลายค่าจากออบเจ็กต์เดียว ส่งออบเจ็กต์ต้นทาง
Replace Parameter with Method Call ค่าที่ส่งมาหาได้จากบริบทของเมธอด ให้เมธอดดึงข้อมูลนั้นเอง
Introduce Parameter Object กลุ่มพารามิเตอร์เดิมปรากฏซ้ำ สร้างออบเจ็กต์แทนแนวคิดร่วม
Remove Setting Method ข้อมูลต้องคงเดิมหลังสร้าง กำหนดค่าครั้งเดียว
Hide Method เมธอดภายในถูกเปิดเผยเกินจำเป็น จำกัดการเข้าถึง
Replace Constructor with Factory Method ขั้นตอนสร้างควรมีชื่อหรือมีการตัดสินใจ รวมการสร้างไว้หลังเมธอดที่สื่อความหมาย
Replace Error Code with Exception ผู้เรียกต้องตรวจรหัสผิดพลาดพิเศษ แจ้งความล้มเหลวผ่าน exception
Replace Exception with Test ใช้ exception จัดการกรณีปกติที่ตรวจได้ ตรวจเงื่อนไขก่อนทำงาน

#1. Rename Method — เปลี่ยนชื่อเมธอด

ปัญหา: ชื่อคลุมเครือ เช่น calc() หรือชื่อเก่าไม่ตรงกับหน้าที่ปัจจุบัน

แนวทาง: ตั้งชื่อจากสิ่งที่เมธอดทำจริง โดยคำนึงถึงคำศัพท์ของโดเมนและมุมมองของผู้เรียก

ก่อนปรับ:

double calc() {
    return unitPrice * quantity;
}

double total = invoice.calc();

หลังปรับ:

double calculateSubtotal() {
    return unitPrice * quantity;
}

double subtotal = invoice.calculateSubtotal();

ชื่อ calculateSubtotal() ยังสื่อว่ายอดนี้เป็นยอดก่อนรายการปรับอื่น เช่น ภาษีหรือส่วนลด หากสูตรรวมรายการเหล่านั้นด้วย ควรใช้ชื่อที่ตรงกับผลลัพธ์จริง

ข้อพิจารณา: ใช้คำสั่ง Rename ของ IDE เพื่อปรับจุดเรียกอย่างเป็นระบบ แต่ยังต้องตรวจการอ้างอิงผ่าน reflection, configuration และผู้ใช้ API ภายนอก หากยังย้ายผู้เรียกทั้งหมดไม่ได้ อาจคงชื่อเดิมไว้เป็นเมธอดที่เรียกต่อไปยังชื่อใหม่และประกาศเลิกใช้ชั่วคราว

#2. Add Parameter — เพิ่มพารามิเตอร์ที่จำเป็น

ปัญหา: เมธอดต้องใช้ข้อมูลที่ผู้เรียกทราบ แต่ interface ยังไม่เปิดทางให้ส่งเข้ามา

ก่อนปรับ:

double calculateTax(double subtotal) {
    return subtotal * 0.07;
}

หลังปรับ:

double calculateTax(double subtotal, double taxRate) {
    return subtotal * taxRate;
}

double tax = calculateTax(1_000.0, 0.07);

พารามิเตอร์ใหม่ทำให้ผู้เรียกระบุอัตราที่เหมาะกับบริบทได้ โดยควรบอกหน่วยให้ชัดว่า 0.07 หมายถึง 7% และตรวจความถูกต้องของข้อมูลตามกติกาของระบบ

ข้อพิจารณา: อย่าเพิ่มพารามิเตอร์ทุกครั้งที่ต้องการข้อมูลใหม่ หากข้อมูลเป็น dependency ที่หลายเมธอดใช้ร่วมกัน อาจเหมาะกับ constructor injection มากกว่า หากต้องคงพฤติกรรมผู้เรียกเดิม สามารถเก็บ overload เดิมให้เรียก calculateTax(subtotal, 0.07) ระหว่างย้ายระบบ

#3. Remove Parameter — ลบพารามิเตอร์ที่ไม่ได้ใช้

ปัญหา: ผู้เรียกต้องจัดเตรียมข้อมูลที่ไม่มีผลต่อการทำงาน

ก่อนปรับ:

String formatName(String firstName, String lastName, String locale) {
    return firstName + " " + lastName;
}

String name = formatName("Naruapon", "Suwanwijit", "th-TH");

หลังปรับ:

String formatName(String firstName, String lastName) {
    return firstName + " " + lastName;
}

String name = formatName("Naruapon", "Suwanwijit");

การลบ locale ทำให้ผู้เรียกไม่เข้าใจผิดว่าเมธอดรองรับรูปแบบชื่อตามภาษา

ข้อพิจารณา: ตรวจ implementation อื่นของ interface และเมธอดที่ override ด้วย นอกจากนี้ ถ้า argument เดิมเป็นการเรียกเมธอดที่มีผลข้างเคียง การลบ argument นั้นอาจทำให้พฤติกรรมเปลี่ยน ต้องประเมินและจัดวางผลข้างเคียงที่ยังจำเป็นใหม่

#4. Separate Query from Modifier — แยกการอ่านออกจากการเปลี่ยนสถานะ

ปัญหา: การเรียกเพื่ออ่านค่ากลับเปลี่ยนข้อมูลโดยที่ผู้เรียกอาจไม่คาดคิด

ก่อนปรับ:

class Counter {
    private int value;

    int getValueAndIncrement() {
        return value++;
    }
}

หลังปรับ:

class Counter {
    private int value;

    int getValue() {
        return value;
    }

    void increment() {
        value++;
    }
}

// คงลำดับเดิม: อ่านค่าก่อน แล้วจึงเพิ่ม
int previousValue = counter.getValue();
counter.increment();

ผู้เรียกเลือกได้ว่าจะอ่านอย่างเดียวหรือเปลี่ยนสถานะ และการอ่านซ้ำจะไม่ทำให้ตัวนับเพิ่มโดยไม่ตั้งใจ

ข้อพิจารณา: ตัวอย่างนี้สมมติการใช้งานแบบเธรดเดียว การแยก operation ที่ต้องเป็น atomic อาจทำให้ผลผิด เช่น การจองรายการในคิวหรือการใช้ atomic counter เมธอดที่คืนค่าและเปลี่ยนสถานะพร้อมกันจึงมีเหตุผลได้ หากเป็นสัญญาที่ตั้งใจออกแบบไว้

#5. Parameterize Method — รวมเมธอดด้วยพารามิเตอร์

ปัญหา: มีหลายเมธอดที่ทำตามขั้นตอนเดียวกัน ต่างกันเพียงค่าคงที่

ก่อนปรับ:

void raiseSalaryByFivePercent() {
    salary *= 1.05;
}

void raiseSalaryByTenPercent() {
    salary *= 1.10;
}

หลังปรับ:

void raiseSalary(double rate) {
    salary *= 1 + rate;
}

employee.raiseSalary(0.05);
employee.raiseSalary(0.10);

เมื่อสูตรเปลี่ยน จะมีจุดแก้หลักเพียงแห่งเดียว หากโดเมนกำหนดอัตราที่ยอมรับได้ ควรตรวจอัตราก่อนคำนวณ

ข้อพิจารณา: ใช้เมื่อมี operation เดียวที่ปรับค่าได้ อย่ารวมพฤติกรรมที่ไม่เกี่ยวข้องกันเพียงเพราะเขียนให้อยู่ในเมธอดเดียวได้

#6. Replace Parameter with Explicit Methods — ใช้เมธอดเฉพาะแทนพารามิเตอร์เลือกพฤติกรรม

ปัญหา: ผู้เรียกต้องจำว่า boolean หรือรหัสแต่ละค่าจะสั่งให้เมธอดทำอะไร

ก่อนปรับ:

void setEnabled(boolean enabled) {
    this.enabled = enabled;
}

feature.setEnabled(true);
feature.setEnabled(false);

หลังปรับ:

void enable() {
    enabled = true;
}

void disable() {
    enabled = false;
}

feature.enable();
feature.disable();

ชื่อเมธอดทำให้เห็นคำสั่งที่ต้องการทันที เหมาะกับจุดเรียกที่มีเจตนาเฉพาะ เช่น ปุ่มเปิดและปุ่มปิด

ข้อพิจารณา: boolean ไม่ใช่ปัญหาเสมอไป หากผู้เรียกรับสถานะจาก configuration แล้วส่งต่อ setEnabled(config.enabled()) อาจอ่านง่ายกว่าการเขียน if เพื่อเลือกสองเมธอด พิจารณาจากรูปแบบผู้เรียกจริง

#เลือกระหว่างเทคนิคที่ 5 และ 6

ลักษณะความต่าง แนวทางที่มักเหมาะ
ขั้นตอนเดียวกัน ต่างกันเพียงอัตราหรือจำนวน Parameterize Method
ผู้เรียกมีเจตนาเป็นคำสั่งคนละอย่าง Replace Parameter with Explicit Methods
ต้องรับค่าที่เปลี่ยนแปลงจากภายนอกโดยตรง ประเมินการคงพารามิเตอร์ไว้

เป้าหมายคือให้ interface อ่านง่ายตามบริบท ไม่ใช่ทำให้จำนวนเมธอดน้อยที่สุด

#7. Preserve Whole Object — ส่งออบเจ็กต์ต้นทางทั้งก้อน

ปัญหา: ผู้เรียกแกะข้อมูลหลายฟิลด์จากออบเจ็กต์เดียวเพื่อส่งให้เมธอดอื่น

ก่อนปรับ:

boolean contains(int min, int max) {
    return min >= allowedMin && max <= allowedMax;
}

boolean accepted = plan.contains(readings.min(), readings.max());

หลังปรับ:

record TemperatureRange(int min, int max) {}

boolean contains(TemperatureRange range) {
    return range.min() >= allowedMin && range.max() <= allowedMax;
}

boolean accepted = plan.contains(readings);

ในตัวอย่างนี้ readings เป็น TemperatureRange อยู่แล้ว ผู้เรียกจึงไม่ต้องรู้ว่า plan ต้องใช้ฟิลด์ใดบ้าง

ข้อพิจารณา: การรับออบเจ็กต์ทำให้เมธอดขึ้นกับชนิดนั้น หากต้นทางเป็น entity ขนาดใหญ่หรือออบเจ็กต์ของ framework ที่ไม่ควรรั่วเข้าสู่ชั้น domain อาจเหมาะกับการรับ value object ขนาดเล็กแทน ต้องคงความหมายเรื่อง snapshot ของข้อมูลด้วย หากออบเจ็กต์ต้นทางเปลี่ยนค่าได้

#8. Replace Parameter with Method Call — ให้เมธอดเรียกหาค่าเอง

ปัญหา: ผู้เรียกส่งค่าที่เมธอดสามารถคำนวณหรืออ่านได้จากบริบทของตัวเองอยู่แล้ว

ก่อนปรับ ภายในคลาส Order:

double total() {
    return discountedTotal(discountRate());
}

private double discountedTotal(double rate) {
    return subtotal * (1 - rate);
}

หลังปรับ:

double total() {
    return discountedTotal();
}

private double discountedTotal() {
    return subtotal * (1 - discountRate());
}

สมมติว่า discountRate() เป็น query ที่ให้ผลคงที่ระหว่าง operation และไม่มีผลข้างเคียง เมธอดภายในจึงอ่านอัตราจากออบเจ็กต์เดียวกันได้โดยตรง

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

#9. Introduce Parameter Object — สร้างออบเจ็กต์รวมกลุ่มพารามิเตอร์

ปัญหา: กลุ่มข้อมูลที่มีความหมายร่วมกันถูกส่งซ้ำในหลายเมธอด

ก่อนปรับ:

List<Sale> findSales(LocalDate start, LocalDate end) { /* ... */ }
Report buildReport(LocalDate start, LocalDate end) { /* ... */ }

หลังปรับ:

record DateRange(LocalDate start, LocalDate end) {
    DateRange {
        Objects.requireNonNull(start);
        Objects.requireNonNull(end);
        if (end.isBefore(start)) {
            throw new IllegalArgumentException("End precedes start");
        }
    }
}

List<Sale> findSales(DateRange period) { /* ... */ }
Report buildReport(DateRange period) { /* ... */ }

ตัวอย่างละส่วน implementation ของเมธอดไว้ โดยต้อง import LocalDate และ Objects ตามปกติ นอกจากลดพารามิเตอร์แล้ว DateRange ยังมีชื่อแทนแนวคิดช่วงเวลา และรวมกฎตรวจสอบข้อมูลไว้ในจุดเดียวได้

ข้อพิจารณา: การเพิ่ม validation ที่ไม่เคยมีมาก่อนเป็นการเปลี่ยนพฤติกรรมสำหรับข้อมูลที่ไม่ถูกต้อง ควรแยกการตัดสินใจนี้ออกจากการย้ายพารามิเตอร์ และกำหนดให้ตรงกันว่าขอบเขตวันเริ่มและวันสิ้นสุดรวมอยู่ในช่วงหรือไม่

#ต่างจาก Preserve Whole Object อย่างไร

เทคนิค จุดเริ่มต้น สิ่งที่ทำ
Preserve Whole Object มีออบเจ็กต์ต้นทางที่เหมาะสมอยู่แล้ว ส่งออบเจ็กต์นั้นแทนการแกะฟิลด์
Introduce Parameter Object มีกลุ่มพารามิเตอร์ที่เกิดซ้ำ สร้างชนิดใหม่ให้กลุ่มข้อมูล

#10. Remove Setting Method — ลบเมธอดเปลี่ยนค่าที่ไม่ควรเปลี่ยน

ปัญหา: ค่าอัตลักษณ์หรือข้อมูลคงที่ของออบเจ็กต์ถูกแก้ได้หลังสร้าง

ก่อนปรับ:

class Customer {
    private String id;

    Customer(String id) {
        this.id = id;
    }

    void setId(String id) {
        this.id = id;
    }
}

หลังปรับ:

class Customer {
    private final String id;

    Customer(String id) {
        this.id = id;
    }

    String getId() {
        return id;
    }
}

ผู้ใช้คลาสต้องกำหนด id ตอนสร้าง และไม่มีคำสั่งเปลี่ยนรหัสลูกค้าหลังจากนั้น

ข้อพิจารณา: ตรวจการใช้งานของ ORM และเครื่องมือ serialization ก่อนเปลี่ยนโมเดล การลบ setter เป็นการจำกัดสัญญาที่เคยเปิดให้แก้ค่า ส่วน final ป้องกันการเปลี่ยน reference แต่ไม่ได้ทำให้ออบเจ็กต์ที่ reference นั้นชี้ไป immutable โดยอัตโนมัติ เช่น final List ยังแก้สมาชิกได้หากไม่ป้องกันเพิ่มเติม

#11. Hide Method — ซ่อนเมธอดที่เป็นรายละเอียดภายใน

ปัญหา: helper ภายในถูกเปิดเป็น public ทำให้คลาสอื่นนำไปใช้จนเกิด dependency ที่ไม่จำเป็น

ก่อนปรับ:

public String normalizeEmail(String email) {
    return email.strip().toLowerCase(Locale.ROOT);
}

หลังปรับ เมื่อพบว่าเมธอดนี้ถูกใช้เฉพาะภายในคลาสเดียว:

private String normalizeEmail(String email) {
    return email.strip().toLowerCase(Locale.ROOT);
}

ใช้สูตรเดิมและเปลี่ยนเฉพาะ visibility โดยสมมติว่ากฎ normalize นี้เหมาะกับระบบอยู่แล้ว ตัวอย่างต้อง import Locale

ข้อพิจารณา: เลือกขอบเขตแคบที่สุดที่ตรงกับการใช้งานจริง ถ้าคลาสใน package ต้องใช้ร่วมกัน อาจเลือก package-private ถ้าตั้งใจเป็น extension point สำหรับ subclass อาจเลือก protected แต่ต้องออกแบบสัญญาการสืบทอดด้วย สำหรับ Java protected ยังเปิดให้คลาสใน package เดียวกันเข้าถึงได้ ไม่ได้จำกัดเฉพาะ subclass เท่านั้น

#12. Replace Constructor with Factory Method — สร้างออบเจ็กต์ผ่านเมธอดที่มีชื่อ

ปัญหา: การเรียก constructor ไม่สื่อเจตนาการสร้าง หรือรายละเอียดการเลือกและเตรียมออบเจ็กต์เริ่มซับซ้อน

ก่อนปรับ:

class ConnectionOptions {
    private final String host;
    private final int port;
    private final boolean tls;

    public ConnectionOptions(String host, int port, boolean tls) {
        this.host = host;
        this.port = port;
        this.tls = tls;
    }
}

var options = new ConnectionOptions("example.com", 443, true);

หลังปรับ เพิ่มจุดสร้างที่สื่อเจตนาและจำกัด constructor:

class ConnectionOptions {
    private final String host;
    private final int port;
    private final boolean tls;

    private ConnectionOptions(String host, int port, boolean tls) {
        this.host = host;
        this.port = port;
        this.tls = tls;
    }

    public static ConnectionOptions secure(String host, int port) {
        return new ConnectionOptions(host, port, true);
    }

    public static ConnectionOptions plain(String host, int port) {
        return new ConnectionOptions(host, port, false);
    }
}

var options = ConnectionOptions.secure("example.com", 443);

เมธอดสร้างทำให้ชื่อที่จุดเรียกบอกโหมดได้ทันที และเป็นที่รวมการเตรียมค่าหรือการเลือก implementation ได้เมื่อจำเป็น ต้องย้ายผู้เรียกโหมด true และ false ไปยัง factory ที่ตรงกัน

ข้อพิจารณา: ตัวอย่างนี้เป็น static factory method การใช้ชื่อ factory method ในเทคนิค refactoring ไม่ได้หมายความว่าต้องเป็น Factory Method Pattern ของ GoF ซึ่งเกี่ยวข้องกับการให้ subclass เลือกชนิดผลิตภัณฑ์ การทำ constructor เป็น private ยังอาจกระทบ subclass และ framework ที่สร้างออบเจ็กต์ผ่าน constructor

#13. Replace Error Code with Exception — ใช้ exception แทนรหัสผิดพลาด

ปัญหา: เมธอดใช้ค่าพิเศษ เช่น -1 เพื่อแจ้งความล้มเหลว ผู้เรียกจึงอาจลืมตรวจหรือสับสนกับผลลัพธ์ปกติ

ก่อนปรับ ภายในคลาสบัญชี โดยสมมติว่าผู้เรียกส่งจำนวนถอนเป็นบวก:

// balanceCents และ amountCents มีหน่วยเป็นสตางค์
int withdraw(long amountCents) {
    if (amountCents > balanceCents) {
        return -1;
    }
    balanceCents -= amountCents;
    return 0;
}

if (account.withdraw(50_000) == -1) {
    showInsufficientBalance();
}

หลังปรับ:

void withdraw(long amountCents) {
    if (amountCents > balanceCents) {
        throw new InsufficientBalanceException();
    }
    balanceCents -= amountCents;
}

class InsufficientBalanceException extends RuntimeException {}

try {
    account.withdraw(50_000);
} catch (InsufficientBalanceException ex) {
    showInsufficientBalance();
}

ส่วนเส้นทางสำเร็จอ่านได้โดยไม่ต้องตรวจรหัส ในระบบจริงต้องกำหนดและทดสอบเงื่อนไขจำนวนถอนที่เป็นศูนย์หรือติดลบด้วย

ข้อพิจารณา: การเปลี่ยนจาก return code เป็น exception เปลี่ยนสัญญาการแจ้งความล้มเหลว ต้องย้ายผู้เรียกและทดสอบการจัดการข้อผิดพลาดไปพร้อมกัน หากความล้มเหลวเป็นผลลัพธ์ทางธุรกิจที่เกิดบ่อยและผู้เรียกต้องตัดสินใจหลายกรณี อาจเหมาะกับ result type ที่มีชื่อชัดเจนกว่า ไม่จำเป็นต้องเปลี่ยนทุก error code เป็น exception

#14. Replace Exception with Test — ตรวจเงื่อนไขแทนการใช้ exception เป็นทางเดินปกติ

ปัญหา: โค้ดลองทำสิ่งที่ตรวจความพร้อมได้ง่าย แล้วใช้ catch จัดการกรณีที่เกิดตามปกติ

ก่อนปรับ:

String firstOrDefault(String[] items) {
    try {
        return items[0];
    } catch (ArrayIndexOutOfBoundsException ex) {
        return "ไม่มีข้อมูล";
    }
}

หลังปรับ:

String firstOrDefault(String[] items) {
    if (items.length == 0) {
        return "ไม่มีข้อมูล";
    }
    return items[0];
}

การตรวจความยาวสื่อว่าอาร์เรย์ว่างเป็นกรณีที่คาดไว้ ทั้งสองเวอร์ชันสมมติว่า items ไม่เป็น null และยังคืน null หากสมาชิกตัวแรกเป็น null เหมือนเดิม

ข้อพิจารณา: การตรวจล่วงหน้าใช้แทนการจัดการ exception ไม่ได้ทุกกรณี เช่น ตรวจว่าไฟล์มีอยู่แล้วค่อยเปิดไฟล์ สถานะอาจเปลี่ยนระหว่างสองคำสั่ง การเปิดไฟล์ยังต้องจัดการความล้มเหลวจริง เช่นเดียวกับ collection ที่เปลี่ยนพร้อมกันได้ ซึ่งควรใช้ operation ที่รองรับ concurrency ตามสัญญาของมัน

#เทคนิคที่ 13 และ 14 ไม่ได้ขัดแย้งกัน

สถานการณ์ แนวคิดในการเลือก
operation ทำไม่สำเร็จและต้องแจ้งให้ชั้นที่เหมาะสมจัดการ exception อาจเหมาะกว่ารหัสที่ตรวจพลาดง่าย
กรณีปกติที่ตรวจเงื่อนไขภายในได้แน่นอน ใช้เงื่อนไขเพื่อบอกทางเลือกให้ชัด
ผลลัพธ์ทางธุรกิจมีหลายแบบและคาดว่าจะเกิดเป็นประจำ พิจารณา result type ที่สื่อความหมาย
ความล้มเหลวจาก I/O หรือสถานะเปลี่ยนพร้อมกัน ตรวจล่วงหน้าอย่างเดียวอาจไม่เพียงพอ

#ขั้นตอนนำไปใช้กับโค้ดจริง

  1. ระบุความสับสนที่จุดเรียก — ชื่อไม่ชัด ส่งข้อมูลซ้ำ มี flag ที่ต้องเดา หรือมีผลข้างเคียงที่ซ่อนอยู่
  2. ตรวจสัญญาเดิม — ผู้เรียกคาดหวังผลลัพธ์ ลำดับผลข้างเคียง และข้อผิดพลาดอย่างไร มีผู้เรียกภายนอกหรือไม่
  3. เลือกเปลี่ยนทีละประเด็น — เช่น เปลี่ยนชื่อก่อน แล้วค่อยจัดกลุ่มพารามิเตอร์ เพื่อให้ตรวจการเปลี่ยนได้ง่าย
  4. ปรับผู้เรียกที่เกี่ยวข้อง — รวมถึง interface, implementation, overload, method reference และ configuration ที่อ้างชื่อ
  5. ยืนยันพฤติกรรมที่ต้องคงไว้ — ตรวจกรณีสำเร็จ ค่าขอบเขต ข้อมูลผิดพลาด และลำดับการเปลี่ยนสถานะที่มีความหมาย
  6. จัดการช่วงเปลี่ยนผ่าน — หากเป็น public API อาจต้องใช้ adapter หรือ overload เดิมชั่วคราว พร้อมแผนเลิกใช้ที่ชัดเจน

การ refactor โดยหลักควรคงพฤติกรรมที่ผู้ใช้ระบบสังเกตได้ แต่บางตัวอย่างเปลี่ยน interface หรือข้อจำกัดโดยตั้งใจ เช่น การเลิกให้แก้ id หรือเปลี่ยน error code เป็น exception จึงต้องจัดการการย้ายผู้เรียกและระบุสิ่งที่เปลี่ยนในสัญญาให้ชัด ไม่ควรถือว่าการแก้ signature อย่างเดียวเพียงพอ

#แบบฝึกหัดสำหรับ Java

พิจารณาจุดเรียกต่อไปนี้แล้วเลือกเทคนิค พร้อมอธิบายเหตุผล:

reportService.process(true);
bookingService.reserve(customer.id(), customer.name(), customer.email());
salesService.find(startDate, endDate);
salesService.export(startDate, endDate);
counter.readAndReset();
  • process(true) ต้องรู้ก่อนว่า flag เลือกพฤติกรรมใด แล้วประเมินการใช้ชื่อเมธอดเฉพาะ
  • การส่งข้อมูลลูกค้าต้องประเมินว่าควรรับ Customer โดยตรง หรือใช้ value object เพื่อไม่เพิ่ม dependency เกินจำเป็น
  • กลุ่ม startDate และ endDate ที่เกิดซ้ำเป็นตัวเลือกสำหรับ DateRange
  • readAndReset() ต้องตรวจว่าผู้เรียกต้องการ operation ที่เป็น atomic หรือไม่ ก่อนแยกการอ่านกับการ reset

คำตอบจึงขึ้นกับสัญญาและบริบทของโปรแกรม ไม่ใช่การจับคู่ชื่อเทคนิคจากรูปร่างโค้ดเพียงอย่างเดียว

#แหล่งอ้างอิง