- Simplifying Method Calls คืออะไร
- ภาพรวม 14 เทคนิค
- 1. Rename Method — เปลี่ยนชื่อเมธอด
- 2. Add Parameter — เพิ่มพารามิเตอร์ที่จำเป็น
- 3. Remove Parameter — ลบพารามิเตอร์ที่ไม่ได้ใช้
- 4. Separate Query from Modifier — แยกการอ่านออกจากการเปลี่ยนสถานะ
- 5. Parameterize Method — รวมเมธอดด้วยพารามิเตอร์
- 6. Replace Parameter with Explicit Methods — ใช้เมธอดเฉพาะแทนพารามิเตอร์เลือกพฤติกรรม
- 7. Preserve Whole Object — ส่งออบเจ็กต์ต้นทางทั้งก้อน
- 8. Replace Parameter with Method Call — ให้เมธอดเรียกหาค่าเอง
- 9. Introduce Parameter Object — สร้างออบเจ็กต์รวมกลุ่มพารามิเตอร์
- 10. Remove Setting Method — ลบเมธอดเปลี่ยนค่าที่ไม่ควรเปลี่ยน
- 11. Hide Method — ซ่อนเมธอดที่เป็นรายละเอียดภายใน
- 12. Replace Constructor with Factory Method — สร้างออบเจ็กต์ผ่านเมธอดที่มีชื่อ
- 13. Replace Error Code with Exception — ใช้ exception แทนรหัสผิดพลาด
- 14. Replace Exception with Test — ตรวจเงื่อนไขแทนการใช้ exception เป็นทางเดินปกติ
- ขั้นตอนนำไปใช้กับโค้ดจริง
- แบบฝึกหัดสำหรับ Java
- แหล่งอ้างอิง
14 เทคนิคปรับการเรียกเมธอดให้สื่อความหมาย ส่งข้อมูลได้เหมาะสม และดูแลต่อได้ง่าย

#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 หรือสถานะเปลี่ยนพร้อมกัน | ตรวจล่วงหน้าอย่างเดียวอาจไม่เพียงพอ |
#ขั้นตอนนำไปใช้กับโค้ดจริง
- ระบุความสับสนที่จุดเรียก — ชื่อไม่ชัด ส่งข้อมูลซ้ำ มี flag ที่ต้องเดา หรือมีผลข้างเคียงที่ซ่อนอยู่
- ตรวจสัญญาเดิม — ผู้เรียกคาดหวังผลลัพธ์ ลำดับผลข้างเคียง และข้อผิดพลาดอย่างไร มีผู้เรียกภายนอกหรือไม่
- เลือกเปลี่ยนทีละประเด็น — เช่น เปลี่ยนชื่อก่อน แล้วค่อยจัดกลุ่มพารามิเตอร์ เพื่อให้ตรวจการเปลี่ยนได้ง่าย
- ปรับผู้เรียกที่เกี่ยวข้อง — รวมถึง interface, implementation, overload, method reference และ configuration ที่อ้างชื่อ
- ยืนยันพฤติกรรมที่ต้องคงไว้ — ตรวจกรณีสำเร็จ ค่าขอบเขต ข้อมูลผิดพลาด และลำดับการเปลี่ยนสถานะที่มีความหมาย
- จัดการช่วงเปลี่ยนผ่าน — หากเป็น 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
คำตอบจึงขึ้นกับสัญญาและบริบทของโปรแกรม ไม่ใช่การจับคู่ชื่อเทคนิคจากรูปร่างโค้ดเพียงอย่างเดียว
#แหล่งอ้างอิง
- Refactoring.Guru: Simplifying Method Calls
- 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
- Replace Exception with Test