วันพฤหัสบดีที่ 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 ครับ

1 ความคิดเห็น:

  1. This is the basic rule that all one|that every one} skilled roulette players follow. Once you assume you've have} discovered the proper roulette table in your video games - let the roulette wheel spin for a few instances before you place 토토사이트 your first wager. Many online roulette video games, together with most of those discovered on TwinSpires Casino, have an choice to wager on the racetrack.

    ตอบลบ