วันพฤหัสบดีที่ 16 กรกฎาคม พ.ศ. 2563

ปัญหาเบสคลาสแตกง่ายเชิงไวยากรณ์ (Syntactic Fragile Base Class Problem)

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

สำหรับปัญหานี้แก้ได้ในระดับตัวภาษา หรือตัวลิงก์เกอร์ (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 ครับ

ไม่มีความคิดเห็น:

แสดงความคิดเห็น