12/08/2018, 13:35

Những dấu chân của nhân loại trên con đường đến với lập trình hướng đối tượng (phần 2)

Ẩn dữ liệu (hiding data) và đóng gói thông tin (encapsulation) Kể cả đối với object composition của ngôn ngữ C, bằng việc tách biệt việc define và implement của header file chúng ta vẫn có thể ẩn những thông tin bên trong kiểu dữ liệu trừu tượng. Tuy nhiên, sau này người ta đã dần dần hỗ ...

progLanguages.jpg

Ẩn dữ liệu (hiding data) và đóng gói thông tin (encapsulation)

Kể cả đối với object composition của ngôn ngữ C, bằng việc tách biệt việc define và implement của header file chúng ta vẫn có thể ẩn những thông tin bên trong kiểu dữ liệu trừu tượng.

Tuy nhiên, sau này người ta đã dần dần hỗ trợ việc tường minh hóa quyền truy cập của những gì bên ngoài, không phải dưới dạng thủ thuật như vậy mà là dưới dạng chức năng chính thức của ngôn ngữ.

Encapsulation hay Black box là những khái niệm rộng hơn khái niệm ẩn dữ liệu. Nhờ việc thực hiện những khái niệm này mà ta tránh được hiện tượng bad coupling đã nói ở phần trên.

Những modifier của Java (public, static, final…) hay C## cũng sinh ra nhằm phục vụ cho mục đích tương tự đó. Những ngôn ngữ không có access modifier như Perl hay JavaScript thì private và public không được phân biệt rõ ràng nên ta phải phân biệt chúng bằng cách tạo ra kiểu dữ liệu giống nhau, sử dụng dùng dấu gạch dưới (ví dụ : _privateMethod).

Dù là bằng phương pháp nào thì lối tư duy cũng là : không để tầng kiến trúc có dữ liệu đã được trừu tượng hóa tiếp xúc với tầng kiến trúc nguyên thủy có dữ liệu chưa được trừu tượng hóa.

Với lối tư duy đó, phong cách lập trình hiện đại – định nghĩa những yêu cầu phức tạp thành từng lớp trừu tượng, đã được hình thành.

Lập trình hướng đối tượng?

Cuối cùng, chúng ta đã có thể bắt đầu nói về chủ đề chính : lập trình hướng đối tượng.

Ngôn ngữ lập trình hướng đối tượng đầu tiên, mặc dù hồi đó nó chưa được gọi với cái tên này, ra đời vào thập niên 1960. Đó là ngôn ngữ Simula.

Ngôn ngữ này vốn được sinh ra chỉ nhằm mục đích phục vụ cho việc thiết kế simulation nhưng dần dần nó đã trở thành thông dụng.

Trong Simula đã bao gồm cả object, class (kiểu dữ liệu trừu tượng), dispatch động (dynamic dispatch), sự kế thừa, trình gom rác (garbage collection). Gọi là thông dụng nhưng thực ra hồi đó chưa có nhiều việc cần sử dụng Simula lắm, tuy nhiên, những khái niệm ưu việt của nó thì vẫn còn tồn tại cho đến ngày nay.

Việc những khái niệm tuy cũ nhưng ưu việt được người thời sau đánh giá lại và tái sinh dưới dạng một ngôn ngữ lập trình mới, sau đó phổ biến rộng rãi là việc rất thường xảy ra đối với ngành Khoa học máy tính.

Một ví dụ nữa cho luận điểm trên là ngôn ngữ lisp – thứ được đánh giá là ngôn ngữ đa hình (multi-paradigm language) mềm dẻo nhất của thời đại này, đã được người ta đưa ra ý tưởng ban đầu từ những năm 1950. Phải công nhận rằng sức sáng tạo của con người thật là tuyệt vời.

Đến đây thì mọi chuyện đã bắt đầu trở nên phức tạp hơn.

Người ta đã mở rộng ngôn ngữ C thành 2 loại, dựa trên những khái niệm ưu việt của Simula. Một loại là C++, một loại là Objective-C.

Ngôn ngữ C là một ngôn ngữ rất thực dụng nên việc người ta đem những khái niệm ưu việt đưa vào ngôn ngữ này, nhằm mục đích thử những đầu ra khác nhau là một việc hoàn toàn đương nhiên.

Dựa trên Simula, một ngôn ngữ, hay nói đúng hơn là một môi trường mới đã xuất hiện như một quả bom tấn. Đó là Smalltalk.

Smalltalk đã bổ sung thêm khái niệm “messaging” vào những khái niệm sẵn có của Simula, tái cấu trúc lại và trở thành ngôn ngữ “thuần túy hướng đối tượng” với tất cả những xử lí đều viết dưới dạng message. Cụm từ “lập trình hướng đối tượng” đã được sinh ra từ đó.

Alan Kay, người phát minh ra cụm từ đó sau này đã nói rằng việc đặt ra cái tên “lập trình hướng đối tượng” là một thất bại. Lí do là bởi vì cái tên đó coi nhẹ tính quan trọng của khái niệm messaging.

Nhưng nói gì thì nói, sau đó người ta đã dựa trên chính Smalltalk để phát triển ngôn ngữ C thành ngôn ngữ Objective C.

Sự hướng đối tượng của Simula và C++

Bjarne Stroustrup, tác giả của C++, đã chỉnh sửa khái niệm “hướng đối tượng” thành :

“1 siêu tập hợp của kiểu dữ liệu trừu tượng mang trong mình tính kế thừa và tính đa trạng thái”. Trong C++, method được gọi là “member function”. Việc này sinh ra do ảnh hưởng của tên gọi “member procedure” trong Simula. Còn “method” mà chúng ta vẫn thường hay nói là cách gọi sinh ra bởi Smalltalk.

Những gì sau này đều có liên quan đến những gì trước đó.

Cơ chế chỉ gọi những xử lí mà ta muốn gọi

“Kiểu dữ liệu trừu trượng mang trong mình tính kế thừa và tính đa trạng thái” là gì? Nếu chỉ nói như vậy thì thật khó hiểu.

Trong đó, đặc biệt khó hiểu là khái niệm đa trạng thái. Tôi sẽ dùng đoạn code trong lập trình hướng đối tượng dưới đây để giải thích.

string = number.StringValue
string = date.StringValue

Bằng cách này, chúng ta có thể gọi từng hàm số khác nhau và điều này được gọi là polymorphism (cấu trúc đa hình).

Nếu chỉ với cách làm như vậy, chúng ta hoàn toàn có thể gọi nhầm sang xử lí khác vì signature là khác nhau. Chúng ta hãy cùng nhìn đoạn code sau xem có gì khác?

string = stringValue(number) //Thực tế là gọi NumberToString
string = stringValue(date)   //Thực tế là gọi DateToString

Có thể thấy làm thế này sẽ dễ hiểu hơn một chút. Tùy vào tham số mà hàm số được gọi sẽ thay đổi. Hàm số dạng này được gọi là hàm số polymorphic (hàm khả biến).

Đến đây, nếu ai đặt ra câu hỏi : “Đây chính là phương thức Chồng hàm (function overloading) chứ gì?” thì người đó rất sắc sảo.

https://en.wikipedia.org/wiki/Function_overloading

Đa trạng thái được hiểu theo nhiều cách, nhưng nhìn chung nó tương tự với việc : tùy theo tham số mà hàm số biến đổi. Tuy nhiên, ở trường hợp sau thì câu chuyện lại khác.

function toString(IStringValue sv) string {
    return StringValue(sv)
}

IstringValue là interface thể hiện một object có mang trong mình hàm StringValue. Khi ta trả về giá trị trong khi đang dùng phương thức Chồng hàm, máy tính sẽ không hiểu phải sử dụng hàm nào trong số đó. Lí do bởi vì khi ta compile, phương thức Chồng hàm sẽ tự động gọi ra tất cả các hàm số đang có gán kiểu dữ liệu.

stringValue(number:Number) => StringValue-Number(number)
stringValue(date :Date)  => StringValue-Date(date)
function toString(IStringValue sv) string {
    return StringValue(sv) => StringValue-IStringValue (không tồn tại!)
}

Đối với trường hợp đó, ta có thể xử lí như ý muốn bằng việc viết code sử dụng cấu trúc đa hình, cho dù ta vẫn đang dùng interface.

function StringValue(v:IstringValue){
    switch(v.class){ //Object tự biết bản chất của mình là gì
    case Number: return StringValue-Number(number)
    case Date   : return StringValue-Date(date)
    }
}

Đây là phương thức ép dữ liệu phải nhớ rằng mình cần gọi hàm số nà và khi compile dữ liệu sẽ tự tìm và gọi hàm số đó. Phương thức này gọi là phân phối động hay dispatch động (dynamic dispatch).

Việc chúng ta sử dụng Đa trạng thái thông qua dispatch động như trên có ý nghĩa gì? Ý nghĩa của việc đó nằm ở sự tái sử dụng và phân tách code thông qua interface.

Ta có thể tái sử dụng code cả trong trường hợp ta muốn tạo một object giống với object đã thỏa mãn một interface nào đó. Bằng cách này, ta vừa tránh được hiện tượng bad cohesion đã nói ở trên, vừa viết được những đoạn code có tính tái sử dụng rất cao.

Khi lập trình hướng đối tượng bắt đầu xuất hiện, cụm từ “tái sử dụng” dần trở nên phổ biến, chúng ta có thể giải nghĩa nó như sau : tính tái sử dụng cao nghĩa là code có tính chất thông dụng và phụ thuộc vào interface, tránh được bad coupling, viết test dễ dàng và thích ứng mạnh với việc thay đổi yêu cầu.

Dispatch động

Điểm cốt lõi của dispatch động là ở việc khiến object tự hiểu mình có vai trò gì, mặt khác, khiến object khi được chạy sẽ tự tìm kiếm xem nên chạy hàm số nào trong table hàm số.

Ở ngôn ngữ Simula hay C++ ta có thể dùng reserved word là virtual để khai báo việc phân phối động các hàm số ảo.

/*
Vtable for B1
B1::_ZTV2B1: 3u entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI2B1)
16    B1::f1

Class B1
   size=16 align=8
   base size=16 base align=8
B1 (0x7ff8afb7ad90) 0
    vptr=((& B1::_ZTV2B1) + 16u)
 */
class B1 {
public:
    void f0(){}
    virtual void f1(){}
    char before_b0_char;
    int member_b1;
};
/*
Class B0
   size=4 align=4
   base size=4 base align=4
B0 (0x7ff8afb7e1c0) 0
 */
class B0{
private:
    void f(){};
    int member_b1;
};

Ta có thể đưa pointer trỏ đến vtable (virtual function table) vào trong chính data như trên và truy dấu theo nó để giải quyết.

Ngược lại nếu ta không khai báo bằng virtual, ta có thể gọi hàm mà không tốn công phát sinh (công do việc trỏ đến hàm số ảo).

Trong C++ hay C#, do chủ trương hạn chế phát sinh công của dispatch động trừ những trường hợp thật sự cần thiết (zero overhead policy), việc khai báo bằng virtual cần phải rất tường minh.

Trên objective-C ta cũng có thể tránh công phát sinh này bằng cách get trực tiếp pointer của hàm số.

objectivce-c.m
SEL selector = @selector(f0);
IMP p_func = [obj methodForSelector : selector ];
//lưu sẵn p_func nhằm mục đích lặp vân vân
   :
pfunc(obj , selector);   //Dùng pfunc có thể làm giảm công phát sinh
//Không cần thiết trừ lúc những quan trọng

Nếu ta thể hiện displatch động bằng một đoạn code có tính năng tương tự thì đoạn code đó có thể viết như sau. Thể hiện dispatch động bằng code diễn giải:

var PERSON_TABLE = {
    "getName" : function(self){return self.name},
};

var object = {
    _vt_ : PERSON_TABLE, // làm cho object hiểu vai trò của mình
    name : "daichi hiroki"
};
// _Gọi method 1 cách động_
function methodCall(object,methodName){
    // Ràng buộc object như một tham số thứ Nhất
    return object._vt_[methodName](object)
}
methodCall(object,"getName");

Đến đây, chúng ta đã hiểu Đa trạng thái có thể được thể hiện bằng 3 yếu tố.

  • Chức năng khiến dữ liệu tự hiểu mình có vai trò gì.
  • Chức năng tự tìm kiếm vai trò đó khi method được gọi.
  • Chức năng ràng buộc object với tham số để tự tham chiếu. Ngôn ngữ xuất hiện sau này - Perl5 đã có tính hướng đối tượng và như ví dụ dưới đây, các yếu tố trên được thể hiện một cách rất rõ ràng.
package Person;

sub new {
    my($class,$ref) = @_;
    # Hàm số bless nối referrence và package
    # $class thể hiện package Person
    return bless( $object, $ref );
}
sub get_name{
    my ($self) = @_;
    $self->{name};
}
//Tìm method một cách động & ràng buộc với tham số thứ nhất => toán tử allow
my $person = Person->new({ name => "daichi hiroki"});
$person->get_name;

Trong ví dụ trên, hàm bless đã chỉ cho reference biết rằng “module cần sử dụng để tìm kiếm hàm số nằm ở đây”. (bless có nghĩa là chúc phúc, có thể hình dung như một vị thần che chở, hoặc bảo hộ cho package).

Ngoài ra với việc sử dụng toán tử “->”, việc tự động gọi và tìm kiếm đã được thực hiện.

Trong giai đoạn người ta có xu hướng muốn thêm chức năng có tính hướng đối tượng, cách tiếp cận của Perl5 – thể hiện tính Đa trạng thái chỉ bằng 2 chức năng thực sự là rất hiếm có.

(Xem tiếp phần 3)

0