เรื่องของ Exception ตอน 2

Anti pattern Exception

ผมเขียนเกี่ยวกับ Exception ไว้ 3 ตอน คุณสามารถกด Link ด้านล่างเพื่ออ่านที่เกี่ยวกับ Exception ตอนต่างๆได้เลย

ตอนเรียนวิชา Software engineering อาจารย์ประจำวิชาบอกว่า “ถ้าคุณนึกว่าจะเขียน Software ออกมาให้ดียังไงไม่ได้ ให้ลองคิดจากมุมที่ว่า Software ที่ไม่ดีมันมีอะไรบ้าง แล้วคุณจะได้ Software ที่ดีออกมา” พอเริ่มมาทำงานผมจะหาสองสิ่งเสมอตอนเขียน Code คือ หา Best practice กับ Anti pattern แต่ที่ชอบหาคือ Anti pattern เพราะอย่างน้อย Code ที่ออกมาจะไม่แย่

Anti pattern ของ Exception นั้นมีอยู่หลายข้อขนาดว่ามีคนทำ Paper เกี่ยวกับเรื่องนี้เลย

  • Catch Generic

คือการที่เราทำการ Catch Exception ด้วย Exception ที่ใหญ่กว่า Exception ที่ Throw ออกมามากๆ ดังตัวอย่างด้านล่าง

1
2
3
4
5
6
7
File newFile = new File("new_file.txt");
try{
newFile.createNewFile();
} catch(Exception ex) {
logger.error("THIS IS EXCEPTION", ex);
handleFile(ex);
}

จากตัวอย่าง Code จะเห็นว่าการสร้างไฟล์นั้นจะ Throw IOException ออกมาแต่เราไป Catch มันด้วย Exception ซึ่งเป็น Exception บรรพบุรุษ ของ Exception ทั้งหลายดังนั้นมัน Catch หมด ซึ่งการ Catch หมดอย่างนี้มันอันตรายตรงที่แทนที่เราจะจัดการเฉพาะกับ IO Exception กลายเป็นว่าเรา Handle ทุก Exception และเราจะไม่รู้สาเหตุที่แท้จริงของ Exception ว่ามันเกิดอะไรกันแน่ แต่ไม่ใช่ว่ามันจะดักด้วย Exception แบบนี้ไม่ได้นะครับ คือจริงๆเราสามารถดักมันด้วย Exception ใหญ่ได้ในกรณีที่กันตัวโปรแกรมตายจากการไม่ดัก Exception ประมาณว่าดักหมดทุกอย่างที่ต้องดักแล้ว (Check Exception) อย่างอื่นที่เกินคาดหมายก็ดักมันด้วย Catch Exception

1
2
3
4
5
6
7
8
9
10
File newFile = new File("new_file.txt");
try{
logger.error("Problem with create file", ex);
newFile.createNewFile();
} catch(IOException ex) {
handleFile();
} catch(Exception ex) {
logger.error("THIS IS UNCHECK EXCEPTION", ex);
handleError(ex);
}
  • Throws Generic

คือการที่เรา Throw exception ระดับสูงหรือ Exception ที่ไม่ตรงใหญ่ๆออกไป ยกตัวอย่างง่ายๆคือการ Throw Exception ออกไป

1
2
3
4
5
6
7
8
public void controlMachineProcess(URI serverURI) throws Exception {
Machine machine = getMachineFromServer(serverURI);
if (!machine.canProcess) {
throw new Exception("Machine not correct process");
} else {
..........
}
}

ปัญหาของการทำแบบนี้มันจะทำให้เกิดปัญหาของคนที่ต้องการใช้เอาฟังก์ชันนี้ไปใช้จะเกิดคำถามทันทีว่า Exception ที่ว่ามันคืออะไร แล้วจะจัดการยังไง ยิ่งตัว Code นี้อยู่ในระดับ low level แล้วมีคนเรียกใช้มันจากระดับสูงๆ จะมีสักกี่คนไปอ่าน Manual หรือ Source code ว่าตัว Exception ที่โยนออกมาหมายถึงอะไร วิธีแก้ก็ไม่ยากไปหา Exception ที่เหมาะสมกับตัวปัญหานี้

  • Log and Throw

คือการที่เราทำ Catch Exception แล้วทำการ Log จากนั้นทำการ Throw ออกไปอีก

1
2
3
4
5
6
7
8
9
public Customer crateCustomerFromFile(File file) throw IOException {
try {
String data = FileUtils.readFileToString(file, "UTF-8");
return new Customer(data);
} catch (IOException ex) {
logger.error("Problem with input file", ex);
throw ex;
}
}

ปัญหาของการทำอย่างนี้คือเวลาเกิดอย่างนี้เวลาไล่ตามดู Log มันจะเกิดความโคตรงงว่าตกลงมันเริ่ม Error ตรงไหน Stack trace นี่จะมั่วไปหมด ทำให้เราค้นหาต้นเหตุของปัญหายากขึ้นกว่าเดิม ดังนั้นวิธีแก้คือ ถ้าจะ Throw ไม่ต้อง log นะครับ คนข้างนอกเขาจะจัดการเองว่าเขาจะ log หรือจะไม่ log (ถ้าเขาไม่ log ก็ด่าเขาไป)

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

Ref :
https://community.oracle.com/docs/DOC-983543#catchingException
https://arxiv.org/pdf/1704.00778.pdf