Lập trình hướng đối tượng là kỹ nghệ quan trọng trong phát triển phần mềm. Bài viết này cố gắng diễn giải thật chính xác bốn trụ cột của lập trình hướng đối tượng để làm cơ sở phát triển cho lập trình viên.
Bao gói
Hiểu lầm thường gặp về tính bao gói là phân vân về hai thái cực:
- Bao gói là đóng gói (pack) cả cấu trúc dữ liệu và hàm vào một mối truy xuất duy nhất.
- Bao gói là giới hạn khả năng truy cập vào thành phần bên trong của một đối tượng.
Quan điểm đúng là tính bao gói bao gồm cả hai khía cạnh trên. Đôi khi ta áp dụng khía cạnh này, đôi lúc là khía cạnh còn lại, và đôi khi cả hai.
Không nhất thiết phải sử dụng những ngôn ngữ tự xưng là lập trình hướng đối tượng thì mới có khả năng triển khai tính bao gói. Lập trình viên C đã thực hiện cả hai khía cạnh trên từ đời (Clean Architect – p53):
/* point.h */
struct Point;
struct Point* makePoint(double x, double y);
double distance (struct Point *p1, struct Point *p2);
/* point.c */
#include "point.h"
#include <stdlib.h>
#include <math.h>
struct Point {
double x,y;
};
struct Point* makepoint(double x, double y) {
struct Point* p = malloc(sizeof(struct Point));
p->x = x;
p->y = y;
return p;
}
double distance(struct Point* p1, struct Point* p2) {
double dx = p1->x - p2->x;
double dy = p1->y - p2->y;
return sqrt(dx*dx+dy*dy);
}
Kế thừa
Trong lập trình hướng đối tượng, kế thừa là lấy một đối tượng hoặc một class làm nền tảng để xây dựng nên một đối tượng hoặc class khác. Phương án thứ nhất được gọi là prototype-based inheritance, và phương án thứ hai được gọi là class-based inheritance.
Cho dù sử dụng phương án nào đi nữa, mục đích vẫn là tạo ra một đối tượng thừa hưởng những thuộc tính và hành vi của đối tượng (của class) cha, qua đó đạt được tính chất is-A (là một đối tượng của class cha, có khả năng đảm nhiệm mọi nhiệm vụ mà các object của class cha có thể thực hiện). Bởi vậy mà đối tượng con sẽ không được thừa hưởng những thứ mà đối tượng (của class) cha không có, chẳng hạn như constructors, overloaded operators, friend funcions…
Kế thừa cũng không phải là một khả năng đặc thù gì của các ngôn ngữ lập trình hướng đối tượng. Clean Architect – p56:
/* namedPoint.h */
struct NamedPoint;
struct NamedPoint* makeNamedPoint(double x, double y, char* name);
void setName(struct NamedPoint* np, char* name);
char* getName(struct NamedPoint* np);
/* namedPoint.c */
#include "namedPoint.h"
#include <stdlib.h>
struct NamedPoint {
double x,y;
char* name;
};
struct NamedPoint* makeNamedPoint(double x, double y, char* name)
{
struct NamedPoint* p = malloc(sizeof(struct NamedPoint));
p->x = x;
p->y = y;
p->name = name;
return p; }
void setName(struct NamedPoint* np, char* name) {
np->name = name;
}
char* getName(struct NamedPoint* np) {
return np->name;
}
/* main.c */
#include "point.h"
#include "namedPoint.h"
#include <stdio.h>
int main(int ac, char** av) {
struct NamedPoint* origin = makeNamedPoint(0.0, 0.0, "origin");
struct NamedPoint* upperRight = makeNamedPoint (1.0, 1.0,
"upperRight");
printf("distance=%f\n",
distance(
(struct Point*) origin,
(struct Point*) upperRight));
}
Ta thấy cấu trúc dữ liệu NamedPoint
đã hoạt động thông qua một tham chiếu của cấu trúc dữ liệu Point
.
Trừu tượng
Đây là khái niệm dễ bị hiểu sai (chẳng hạn lấy mô tả của từ khóa abstraction trong ngôn ngữ Java để giải thích) . Đôi khi là hiểu không tới (chẳng hạn lấy mục đích hay ví dụ về trừu tượng ra để làm định nghĩa).
Để hiểu về khái niệm trừu tượng, chúng ta cần bắt đầu từ khái niệm về chủ thể (study). Chủ thể là đề tài, trong khoa học thì là cho việc nghiên cứu, và trong lập trình thì la cho việc mô hình hóa. Chẳng hạn tài khoản ngân hàng là một chủ thể và đối tượng account nằm trong bộ nhớ của một chương trình đang chạy ở đâu đó chính là mô hình.
Trong lập trình hướng đối tượng, trừu tượng có hai khía cạnh:
- quá trình lược bỏ đi các chi tiết không quan trọng trong chủ thể của các đối tượng của hệ thống phần mềm, nhằm tập trung vào những chi tiết quan trọng hơn với ngữ cảnh.
- kết quả của quá trình trừu tượng là các đối tượng trừu tượng mà mô tả khái niệm về các đối tượng không trừu tượng khác hoặc các chủ thể thực tế.
Ý thứ nhất thường được hiểu nôm na là dấu bớt chi tiết, chẳng hạn chủ thể tài khoản ngân hàng có rất nhiều thông tin nhưng chúng ta sẽ chỉ chọn mô hình hóa những thông tin cốt lõi để nghiệp vụ hoạt động mà thôi.
Ý thứ hai thường được hiểu dễ dãi là mô hình hóa, nhưng lưu ý là kết quả của mô hình hóa trong ngữ cảnh này là những đối tượng trừu tượng. Một tham chiếu của một abstract class trong ngôn ngữ Java chính là một ví dụ về đối tượng trừu tượng – chúng ta có tham chiếu, nhưng không có đối tượng thực tế nào của class đó cả, tham chiếu đó luôn chỉ có thể trỏ tới đối tượng của các concret class (các đối tượng không trừu tượng). Đó là lý do tại sao lại có khái niệm về abstract method và abstract class.
Mục đích cuối cùng của trừu tượng là để giúp (ép) chúng ta (chỉ có khả năng) tập trung vào những thông tin thật sự quan trọng và thật sự hữu dụng trong ngữ cảnh làm việc.
Đa hình
Đa hình bao gồm hai khía cạnh:
- áp đặt điều khoản của một interface tới các thực thể của nhiều kiểu khác nhau.
- một tham chiếu của kiểu này biểu diễn (hiện diện thay cho) một thực thể của kiểu khác.
Đa hình tồn tại ở nhiều tình huống, và theo đó chúng ta có các loại đa hình khác nhau như Subtype polymorphism, Parametric polymorphism, Ad hoc polymorphism, Coercion polymorphism.
Theo Uncle Bob, đa hình là trụ cột cốt lõi của OOP, nó trao cho lập trình viên khả năng đảo ngược phụ thuộc và từ đó đạt được khả năng điều hướng các mối phụ thuộc của chương trình theo bất kỳ chiều nào mà họ muốn (Clean Architecture – p63).
Tham khảo
- https://en.wikipedia.org/wiki/Encapsulation_(computer_programming)
- https://en.wikipedia.org/wiki/Inheritance_(object-oriented_programming)
- https://en.wikipedia.org/wiki/Abstraction_(computer_science)
- https://en.wikipedia.org/wiki/Polymorphism_(computer_science)
- https://www.guru99.com/java-data-abstraction.html
- https://nguyenbinhson.com/2021/06/27/khai-quat-ve-cac-programming-paradisms/