หลังจากได้เข้าใจว่าปัญหาเบสคลาสแตกง่ายคืออะไรจาก
บล็อกที่แล้วแล้ว ในวันนี้ผมจะมานำเสนอตัวอย่างเพื่อให้เห็นภาพของปัญหามากขึ้นครับ แต่ก่อนอื่นมาทวนกันก่อนนะครับ ปัญหานี้คือการแก้ไขคลาสแม่เช่นการย้ายเมท็อดระหว่างคลาสที่อยู่ในครอบครัวเดียวกัน แต่มีผลให้ต้องคอมไพล์คลาสลูกที่ไม่ได้แก้ด้วย
สำหรับปัญหานี้แก้ได้ในระดับตัวภาษา หรือตัวลิงก์เกอร์ (linker) เอง แต่ก็มีภาษาบางภาษาอย่าง C++ ที่ยังไม่ได้แก้ปัญหานี้ แต่ภาษาอย่าง Java ซึ่งใช้ class loader โหลดคลาสขึ้นไปในตอนรัน จะมีการเช็คให้จะไม่มีปัญหานี้ ซึ่งในบล็อกนี้ผมจะแสดงตัวอย่างให้ดูโดยใช้สองภาษานี้ครับ
หมายเหตุ: สำหรับใครที่ต้องการจะคอมไพล์และรันโปรแกรมแต่ไม่อยากพิมพ์โปรแกรมเองสามารถดาวน์โหลดโปรแกรมได้
ที่นี่ครับ
เริ่มจาก C++ ก่อนแล้วกันนะครับ
สมมติมีคลาส foo แบบนี้นะครับ
//foo.hpp
class foo {
private:
int a;
public:
void setA(int val);
int getA();
void f1();
//void f3();
};
//foo.cpp
#include "foo.hpp"
#include <iostream>
using namespace std;
void foo::setA(int val) {
a = val;
}
int foo::getA() {
return a;
}
void foo::f1() {
cout << "I am f1" << endl;
}
/*void foo::f3() {
cout << "I am f3" << endl;
}*/
ให้สังเกตที่ผมคอมเมนต์ไว้นะครับ ตอนนี้คลาส foo ยังไม่มีเมท็อด f3()
ต่อไปมาดูคลาส bar ซึ่งเป็นคลาสลูกของ foo ครับ
//bar.hpp
#include "foo.hpp"
class bar: public foo {
public:
void f2();
void f3();
};
//bar.cpp
#include "bar.hpp"
#include <iostream>
using namespace std;
void bar::f2() {
f1();
cout << "I am f2" << endl;
}
void bar::f3() {
cout << "I am f3" << endl;
}
จะเห็นนะครับว่า เมท็อด f3() ตอนนี้ถูกอิมพลีเมนต์ในคลาส bar
สมมติว่าสองคลาสนี้เราไม่ได้เขียนเอง มีคนส่ง object code มาให้เรา พร้อมทั้ง header file นั่นคือสองคลาสนี้ถูกคอมไพล์ด้วยคำสั่ง
g++ -c foo.cpp
g++ -c bar.cpp
ซึ่งจะทำให้ได้ objce file ชื่อ foo.o และ bar.o ตามลำดับ ดังตัวอย่างด้านล่าง และไฟล์ .o ถูกส่งมาให้เรา พร้อมทั้ง foo.hpp และ bar.hpp
sarunintakosum@Saruns-MacBook-Pro cpp % g++ -c bar.cpp
sarunintakosum@Saruns-MacBook-Pro cpp % g++ -c foo.cpp
sarunintakosum@Saruns-MacBook-Pro cpp % ls *.o
bar.o foo.o
sarunintakosum@Saruns-MacBook-Pro cpp %
ต่อไปสมมติว่าเราเขียนคลาส dar ให้เป็นลูกของคลาส bar ดังนี้ครับ
//dar.hpp
#include "bar.hpp"
class dar: public bar {
public:
void f4();
};
//dar.cpp
#include "dar.hpp"
#include <iostream>
using namespace std;
void dar::f4() {
f3();
cout << "I am f4" << endl;
}
ให้สังเกตว่าเมท็อด f4() เรียกใช้ เมท็อด f3() ซึ่งในเวอร์ชันนี้อยู่ในคลาส bar
และสมมติ main program เป็นดังนี้
#include <iostream>
#include "dar.hpp"
using namespace std;
int main() {
dar *obj = new dar();
obj->f4();
return 0;
}
จากนั้นใช้คำสั่งดังนี้
sarunintakosum@Saruns-MacBook-Pro cpp % g++ -c main.cpp
sarunintakosum@Saruns-MacBook-Pro cpp % g++ -c dar.cpp
sarunintakosum@Saruns-MacBook-Pro cpp % g++ -o main main.o foo.o bar.o dar.o
และรันโปรแกรม
sarunintakosum@Saruns-MacBook-Pro cpp % ./main
I am f3
I am f4
sarunintakosum@Saruns-MacBook-Pro cpp %
จะเห็นว่าคอมไพล์ผ่าน และรันได้ผลลัพธ์ถูกต้อง
สมมติว่ามีการแก้คลาส foo และ bar โดยการเลื่อนเมท็อด f3() จาก bar ขึ้นไป foo ดังนี้
//foo.hpp
class foo {
private:
int a;
public:
void setA(int val);
int getA();
void f1();
void f3();
};
//foo.cpp
#include "foo.hpp"
#include <iostream>
using namespace std;
void foo::setA(int val) {
a = val;
}
int foo::getA() {
return a;
}
void foo::f1() {
cout << "I am f1" << endl;
}
void foo::f3() {
cout << "I am f3" << endl;
}
//bar.hpp
#include "foo.hpp"
class bar: public foo {
public:
void f2();
//void f3();
};
//bar.cpp
#include "bar.hpp"
#include <iostream>
using namespace std;
void bar::f2() {
f1();
cout << "I am f2" << endl;
}
/*void bar::f3() {
cout << "I am f3" << endl;
}*/
จากนั้นทั้งสองคลาสถูกคอมไพล์และ ไฟล์ foo.o และ bar.o และ foo.hpp กับ bar.hpp ตัวใหม่ถูกส่งกลับมาที่เรา คราวนี้สมมติว่าเราเห็นว่าเราไม่ได้แก้โปรแกรม dar.cpp และ main.cpp เลย เราก็เลยใช้คำสั่งต่อไปนี้ลิงก์โปรแกรมเข้าด้วยกัน
sarunintakosum@Saruns-MacBook-Pro cpp % g++ -o main main.o foo.o bar.o dar.o
Undefined symbols for architecture x86_64:
"bar::f3()", referenced from:
dar::f4() in dar.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
sarunintakosum@Saruns-MacBook-Pro cpp %
ซึ่งจะเห็นนะครับว่ามีข้อผิดพลาดเกิดขึ้นจากการลิงก์ เพราะจากข้อมูลเดิมของ dar.o f3() ต้องอยู่ใน bar ซึ่งจริง ๆ แล้วตามแนวคิดของ object-oriented คลาส dar ซึ่งเป็นคลาสลูก ไม่มีความจำเป็นต้องรู้นะครับว่า f3() อยู่ที่ไหนกันแน่จะอยู่ที่คลาสแม่ หรือคลาสยาย :) ก็ควรจะเรียกได้เหมือนกัน ตราบใดที่ยังอยู่ในลำดับชั้นของครอบครัวเดียวกัน แต่จากข้อผิดพลาดนี้ C++ บอกเราว่าเราต้องรู้ครับ
ซึ่งข้อผิดพลาดนี้แก้ได้ด้วยการคอมไพล์คลาส dar แล้วก็ลิงก์ใหม่ครับ
sarunintakosum@Saruns-MacBook-Pro cpp % g++ -c dar.cpp
sarunintakosum@Saruns-MacBook-Pro cpp % g++ -o main main.o foo.o bar.o dar.o
sarunintakosum@Saruns-MacBook-Pro cpp % ./main
I am f3
I am f4
sarunintakosum@Saruns-MacBook-Pro cpp %
จะเห็นว่าไม่มีข้อผิดพลาดใด ๆ และได้โปรแกรมที่รันได้เหมือนเดิม และในตัวอย่างนี้ก็ไม่ได้แก้ยากอะไร หลายคนอาจคิดว่าไม่ใช่ปัญหาใหญ่ แต่คำถามคือทำไมต้องทำด้วย เพราะตามหลักการของ object-oriented แล้ว มันไม่ควรต้องทำ และถ้าเป็นโปรแกรมจริง ๆ ที่มันมีลำดับชั้นของครอบครัวที่ซับซ้อนกว่านี้จะเป็นยังไง และยิ่งไปกว่านั้นสมมติว่าถ้าเป็น .dll เวอร์ชันใหม่แล้วถูกส่งมาติดตั้งในเครื่องที่เราใช้ แล้วเราคอมไพล์อะไรใหม่ไม่ได้เลย อยู่ ๆ โปรแกรมก็ใช้ไม่ได้จะเป็นยังไง
แต่จะเรียกว่าข่าวดีได้หรือเปล่าไม่รู้นะครับ เพราะภาษา object-oriented ที่ยังมีปัญหานี้อยู่เท่าที่รู้ก็คือ C++ เท่านั้น ซึ่งเหตุผลที่ C++ ใช้ในการไม่แก้ก็คือเรื่องของประสิทธิภาพของโปรแกรมครับ คือถ้าใช้วิธีที่ใช้อยู่นี้จะได้โปรแกรมที่ทำงานเร็วกว่าการไปแก้ปัญหานี้ ดังนั้นถ้าใช้ C++ ต้องขยันคอมไพล์ครับ :)
คราวนี้ลองมาดูภาษาที่ไม่มีปัญหานี้กันบ้างนะครับ ตัวอย่างที่จะมาดูกันก็คือภาษา Java ครับ ซึ่งก็ขอใช้โปรแกรมที่ทำงานเหมือนกับ C++ นะครับ เริ่ม จากคลาส Foo และ Bar
public class Foo {
int a;
public void setA(int val) {
a = val;
}
public int getA() {
return a;
}
void f1() {
System.out.println("I am f1");
}
/*void f3() {
System.out.println("I am f3");
}*/
}
public class Bar extends Foo {
public void f2() {
System.out.println("I am f2");
}
public void f3() {
System.out.println("I am f3");
}
}
ซึ่งในเวอร์ชันแรกนี้ให้สังเกตว่าเมท็อด f3() อยู่ในคลาส Bar นะครับ และก็สมมติเหมือนเดิมว่าสองคลาสนี้เราไม่ได้เขียนเองแต่มีคนคอมไพล์แล้วส่งไฟล์ .class มาให้เรา
sarunintakosum@Saruns-MacBook-Pro java % javac Foo.java
sarunintakosum@Saruns-MacBook-Pro java % javac Bar.java
sarunintakosum@Saruns-MacBook-Pro java % ls *.class
Bar.class Foo.class
sarunintakosum@Saruns-MacBook-Pro java %
และเราก็เขียนคลาส Dar ขึ้นมาดังนี้ครับ
public class Dar extends Bar {
public void f4() {
f3();
System.out.println("I am f4");
}
}
ก็เหมือนใน เวอร์ชัน C++ นะครับ คือเป็นลูกของ Bar มีเมท็อด f4() ซึ่งเรียกใช้เมท็อด f3() ส่วน Main โปรแกรมก็เป็นดังนี้ครับ
public class Main {
public static void main(String[] args) {
Dar obj = new Dar();
obj.f4();
}
}
จากนั้นเราก็คอมไพล์ Main และ Dar และรันโปรแกรมครับ
sarunintakosum@Saruns-MacBook-Pro java % javac Dar.java
sarunintakosum@Saruns-MacBook-Pro java % javac Main.java
sarunintakosum@Saruns-MacBook-Pro java % java Main
I am f3
I am f4
sarunintakosum@Saruns-MacBook-Pro java %
ต่อไปก็จะสมมติเหมือนเดิมนะครับว่ามีการย้ายเมท็อด f3() จาก Bar ไป Foo และมีการส่ง .class ตัวใหม่มาให้เรา
public class Foo {
int a;
public void setA(int val) {
a = val;
}
public int getA() {
return a;
}
void f1() {
System.out.println("I am f1");
}
void f3() {
System.out.println("I am f3");
}
}
public class Bar extends Foo {
public void f2() {
System.out.println("I am f2");
}
/*public void f3() {
System.out.println("I am f3");
}*/
}
sarunintakosum@Saruns-MacBook-Pro java % javac Foo.java
sarunintakosum@Saruns-MacBook-Pro java % javac Bar.java
sarunintakosum@Saruns-MacBook-Pro java %
แต่เมื่อได้ .class ใหม่มาแล้ว เนื่องจากไม่มีการแก้ไขใด ๆ ใน Dar และ Main ดังนั้นผมก็จะรันโปรแกรม Main เลยนะครับ
sarunintakosum@Saruns-MacBook-Pro java % java Main
I am f3
I am f4
sarunintakosum@Saruns-MacBook-Pro java %
ซึ่งก็จะเห็นว่าโปรแกรมทำงานได้ถูกต้อง ก็แสดงว่า Java ไม่มีปัญหานี้นะครับ
เมื่ออ่านมาถึงตรงนี้ ก็หวังว่าจะได้เข้าใจ Syntactic Fragile Base Class Problem กันแล้วนะครับ โดยสรุปปัญหานี้เป็นปัญหาระดับตัวภาษา ซึ่งภาษาที่ยังมีปัญหานี้ก็เช่น C++
ในบล็อกต่อไป จะเขียนถึงปัญหา FBC ที่ไม่ได้ขึ้นกับภาษาโปรแกรม แต่เป็นปัญหาในด้านการออกแบบของแนวคิด object-oriented เอง นั่นคือปัญหา Semantic Fragile Base Class Problem ครับ