Hike News
Hike News

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

Anti pattern Exception ต่อ

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

ว่าจะเขียน Blog ทุกสัปดาห์ไปๆมาๆก็ไม่ได้เขียนเนื่องจากงานเยอะแต่จริงๆคือขี้เกียจเขียน จนมีเพื่อนบอกว่า เฮ้ยเขียนทิ้งไว้ไมไม่กลับมาเขียนต่อ วันนี้เลยมาเขียนต่อให้จบดีกว่า ตอนนี้อยู่หัวข้อเดิมเรื่อง Anti pattern Exception

  • Relying on getCause()

อันนี้น่าจะเจอบ่อยใน JAVA แต่ภาษาอื่นก็ทำคล้ายๆแบบนี้ได้อาการมันประมาณว่าเราดัก Exception จากนั้นดึงเอาสาเหตุของ Exception ออกมาอีกทีหน้าตา Code มันจะประมาณนี้

1
2
3
4
5
6
7
8
9
10
11

try {
idolProfile cherprang = IdolService.getProfile("BNK48","Cherprang");
printToScreen(cherprang);
} catch( CustomException ex) {
if (ex.getCause() instanceof FileNotFoundException ) {
doClearResource();
} else {
throw ex;
}
}

ตอนแรกอ่านมันก็โอเคนะแค่ไปดึงเขาสาเหตุที่เด้ง exception ออกมาตรวจ อีกทีว่ามันคืออะไรแล้วเอาไป Check ใน if แล้วมันจะเป็นอะไรไป แต่พอมาอ่านที่เขาอธิบายมันแล้วก็คิดตามก็เข้าใจ คืออย่างแรกเข้าใจก่อนว่า Code ที่ผมเขียนอธิบายนี้เป็นส่วนเรียกใช้งาน idolService ซึ่งเป็น Code ที่คนอื่นเขียน หรือ อาจจะเป็นผมเขียน โดยผมไปอ่าน Manual และ Source code ของเขาอย่างละเอียดเลยว่ามันจะ Throw อะไรออกมาแล้วผมก็ได้ Code ประมาณนี้ คราวนี้เวลาผ่านไป idolService เกิดการแก้ไข Source code ในส่วน Implementation จากการที่เขาเก็บข้อมูลเป็น File เขาย้ายไปเก็บข้อมูลที Server เวลาดึงข้อมูลเขายิงไป Server เวลาได้ Error ก็จะเปลี่ยนจาก FileNotFoundException เป็นอย่างอื่น คราวนี้ฝันร้ายก็มาเยือนเพราะไม่มีใครรู้ว่ามันมีผลกระทบมาถึง Code ส่วนนี้ด้วย จริงๆรู้แน่ตอนทำ Full loop แต่เขามองว่าแก้ Service เกี่ยวไรกับส่วนแสดงผล ในเมื่อรักษา Interface ทุกอย่างครบ นั่นแหละครับปัญหาทีี่ทำให้การทำอย่างงี้กลายเป็น Anti pattern

  • Destructive Wrapping

ข้อนี้เลวร้ายมากมันคือการทำลาย stack trace ของ Exception ซึ่งการทำไม่ยากเลยทำด้วยวิธีนี้

1
2
3
4
5
6
7

try {
idolProfile cherprang = idolService.getProfile("BNK48","Cherprang");
printToScreen(cherprang);
} catch( CustomException ex) {
throw new idolException("Problem with net idol service" + ex.getMessage());
}

ตอนแรกอ่านก็ไม่เห็นไม่เป็นอะไรเลยนี่พอมาอ่านดีๆ อ้าว stack trace ตอนเราสั่ง print stack มันหายไปเลยมาหยุดตรงนี้พร้อม message ซึ่งบางทีมันอาจไม่ใช่ต้นกำเนิดของสาเหตุด้วย ประมาณดูหนังเจ้าพ่อที่ตำรวจตามจับแก๊งของเจ้าพ่ออยากจะสาวไปหาตัวเจ้าพ่อ กำลังไล่ตามหลักฐานจากโจรคนนึงไล่ไปหาเจ้าพ่อแต่สุดท้ายเจ้าพ่อสั่งฆ่าตัดตอนตอนก่อนถึงตัวเอง ทุกอย่างก็เลยเป็นปริศนาต่อไปเจ้าพ่อก็ยังคงลอยนวล

  • Log and Return Null

อันนี้เลวร้ายพอๆกับข้างบนและผมก็เคยทำด้วยเพราะคิดว่าไม่เป็นไร ตัวอย่าง Code มันจะประมาณนี้

1
2
3
4
5
6
7
8
9
10
11
12
13
14

public class IdolService {
public IdolProfile getProfile(String group, String name) throws CustomException {
try {
InputStream input = FileUtils.openInputStream(new File("idol.dat"));
return streamToProfile(input, group,name);
} catch (FileNotFoundException ex) {
return null;
} catch (IOException ex) {
throw ex;
}
}
}

ถามว่าทำไมเขียนอย่างนี้คือผมไปอ่านไฟล์ที่ชื่อ “idol.dat” ซึ่งผมการันตีเลยว่าไฟล์นี้มีแน่นอนแล้วผมก็ทิ้ง code นี้ไว้อย่างนี้ จนวันนึง มีคนมาแก้ไขข้อมูลในเครื่อง server ตามรอบการดูแลของเขา เขาเขียน script บางอย่างเกี่ยวกับการ backup แล้วมันดันเขียนผิดไปล้ายไฟล์ “idol.dat” ของผมไปที่อื่น แต่ service ของผมยังทำงานปกติเหมือนไม่มีอะไรเกิดขึ้นทั้งๆที่เกิดปัญหาอยู่ ลูกค้าโทรมาหาผมพร้อมบอกว่า เฮ้ยทำไม Search หาข้อมูลน้องคนนี้ไม่เจอเลย เกิดอะไรขึ้น ตอนนั้นผมคงไปหาข้อมูลใน Log พร้อมกับงงๆว่าทุกอย่างทำงานปกติดีนี่ไม่มี Error ไม่มีอะไรเลย คงต้องใช้เวลานานระดับหนึ่งเลยกว่าผมจะรู้ว่ามันเกิดจาก Source code ตรงนี้ ดังนั้นอย่าทำแบบนี้นะครับอันตรายมากๆ

ก็จบไปแล้วนะครับกับเรื่อง Exception จริงๆมีอีกหลายอย่างแต่ผมไม่เข้าใจมันอย่างแท้จริง หรือยังไม่เคยโดนกับตัวเลยไม่อยากจะเขียนเพราะจะทำให้เกิดความเข้าใจผิด ส่วนใจสนใจอ่านต่อไปอ่านได้ตาม ref ที่ผมแนบไว้ข้างล่าง

ปล. ผมไม่ได้เป็นแฟนคลับ idol วงไหนนะครับพอดีเห็นเพื่อนๆผมหลายคนชอบแล้วน้องเขาก็ดูน่ารักดีเลยเอามาเป็นมุกเวลามาอ่านบทความนี้จะได้ไม่เซ็งหรือเครียดจนเกินไป

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

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

Dependency Inversion

Dependency คืออะไร

ไม่รู้จะเขียนอะไรจะเขียน Angular4 ก็ยังไม่เคยลอง จะ Webpack ก็เขียนไปแค่ Project เดียว แถมยังใช้ angularjs อยู่ด้วยเลยยังไม่เอามาเขียน ตอนนี้เลยขอกินบุญเก่าเอาเรื่องที่เรียนตอนปี 4 วิชา OOAD (อีกแล้ว) มาเขียนดีกว่า

Dependency คือ เอ่อ… อธิบายยากเหมือนกันนะ ไม่รู้จะอธิบายยังไงให้กลางและเข้าใจง่าย ขอเป็นอธิบายในมุมมองของผมละกัน Dependency เหมือนกับว่าถ้าเราจะใช้ Module นี้เราจะต้องทำอะไรบ้าง เช่น ต้องส่งตัวแปรเข้าไปยังไง ต้องมี Environment แบบไหน ต้องมีเปิด Permission อะไร และอื่นๆที่ทำให้เราสามารถใช้ Module นั้น อ่าถ้าจะให้ยกตัวอย่างก็ประมาณ LEGO อะ ถ้าเราอยากต่อกับ LEGO 4 หัวทิ่มและก็ต้องมี LEGO ที่มีรูให้เสียบ 4 รูตรงกับ LEGO ที่เราจะไปต่อ หรือ ถ้าเป็นนักเล่นเกมส์อยากเล่นเกมส์ก็ต้องไปตรวจสอบ Spec คอมว่ามันต้องลงอะไรบ้าง บางทีต้องไปโหลดส่วน Lib C++, DirectX8, 9, 10 มาลงบ้างเพื่อให้สามารถเล่นเกมส์ได้

เวลาเปลี่ยนอะไรก็เปลี่ยน

วันหนึ่งกำลังเขียน Code แล้วมีคนเดินเข้ามาแล้วบอกว่า “ต้องเปลี่ยนตรงนี้นะ” , “เปลี่ยน DBMS” , “ลูกค้าอยากได้…เลยต้องเปลี่ยนไปใช้…” , “เข้าไม่อยากใช้ Rest เขาอยากใช้ FTP” ในชีวิตคนสายนี้จะต้องได้ยินคำพูดพวกนี้มาบ้างไม่มากก็น้อย มันก็ตลกในหลายๆครั้งที่อาชีพที่เราทำมัน อยากจะเปลียนอะไรก็เปลี่ยนกันง่ายอะไรขนาดนัน อาจเป็นเพราะอาชีพที่เราทำมันเพิ่งมีขึ้นมายังไม่ถึง 100 ปี อะไรๆมันก็เลยไม่นิ่งไม่มีอะไรตายตัวเมื่อเทียบกับสายงานสร้างอื่นๆไม่ว่าจะโยธา เครื่องกล หรืออย่างอื่นที่ศาสตร์ของเขามีมานานมีการลองผิดลองถูกจนได้วิธีการที่ดีที่ถูกต้องแล้ว หรือจริงๆแล้วงานของพวกเราต้องยอมรับกับความเปลี่ยนแปลงได้เสมอ

เมื่อเปลี่ยนก็ต้องแก้

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

Dependency Inversion

เป็นตัวย่อ D จากกฏ SOLID ตัว Dependency Inversion ว่าง่ายๆคือ …. เอออธิบายยากเหมือนกันว่ะ เอาเป็นยกตัวอย่างละกัน ผมเคยเขียน Web appliction ตัวนึงตอนนั้นพัฒนาโดยใช้ Database เป็น NoSQL รูปแบบหนึ่ง คราวนี้ด้วยความอ่อนด้อยด้านประสบการณ์ เวลาเข้าถึงข้อมูลผมก็เขียนแบบให้มันต่อ Database ตรงๆเลย ทำอย่างรวดเร็วว่องไว Project นี้ก็จบไป ผ่านไปไม่นาน คนที่รับ Project นี้ไปทำต่อมาพูดคุยว่าต้องเปลี่ยนจาก NoSQL เป็น SQL เนี่ยจะต้องแก้อะไรบ้าง ตอนนั้นนี่บอกเลยว่าแก้เยอะมากๆ เยอะแบบเยอะที่สุด แถบจะทุกที่เลยเพราะว่าเราเขียนต่อ Database โดยตรงไปทุกส่วน ใจผมตอนนั้นนี่บอกเลยว่า เขียนใหม่อาจจะง่ายกว่าเสียด้วยซ้ำ พออ่านถึงตรงนี้พอจะเริ่มเข้าใจเล็กน้อยแล้วใช่ไหมครับ

ภาพแสดงการ Design ที่ dependency กับ DBMS

ปัญหาของ Code ที่ผมเขียนคือผม Design ออกแบบข้อมูลโดยอ้างอิงจาก DBMS เป็นหลัก พอได้ปุ๊ปก็ออกแบบ Data model ตาม DBMS connection lib return ออกมาให้แบบเพียวๆไปจากนั้น
ก็ให้ส่วน Business logic มีวิธีเรียกใช้ DB และเห็นข้อมูลตาม Data Model จะเห็นว่าอะไรก็ไปขึ้นอยู่กับ DBMS connection lib

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ตัวอย่าง Code ที่เขียนด้วย NodeJs

/* RpgUserObject จะประมาณนี้
{
_id : String
name : String
username : String
password : String
item : [Object]
}
*/

// ใน Controller สักที่หนึ่งจากหลายล้านที่

function handleFindRpgUser(req, res) {
var _id = req.headeer("_id");
db.rpgUser.findOne({_id: _id }, function(err, result) {
if (err) {
res.status(500).send("Internal server error");
} else {
res.json(result);
}
});
}

พอมันเกิดอะไรเปลี่ยนแปลงกับ DBMS ไม่ว่าจะเป็นเปลี่ยน (อันนี้ยากแต่ถ้าเปลี่ยนล่ะก็) แต่ที่ผมโดนมาคือ DBMS connection lib มันเปลี่ยน version แล้วเขาก็เปลี่ยนการรูปแบบการ Return แปลว่ามันจะเกิดอะไรขึ้น ทุกส่วนที่ยุ่งเกียวกับ DBMS connection lib จะต้องมีการแก้ไขแน่นอน ดูเส้นได้เลย ผมเลยบอกว่าถ้ามานั่งไล่แก้ซึ่งไม่รู้กี่ที่ กระทบกี่ตำแหน่ง ต้อง Test ใหม่อาจจะ Test ทั้งหมด ถ้าใช้ Automate ก็โชคดีไปแต่ถ้ามีอเปล่าก็…. ไปนึกภาพเอาเองละกัน

กลับหัว

ภาพแสดงการ Design ที่ dependency กับ DBMS

หลังจากเกิดปัญหานี้ผมก็ลองไปค้นหนังสือสมุดที่เคยเรียน หาใน internet ว่ามีวิธีการดีๆอะไรบ้าง จนเจอเรื่อง SOLID แล้วก็เห็นว่าคนส่วนใหญ่เจอปัญหาแบบเดียวกันหมด เขาเลยมาตั้งเป็นหลักการ จากการอ่านคือ ผมไปเขียน Code ไปยึดติดกับส่วนที่เป็น low level ในส่วนที่อธิบายให้เราแก้ Design ทำการสร้างส่วนที่เป็น Abstract หรือ Layer ขึ้นให้มันบอกแค่มันทำอะไรได้ return อะไร อย่างของผมสร้างเป็น Data layer ผมกำหนดไปเลยว่า Data model หน้าตาเป็นยังไง มันสามารถค้นหาด้วยอะไรบ้าง ส่วนวิธีค้นหาเป็นยังไงผมไม่บอกรู้แค่ว่าไปหามาให้ได้ก็พอ หรือง่ายๆเวลา Design ให้พยายาม Design ให้เป็นอะไรแบบ หน้าตาเป็นยังไง เรียกแล้วออกมาได้อะไร ส่วนวิธีการเป็นหน้าที่ของ low level เป็นคนพยายามทำให้ได้ตามรูปแบบที่ hight level กำหนดไว้

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ตัวอย่าง Code ที่เขียนด้วย NodeJs

///ไฟล์ RpgUserDataLayer.js
exports.findById(_id, callback) {
// implement how to get data from database
db.rpgUser.findOne({_id: _id }, callback );
};

/// ใน Controller จากหลายล้านที่
function handleFindRpgUser(req, res) {
var RpgUserDataLayer = require("path/to/RpgUserDataLayer.js");

RpgUserDataLayer.findById(req.header("id"), function(req, res) {
if (err) {
res.status(500).send("Internal server error");
} else {
res.json(result);
}
});
}

ไม่เห็นมีไรเปลี่ยนแปลงแถมซับซ้อนขึ้นอีก “หลอกกันนี่นา”

ครับซับซ้อนขึ้นแน่นอนครับ แต่เมื่อเกิดการเปลี่ยนแปลงที่ชั้น DBMS เช่น เปลี่ยนยี่ห้อ เปลี่ยนการ Design model อันนี้ผมยกตัวอย่างเปลียนเป็น SQL จะเกิดอะไรขึ้น สิ่งที่ผมต้องไปแก้ก็แค่ที่เดียวคือ RpgUserDataLayer.js ส่วนที่ Controller อีกหลายล้านที่ผมก็ไม่ต้องแก้เพราะ Data layer ผมไม่ได้เปลี่ยนรูปแบบการรับส่งข้อมูล

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ตัวอย่าง Code ที่เขียนด้วย NodeJs

///ไฟล์ RpgUserDataLayer.js
exports.findById(_id, callback) {
// implement how to get data from database
connection.query("SELECT * FROM RpgUserTable WHERE _id = '" + _id + "'", function (err, resultUser) {
if (err) {
callback(err, null);
} else {
connection.query("SELECT * FROM RpgUserItemTable WHERE _id = '" + _id + "'", function (err, resultUserItem) {
resultUser.item = resultUserItem;
callback(err, resultUser);
});
}
}
};

สรุปเลยดีไหม น่าจะยาวเกินไปละ

ส่วนสรุปเป็นส่วนที่เขียนยากมากๆเลยนะไม่รู้ว่าจะสรุปยังไง เอาเป็นว่า Dependency Inversion คือการกลับความสัมพันธ์ Dependency จากที่เราไปยึดติดขึ้นตรงต่อกับชั้น Low level เราควรกลับหัวความสัมพันธ์ เราควรให้ส่วน Low level ขึ้นตรงกับ Hight level เหมือน code ที่ผมใช้ตัว Low level คือ DBMS connection lib ที่ต้องพยายามเขียน พยายามหาวิธีดึงข้อมูลออกมาให้ตรงกับรูปแบบ Data layer ที่ผม Design ไว้

แหล่งอ้างอิงและเรื่องที่น่าไปอ่านต่อ :
https://en.wikipedia.org/wiki/Dependency_inversion_principle
https://en.wikipedia.org/wiki/SOLID_(object-oriented_design)
https://en.wikipedia.org/wiki/Coupling_(computer_programming)
https://en.wikipedia.org/wiki/Software_design_pattern
https://sourcemaking.com/

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

ใช้ static ได้ไหม

Static

วันหนึ่งระหว่างทำงานรุ่นน้องที่ทำงานถามว่าผมจะทำ Class แล้วใช้ Method เป็น Static ได้ไหม (static คือ keyword ของ java ที่ทำให้สามารถใช้ method หรือ attribute ได้โดยไม่ต้องสร้าง object) ตอนนั้นในสมองก็ผุดคำนึงตอนเรียนวิชา OOAD (Object-oriented analysis and design) ที่ว่า static is evil ขึ้นมาทันที แต่ตอนนั้นก็ต้องฟังก่อนว่าทำไมต้องใช้ จริงๆถามว่าใช้ได้ไหมคำตอบก็คือว่าได้ จริงๆแล้วจะทำอะไรก็ได้หมดแหละจะ SELECT * FROM TABLE แบบไม่ limit จะตั้งชื่อตัวแปรมั่วๆ หรืออะไรก็ตามแต่ แต่คำถามคือมันสมควรทำไหม ในมุมมองของผมการจะใช้ static ต้องดูงานที่จะใช้กับมันว่าทำไมต้องใช้ อย่างแรกเราต้องทำการแยกระหว่าง Class กับ Object ก่อน

Class Vs Object

ภาพแสดงความแตกต่างระหว่าง Class กับ Object ขอขอบคุณภาพจาก https://www.pinterest.com

ถ้าจะเปรียบเทียบ Class เปรียบเสมือนพิมพ์เขียวหรือกรอบที่บอกว่าสิ่งๆหนึ่งต้องมีค่าอะไรบ้างและสามารถทำอะไรได้บ้างเหมือนกับรูปภาพด้านบน Class Dog เป็นตัวบอกว่าหมาตัวนี้ต้องมีค่า Color, Height, Weight และต้องสามารถ Sit, LayDown ได้เป็นต้น แต่ Class ก็ยังคงเป็น Class เป็นแค่กรอบไม่ได้มีตัวตนจริงๆ การที่จะมี Dog จริงๆปรากฏขึ้นมาเราจะต้องทำการสร้าง Object ขึ้นมาภาษาโปรแกรมคือการ Create instance ขึ้นมา Object ที่ถูกสร้างขึ้นมาจะมีค่าเหมือนกับภาพด้านบนที่เมื่อเรา Create instance ของ Class Dog ออกมาเป็น Rayne (อ่านว่าอะไรไม่รู้) จะเห็นว่า Rayne จะมีค่า Attribute ต่างๆขึ้นมาจริงๆและสามารถเรียกใช้ Method ได้

มีไว้ทำไม Static

คราวนี้คำถามว่าแล้วจะมี Static ไปทำไมล่ะ บางครั้งเรามีบางสิ่งที่ไม่จำเป็นต้องมีตัวตนจริงๆแต่ก็สามารถใช้ได้ยกตัวอย่างง่ายๆของ Java คือ Class Math (ได้ยินชื่อแล้วนึกถึงตอนอ่าน Engineering mathematics 1 เลย) ที่มี method และ attribute ที่เกี่ยวกับการคำนวนไว้อยู่แล้วจะเห็นว่า Math ไม่จำเป็นต้องมีตัวตนจริงๆ (ในความหมายของผมคือ Object ) ก็ได้ (ถ้ามีตัวตนเลขวิ่งเข้ามาผมผมคงกลัวน่าดู)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

// STATIC MAX_LEG

public class Dog {
public static final int MAX_LEG = 4;
....
}

// NORMAL MAX LEG

public class Dog2 {
public int MAX_LEG = 4;
.....
}

/// ANOTHER CLASS USE Dog and Dog2

// STATIC
System.out.println("Dog has max leg : " + Dog.MAX_LEG);

// NORMAL
Dog2 tempDog2 = new Dog2();
System.out.println("Dog has max leg : " + tempDog2.MAX_LEG);

หรือว่าบางที Class อาจจะมีคุณลักษณะบังคับที่ทุก Object ที่ถูกสร้างจาก class นั้นต้องมี และ เราต้องการบอกให้คนที่อ่าน class รู้ว่ามี ตัวอย่างเช่นค่าคงที่บางค่าเช่น เรากำหนดค่า MAX_LEG ของ Dog ให้เท่ากับ 4 เพื่อบอกให้ทั้งจักรวาลรู้ว่า Class Dog อะขามีมากสุดได้ 4 โดยเรียกใช้ผ่าน Class โดยใช้วิธี static แต่วิธีธรรมดาต้องสร้าง Object ขึ้นมาเพื่อถามว่า MAX_LEG มีเท่าไหร่ ลองนึกภาพตลกๆดูเอาละกันว่า อยากจะรู้ว่า Dog มีลักษณะคร่าวๆเป็นยังไงแล้วเราต้องสร้าง Object Dog ขึ้นมา 1 ตัว จากนั้นดูค่าขามากที่สุด พอหมดประโยชน์ก็ทำลาย Dog ทิ้งคงน่าสงสารแย่ (จริงๆคือมันเสียเวลาสร้างและ memory อีกต่างหาก)

ใช้ Static ทุกที่ไปเลย เย้

ตอนผมรู้จัก static ผมก็กะทำอย่างนี้เลยปัญหาคืองานบางงานมันไม่ได้ออกแบบมาให้ใช้ static ลองนึกภาพงานงานนึงนะครับต้องการ print ชื่อคนออกหน้าจอนะครับ ถ้าเขียนแบบ Static จะเป็นแบบนี้

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class People {
public String name;
}

public class PrintPeople {
public static void printPeople(People people) {
System.out.println("People name is : " + people.name);
}
}

// Use it
Perople people = new People();
people.name = "Syntax";

PrintPeople.printPeople(people);

จากภาพจะเห็นว่าแค่จะ Print ถึงกับต้องสร้าง 2 Class มาทำงานแบบนี้ ถามว่าจริงๆผิดไหม ผมว่าไม่ผิดหรอกครับคล้ายๆเราเขียนภาษา C ที่กำหนด struct แล้วเขียน function รับ struct เข้าไปเพื่อ print ออกมา แต่ตอนนี้เราเขียน OOP ที่มันมี method ผูกติดกับ Class แทนที่เราจะมี 2 class เราทำ class เดียวไปเลยสิ

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

public class People {
public String name;
public void printPeople() {
System.out.println("People name is : " + name)
}
}

// Use it

Perople people = new People();
people.name = "Syntax";

people.printPeople();

อีกทั้งทุกครั้งที่เราประกาศ static method แล้วเรียกใช้ attribute หรือ method ใน Class ตัวเอง มันจะเป็นการบังคับให้เราต้องให้ attribute หรือ method ที่ถูกเรียกใช้เป็น static ไปด้วย (เหมือนไวรัส) และ ตัว Attribute ที่เป็น public static ที่ไม่ได้ประกาศ final มันก็คือ Global variable ดีๆนี่เอง ใครจะเรียกใช้ ใครจะแก้ก็ได้ตามใจ แก้ตอนไหนก็ไม่รู้ ดังนั้นอันตรายมากที่จะใช้ attribute เป็น public static

สรุป

หลังจากร่ายยาวมานานผมสรุปคร่าวๆได้ประมาณนี้ “เราสามารถใช้ทั้ง static และ ธรรมดาร่วมกันได้แล้วแต่สถาการณ์และความเหมาะสม” และจากนี้จะเป็นวิธีการเลือกใช้ไม่ใช้ static ตอนเขียน code ของตัวผมเองว่าตอนใช้ตอนไหนไม่ใช้

  • static
    1. เราใช้ static กับ Attribute มันเป็นค่าคงหรือค่าที่อยากให้ Class อื่นเห็นและสามารถใช้งานได้ ส่วนใหญ่ผมจะประกาศเป็น public static final type ATTRIBUTE_NAME = X ;
    2. เราใช้ static กับ Method เมื่อมันเป็นการใช้งานระดับ Class ไม่ใช่ระดับ Object ขอให้นึกถึงตอนเรียกใช้ Math หรือ MAX_LEG เป็นต้น
    3. เราใช้ static กับ Method เมื่อเรามองว่าเราสร้าง Class นั้นมาเป็น Util หรือ Function Class ประมาณว่าเป็น Class คอยช่วยทำงานบางอย่างซ้ำๆแบบไม่ต้องมีตัวตน Object ตัวอย่างที่เห็นได้ชัดคือ FileUtils, IOUtils

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

เรื่องของ gitignore

gitignore

ในยุคสมัยใหม่แห่งการเขียน Code แทบทุกคนจะต้องรู้จัก Git ซึ่งเป็นตัว version control ของ Source code ซึ่งทำหน้าที่ดูว่าไฟล์ไหนถูกเปลี่ยน ไฟล์ไหนเพิ่ม ไฟล์ไหนหายไป และทำ version ของ source code เพื่อให้เราสามารถย้อนไปย้อนกลับตัว source code ได้ ซึ่งเป็นที่นิยมมากในปัจจุบัน เพราะใช้งานง่าย ฟรี และมี Community มากมายมหาศาล แล้วตัว gitignore มันคืออะไรล่ะ ตัว gitignore ก็คือไฟล์ที่ชื่อ .gitignore ซึ่งเป็นตัวบอกตัวโปรแกรม git ว่า “เราจะไม่สนใจไฟล์เหล่านี้” ย้ำอีกครั้งว่า “เราจะไม่สนใจไฟล์เหล่านี้” เราจะไม่ track version มัน

พูดมาถึงตรงนี้ก็คงจะมีคำถามว่ามีไปทำไม คำตอบคือ Git นัั้นต้องการเก็บ source code ไม่ได้ต้องการเก็บอย่างอื่น แต่ในโปรเจคที่เรากำลังพัฒนานั้นมันมีหลายอย่างเช่น Lib ไฟล์ที่ compile ไปแล้ว ซึ่งเวลาเราสั่งกับ git ว่ามีไฟล์ไหนเปลี่ยนแปลงหรือยังไม่ได้ tracking เข้า version มันจะแสดงให้เราเห็นด้วย ซึ่งบางทีมันสร้างความรำคาญสำหรับคนที่รู้ว่ามันไม่ใช่ไฟล์ที่ต้องการแต่ดันมาแสดงให้ดู แต่ร้ายกว่าคือคนที่พึ่งหัดเขียนหรือไม่รู้ เขาจะทำการ Add ไฟล์พวกนี้เข้าไปใน git ด้วย ทำให้ตัว repo มันใหญ่ ซึ่งจริงๆ source code อาจจะมีขนาดแค่ 50 KB เท่านั้น ตอนผมเริ่มหัดใช้ git ใหม่ก็เอาไฟล์ lib ขึ้นไปบน git ด้วย เวลา clone ทีนี่รอ 10 กว่านาที

แล้วเราจะต้องมานั่งหาเหรอว่าเราจะไม่เอาไฟล์อะไรบ้าง คำตอบคือต้องครับแต่ไม่มากหรือยากเย็นอะไรขนาดนั้น เพราะมนุษย์โลกจำนวนหนึ่งได้สร้าง Tool ขึ้นมาเพื่อ Gen ไฟล์ gitignore ให้กับเรา ซึ่งสามารถไปใช้งานได้ที่ https://www.gitignore.io/ โดย Tool ตัวนี้จะให้เราใส่ ภาษา หรือ IDE ที่ใช้พัฒนา ตัว Tool จะสร้างไฟล์ .gitnore ออกมาให้ จากนั้นเราก็แค่เอาไปวางที่ directory ที่ source code โปรเจคเราอยู่ เพียงเท่านี้ก็ช่วยเราจัดการกับพวกไฟล์พื้นฐานที่เราจะไม่เอาเข้า git ที่เหลือเราก็ไปเพิ่มเอาเองว่าจะไม่เอาไฟล์ไหนขึ้น git อีก

ตัวอย่างการใช้งาน

กรณีเติมในช่วงว่างเป็น Node

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

# Created by https://www.gitignore.io/api/node

### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# Typescript v1 declaration files
typings/

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env


# End of https://www.gitignore.io/api/node

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

เรื่องของ 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
10
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

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

Exception

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

Exception คืออะไร Exception คือ อาการผิดปกติหรือสถานะที่ไม่ปกติของโปรแกรมที่ไม่ได้อยู่ในการควบคุมของโปรแกรมส่วนนั้น ตัวอย่างง่ายๆคือ เรียกฟังก์ชั่นอ่านไฟล์แต่เกิดปัญหาคือไฟล์ที่จะอ่านไฟล์นั้นไม่มีอยู่จริง ซึ่งฟังก์ชันอ่านไฟล์เขา Design ไว้แค่อ่านไฟล์ไม่ได้ครอบคลุมถึงการดูแลว่าถ้าเกิดไฟล์ไม่มีอยู่จริงจะต้องทำยังไง ดังนั้นคนที่ Design ฟังก์ชั่นอ่านไฟล์เขาจึงใช้ Feature Exception ทำการส่ง Exception ออกไปบอกว่า “ผมเปิดไฟล์นี้ไม่ได้นะไฟล์นี้ไม่มีอยู่จริงอันนี้ผมไม่สามารถตัดสินใจได้ว่าผมจะทำยังไงเมื่อไฟล์ไม่มีอยู่จริง คุณไปตัดสินใจเอาเองก็แล้วกันว่าจะทำไงต่อ”

ผมเชื่อว่าจะต้องคำถามเกิดขึ้นมาในใจ อ้าวทำไมต้องโยน Exception ออกมาทำไมไม่จัดการมันในฟังก์ชันนั้นเลยล่ะ คำตอบก็ง่ายๆครับคือ เขา Design ให้มันไปใช้ได้หลายที่ ประมาณว่าเขียนฟังก์ชันอ่านไฟล์ที่เดียว คุณสามารถเอาไปใช้ได้กับทุกที่ทุกงาน แต่ละงานอาจจะต้องการการจัดการ Exception ต่างกันเช่น โปรแกรมกระโหลกกะลาของผมเวลาเจอ Exception นี้ผมสั่งให้โปรแกรมมแสดงหน้าจอตลกๆว่า “Correct file path ?” แต่บางโปรแกรมเขาอาจจะเป็น GUI แสนสวยพร้อมบอกว่า คุณเลือกไฟล์ที่ไม่มีอยู่จริงนะพร้อมใจดีให้ User กรอก ตำแหน่งไฟล์ใหม่ หากเราไม่ทำเป็น Exception กลางๆก็เท่ากับว่าเวลามีการแก้ไขวิธีการจัดการปัญหาเราต้องไปแก้ฟังก์ชันอ่านไฟล์ด้วย ซึ่งมันไม่ควรเพราะงานสองงานนั้นแยกเป็นคนะลงาน งานอ่านไฟล์ก็อ่านไฟล์ งานจัดการกับความผิดพลาดก็งานจัดการกับงานผิดพลาด

1
2
3
4
5
6
7
8
9
10
11
// EXAMPLE try catch exception
// from file input stream
try {
File file = new File("/root/text.txt");
IOStream input = FileUtils.openInputStream(file, "UTF-8");
/// do someting with input stream
} catch(FileNotFoundException ex) {
System.out.println("Correct file path ?");
} catch (IOException ex) {
System.out.println("Hey my program have a problem with file");
}

เพิ่มเติม FileUtils เป็น Lib ที่ช่วยเกี่ยวกับการจัดการไฟล์ไม่ว่าเปิด ปิด ลบ หรือ อื่นๆ แนะนำให้ใช้จะได้ไม่ต้องมาจัดการเอง

ประเภทของ Exception

ในภาษาแต่ละภาษาที่มี Feature Exception นั้นมีการแบ่งประเภทไว้แตกต่างกันแต่มันก็มีแนวคิดคล้ายคลึงกันโดยผมขอยึดตามตัวของภาษา Java ละกัน java แบ่ง Exception ออกเป็น 2 พวกใหญ่ๆคือ Check exception กับ Uncheck exception

Image of exception from : /www.tutorialspoint.com

Check Exception คือ Exception ที่เราต้องเตรียมจัดการกล่าวคือคนเขียนฟังก์ชันเขามองดูแล้วว่า เฮ้ยมันจะต้องเกิด Exception พวกนี้ขึ้นมาแน่ๆซึ่งเป็นเหตุปกติที่มาจากการใส่ชื่อไฟล์ไม่ถูกต้อง หรือ ต่างๆนาๆ ที่คุณต้องเตรียมจัดการนะตัวอย่างง่ายๆคือ IOException และ FileNotFoundException เป็น Exception ที่คนเขียนฟังก์ชันอ่านไฟล์บอกเราแล้วว่า มันมีแน่ๆเราจึงต้องเตรียมจัดการมันเหมือน Code ข้างบน

Uncheck exception คือ Runtime exception จากภาพเราจะเห็นว่า Runtime exception ก็เป็นหนึ่งใน Exception ตามการ Inheritance แต่ในส่วนของ Rumtime exception เขาได้เพิ่มความสามารถเวลาโดน check ว่าจะต้อง check ไหมให้ไม่ได้ต้อง check โดยใช้คุณสมบัติ Polymorphism (เริ่มนอกเรื่อง) เอาเป็นว่าไอ Uncheck exception มันเป็น Exception แบบไม่ที่ต้องมาบอกว่าต้องจัดการนะโดยส่วนใหญ่มันเกิดจากความผิดพลาดของคนเขียน Code ตัวอย่างง่ายๆคือ NullPointerException พี่ส่ง Null ไปให้คนอื่นใช้ หรือ InexOutOfBoundException พี่อ้างอิงถึง index ที่เกิน Array คือลองคิดดูว่าถ้าเราบอกว่าต้องจัดการ NullPointerException, InexOutOfBoundException เนี่ย Code เรามันจะวุ่นวายขนาดไหนกัน เขาเลยเอามันมาเป็น Uncheck exception ถ้าคุณเขียน Code ดีพอ Save พอเหตุการณ์พวกนี้มันจะไม่เกิดไอพวกนี้คือสิ่งที่คุณ Control ได้มันเป็นสิ่งที่คุณ Control ได้โดยไม่ต้องบอกให้จัดการ

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

Ref :
https://community.oracle.com/docs/DOC-983543
http://www.onjava.com/pub/a/onjava/2003/11/19/exceptions.html
https://www.tutorialspoint.com/java/java_exceptions.htm

รักเธอแล้ว

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

Programmerรักเธอแล้ว

รักเธอแล้วแม้จะปวดใจที่เธอไม่รัก ทุกข์ใจที่ทำได้แค่มอง ทั้งที่รู้เป็นเพียงฝัน ทั้งที่รู้ว่าสุดท้ายต่อให้รักเธอมากเพียงใด ความรู้สึกนั้นเธอก็ไม่เห็น แต่จะเป็นไรเล่าเพราะอย่างน้อยเราก็ได้รักเธอแล้ว

ไม่อยาก

ไม่อยากมีเธออยู่ในความทรงจำ

ไม่อยากช้ำที่คิดถึงเพียงแต่เธอ

ไม่อยากเป็นเหมือนคนบ้าคอยพร่ำเพ้อ

ไม่อยากเป็นคนที่เธอไม่สนใจ

Programmerไม่อยาก

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

© 2024 Normal Programmer Blog All Rights Reserved.
Theme by hipaper