นึกถึงระบบหนึ่งที่ต้องลงทะเบียนผู้ใช้งาน เมื่อฝั่ง Business อธิบายมาว่าระบบนี้จะต้อง
- ตรวจสอบข้อมูลของลูกค้า
- เก็บไฟล์ข้อมูล Avatar
- ตอบกลับลูกค้าว่าสำเร็จหรือไม่
โค้ดที่อธิบายได้ชัดเจนที่สุดคือ
class User {
public User Register(string name, string password, stream avatar) {
// Validate
var user = new User(name, password);
if (!user.valid()) throw new InvalidUserException();
// Uploading
var avatarPath = Uploader.upload(avatar);
user.avatarPath = avatarPath;
// Save
user.save();
return user;
}
}
จะเห็นว่าโค้ดชุดนี้แทบจะล้อกับ Flow งานที่ Business เข้าใจได้เลย
ถัดมาถ้าสมมติว่าระบบเราต้องการให้สามารถอัพโหลดได้สองที่ล่ะ มี S3 (ของ Amazon) กับ Google Stroage ของ Google แล้ว
ถ้าเราสร้าง Class สำหรับ Upload S3,
ถ้าเอาดื้อๆ เลยก็จะเป็น
class User {
public User Register(string name, string password, stream avatar) {
// Validate
String avatarPath = "";
if (Environment.get("STORAGE") == "S3") {
// Setup S3 Uploader
// Setup credentials
// Setup some more
avatarPath = Uploader.uploadS3(avatar);
} else {
// Setup Google Uploader
// Setup credentials
// Setup some more
avatarPath = Uploader.uploadGoogleCloud(avatar);
}
user.avatarPath = avatarPath;
// Save
}
}
(ขอละส่วนไม่เกี่ยวข้องทิ้งนะครับ)
แต่พอมาถึงจุดนี้แล้ว เราจะพบว่า โค้ดของเรากับ Business flow ที่คนธุรกิจเข้าใจได้ ไม่ค่อยจะตรงกัน เรามีรายละเอียดมากกว่า โค้ดมีรายละเอียดด้าน Infrastructure อยู่ด้วย
ดังนั้นตอนนี้โปรแกรมเมอร์ที่จะแก้โค้ดนี้ก็ต้องเข้าใจงานด้าน Infrastructure ด้วย
ทีนี้ถ้าเราไม่อยากให้คนทำโค้ดตรงนี้ต้องเข้าถึงรายละเอียดทาง Infrastructure ล่ะ เพราะการจะสร้าง Google Cloud uploader อาจจะต้องใช้ Credential ที่ซับซ้อน มีขั้นตอนมากมาย ถ้าให้ Junior developer ที่พึ่งเข้ามาใหม่มาเห็นเดี๋ยวจะช็อคกันไปข้าง
ทำไงดี
เราสามารถเขียนแบบนี้ได้
// ตัว Upload ต่างๆ แยกกันคนละคลาส
interface IUploader {
public string Upload(Stream fileContent);
}
class S3Uploader: IUploader {
public string Upload(Stream fileContent) { // Something }
}
class GoogleCloudUploader: IUploader {
public string Upload(Stream fileContent) { // Something }
}
// Factory เลือกคลาสตาม Environment
class Uploader {
public static IUploader CreateUploaderByEnvironment() {
if (Environment.get("STORAGE") == "S3") {
var result = new S3Uploader();
// Setup S3 Uploader
// Setup credentials
// Setup some more
return result;
} else {
var result = new GoogleCloudUploader();
// Setup Google Uploader
// Setup credentials
// Setup some more
return result;
}
}
}
ทีนี้เราก็จะตัวเนื้อโค้ดในส่วนของกระบวนการลงทะเบียนผู้ใช้ ก็ยุบให้เป็นแบบนี้ได้
class User {
public User Register(string name, string password, stream avatar) {
// Validate
var user = new User(name, password);
if (!user.valid()) throw new InvalidUserException();
// Uploading
var avatarPath = Uploader.CreateByEnvironment().upload(avatar);
user.avatarPath = avatarPath;
// Save
user.save();
return user;
}
}
ตอนนี้โค้ดกับ Business flow แทบจะล้อกันอีกแล้ว และคนสามารถแก้ไขโค้ดชุดนี้ได้โดยไม่ต้องสนใจรายละเอียดทาง Infrastructure ได้อีกครั้งหนึ่ง เหมือนโค้ดชุดแรกสุดเลย
เอาโค้ดชุดนี้ไปให้คนที่ไม่รู้ S3, Google Storage ทำงานยังไง เขาก็ไม่มีวันทำพัง เพราะ ส่วนของการเซ็ตระบบตรงนั้นไปอยู่ใน Uploader.CreateByEnvironment()
จุดที่น่าสนใจคือ
- เราทำให้ S3Uploader, GoogleCloudUploader ใช้แทนกันได้ ซึ่งเรียกว่า Polymorphism
- การที่มี Method นึงสำหรับสร้างของตามสถานการณ์ต่างๆ เรียกว่า Factory Pattern ถ้าสนใจลองไปอ่านต่อได้ (แต่ผมไม่ชอบใช้คำว่า Factory เองแหละ)
นอกจากนั้น ถ้าเรามีหลายกรณีที่ Implementation ขึ้นกับ Configuration เราสามารถผลักมันไปไว้ที่เดียวกันได้หมดเลยนะ
class EnvImplementationProvider {
public Uploader UploaderByEnv();
public Renderer RendererByEnv();
public Encryptor EncryptorByEnv(); // เลือก Encryption Algorithm
}
(และพวก Dependency Injection Framework ที่เราใช้กันเพื่อเลือกคลาสตอน Test, Local, Production คนละตัว ที่เห็นๆ ในตาม Spring, MVC C# เบื้องหลังก็คือมันมีคลาสแบบนี้แหละ แล้วเปลี่ยนให้เราตอนเริ่ม Server เป็นเกร็ดความรู้สำหรับใครที่คิดจะสร้าง Framework เอง ที่มีระบบ Dependency Injection)
โค้ดแบบนี้ไม่ได้ดีกว่าหรือแย่กว่า Uploader.CreateByEnvironment แต่เป็นทางเลือกหนึ่ง
จะเห็นว่าการมี Polymorphism ทำให้เรามีอิสระในการจัดวางโค้ดสูงขึ้นมากจริงๆ
=============================
เนื้อแท้ของ Polymorphism คือการซ่อนรายละเอียด ขับเน้นส่วนละเอียด
อย่างในกรณีนี้ ถ้าไม่มีเทคนิคนี้ใน Object oriented ถ้าเราอยู่ในโลกของ Procedural programming เราไม่มีทางสร้างโคตรที่ล้อ Business flow ที่ลูกค้าเข้าใจได้เลย
อย่างในกรณีนี้ ถ้าไม่มีเทคนิคนี้ให้ใช้เราไม่มีทางสร้างโคตรที่ล้อ Business flow ที่ลูกค้าเข้าใจได้เลย
เทคนิคนี้ ทำให้เราก็สามารถจัดเรียงของที่เกี่ยวข้องได้หลายแบบ
แล้วถ้ามองไปที่บริบทว่า Polymorphism ถูกโปรโมตหนักๆ ในสมัย Object-oriented ยังตั้งไข่ และสมัยนั้น ยังเป็นโลกที่กาง Requirement แล้วดีไซน์คลาส แล้วแจกแต่ละคลาสให้โปรแกรมเมอร์จำนวนมากทำโดยไม่ต้องรู้ว่าระบบใหญ่เป็นยังไง
เครื่องมือนี้ทำให้คนสามารถเขียน Upload workflow ได้ โดยไม่ต้องรู้เลยว่าจริงๆ ระบบเรามันอัพโหลดไฟล์ไปที่ไหนกันแน่
นี่คือเนื้อแท้ของ Polymorphism
มันคือเครื่องมือที่ทำให้เราสามารถอธิบายโค้ดในมุมมองที่แตกต่างกันได้
ในมุมมอง Business เราซ่อนรายละเอียดทาง Infra
ในทาง Infra ทุกคลาสที่ขึ้นอยู่กับ Environment configuration จะไม่มีรายละเอียดทางธุรกิจเลย
ผมสามารถเอาโน้ตบุ๊กเข้าห้องประชุมไปคุยกับ Stakeholder เปิดไฟล์หนึ่งขึ้นมา และโค้ดที่ผมอ่าน กับไดอะแกรมบน Powerpoint ที่อยู่บนสไลด์ จะล้อกันมาเด๊ะๆ
และผมสามารถเอาโน้ตบุ๊กเข้าห้องประชุมไปคุยกับ Infrastructure team เปิดอีกไฟล์หนึ่งขึ้นมา แล้วโค้ดที่ผมอ่าน กับ Infrastructure diagram ที่เขาเขียนแปะผนัง ก็จะล้อกันเด๊ะๆ อีก
แค่อยู่คนละไฟล์คนละคลาสกันเท่านั้นเอง
ผมสามารถใช้โค้ดเป็น Source of truth ในการคุยงานกับทุกแผนกที่เกี่ยวข้องได้โดยไม่ต้องมี Documentation แยกสำหรับว่า อันนี้ไว้คุยกับทีมนั้น อันนั้นไว้คุยกับทีมนี้
นี่คือปลายทางของ Polymorphism ที่ทำได้ถูกต้องแบบเป๊ะๆ ในอุดมคติ
บทความนี้เริ่มจากการบ่นของผมเอง
ผมบ่นเรื่อง if-else เป็น Code smell ว่ามันใช่ แต่ก็ไม่ใช่เหมือนกัน
ในทางทฤษฎี ทุกๆ If-else สามารถแก้ไขได้ด้วย Factory และ Polymorphism
ยกโค้ดแบบไร้สาระเป็น if ธรรมดาหนึ่งอันเลยนะ
class DoSomething {
public void do() {
if (this.type == "HARD") {
doThisTheHardWay();
} else {
doThisAnotherWay();
}
}
}
ผมสามารถแปลงได้เป็นแบบนี้เสมอ
interface IWay {
public void do();
}
class TheHardWay: IWay {
public void do() { };
}
class TheAnotherWay: IWay {
public void do() { };
}
class WayFactory {
public static IWay ChooseTheWay(string type) {
if (type == "HARD") return new TheHardWay();
return new TheAnotherWay();
}
}
// ของเดิมที่ถูกเปลี่ยน
class DoSomething {
public void do() {
WayFactory.ChooseTheWay(this.type).Do();
}
}
โค้ดชุดหลังเราซ่อน If ใน Factory แล้วใช้ Polymorphism ในการกำจัด If ออกจากโฟลว์งานหลัก
ดังนั้น ในโลก Object-oriented ทุกๆ If-else จึงสามารถเปลี่ยนเป็น Polyporphism ได้เสมอ
ดังนั้น การที่บอกว่าทุกๆ If-else เป็น Smell แบบนึงในโลก Object-oriented มันไม่ผิด ถูกต้องเลย
กูรูของเมืองนอกหลายคน รวมไปถึงคนที่เป็นไอดอลของผมอย่าง Sandi Metz ก็พูดไว้แบบนี้ว่า Object oriented การมี if-else นับเป็น Smell แบบหนึ่ง
แต่ผมบ่นว่ามันอันตรายและอาจจะ Do more harm than good ไว้นี่ (จะว่าไป ผมบ่นไอดอลตัวเองด้วยซ้ำไปนะเนี่ย)
เพราะอะไรนะ มันจะเกิดอะไรได้เหรอ
ไว้มาต่อภาค 2 ครับ แค่นี้ก่อนละกัน
ปล. 1: ขอบคุณคุณ Weerasak Chongnguluam ที่มาบอกว่ามันมี Polymorphism ตั้งแต่สมัยก่อน ซึ่งผมก็นึกได้ว่าจริง ถ้าจะพูดให้ถูกคือ คอนเซปต์นี้มันถูกโปรโมตช่วงที่ OOP ตั้งไข่ ว่าเป็นหนึ่งในสี่เสาหลัก OOP และเริ่มมาเป็นที่นิยมพูดถึงกันมากในช่วงนั้นครับ
ปล. 2: มีมิตรท่านหนึ่งทักมาว่า การเคลมว่านี่เป็นเนื้อแท้ของ Polymorph อาจจะไม่ถูกนักเพราะ Polymorphism มันมีวิธีมากกว่าการ Implement ด้วย Construct ของ Object-oriented language ล้วนๆ เพิ่มเติมที่นี่้
ซึ่งผมเห็นด้วย เลยขอเปลี่ยนชื่อบล็อกทีหลังเป็น "เนื้อแท้ของการใช้งาน Polymorphism in OOP" และขอบคุณ ณ ที่นี้ที่ทักมา
แต่ผมจะขอขยายความว่าที่ผมใช้คำว่า "เนื้อแท้" ไม่ได้แปลว่ามันคือ "ถูกต้องตามมาตรฐาน" ผมใช้คำว่า "เนื้อแท้" แทนคำว่า "สาเหตุเบื้องลึก" ของการใช้ Polymorphism ในภาษา Object-oriented ซึ่งสำหรับผม เทคนิคก็เรื่องหนึ่ง ก็ว่ากันไปตามภาษาและ Construct ที่มี แต่สาเหตุเบื้องลึกที่ทำให้มันมีประโยชน์มากพอที่ควรจะพูดถึงและควรจะรู้จักไว้ใช้งาน คือการที่มันอนุญาตให้เราอธิบายเรื่องราวในธุรกิจในมุมมองที่หลากหลายได้ครับ
Top comments (0)