Khi nói đến các tính chất của lập trình hướng đối tượng. Anh em lập trình có thể liệt kê ra cả bốn đặc tính ngay lập tức. Này là tính đóng gói, tính kế thừa, tính đa hình và tính trừu tượng. So với một số anh em, lý thuyết phần này khá khó hiểu và khó nhớ. Ở phần 2 của series “Hướng đối tượng bỏ túi”. Mình sẽ hệ thống lại tri thức về bốn đặc tính trọng yếu khi lập trình hướng đối tượng.
Đặt vấn đề
Để nội dung của nội dung liền mạch hơn, mình sẽ đặt vấn đề trước. Mọi tính chất của hướng đối tượng sẽ được mình lồng ghép và xây đắp từ vấn đề này.
Tiếp nối phần 1 của series. Sau khi thiết kế ra chiếc xe chỉ chạy được trên … NetBeans. Mình đã được sếp thăng chức, chuyển sang làm nhân viên vườn thú. Với người thông minh và tài giỏi như mình, việc này dễ như tìm đường vào tim crush vậy. Suốt ngày chỉ có cho ăn, đếm thú, tối đến lại lùa vào chuồng. Dù thời gian bận không nhiều nhưng mình vẫn rãnh để mô phỏng sở thú này trên máy tính. Vậy mình nên thiết kế như nào anh em nhỉ?
Encapsulation – Tính bao đóng
Tính bao đóng là gì?
Encapsulation means that a group of related properties, methods, and other members are treated as a single unit or object.
Tính chất trước hết tất cả chúng ta cần nhớ chính là tính bao đóng (đóng gói). Tính bao đóng yêu cầu thực hiện gom những thứ liên quan lại thành một nhà cung cấp hoặc đối tượng, Class. Đồng thời ẩn những dữ liệu nhạy cảm khỏi khả năng truy xuất của người dùng.
Tính bao đóng có thể đạt được bằng cách sử dụng Access Modifier
và Getter - Setter
. (xem lại khái niệm ở phần 1)
Phân tích và vận dụng tính bao đóng
Tính chất này cũng dễ hiểu thôi, tất cả chúng ta sẽ khởi đầu với con Vịt nhé. Vậy là giờ tất cả chúng ta cần gom mấy thứ có liên quan lại thành một class, đặt tên là Duck. Để xem nào, nó là loài là vịt nè, phải có tên riêng nữa. Nó có thể ăn, bơi rồi kêu cak cak cak. ? Nếu như vậy thì tên loài sẽ không cho phép bên ngoài thay đổi, còn tên riêng con vịt có thể đặt sao cũng được. Sau này là khái niệm các phương thức đại diện cho hành động ăn, bơi, kêu của con vịt.
Example 01: Khái niệm class Duck vận dụng tính đóng gói
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class
Duck
{
// Constructor để tạo con vịt
public
Duck
(
string
name
)
{
Name
=
name
;
}
// Tên loài, thông tin này dùng nội bộ
private
string
Species
{
get
;
set
;
}
=
“Duck”
;
// Đặt tên cho con vịt
// Chỉ đọc và đặt tên 1 lần lúc tạo
public
string
Name
{
get
;
private
set
;
}
// Các hành động của con vịt
public
void
Eat
(
)
{
Console
.
WriteLine
(
$
“{Species} name: {Name} is Eating!”
)
;
}
public
void
Swim
(
)
{
Console
.
WriteLine
(
$
“{Species} name: {Name} is Swimming!”
)
;
}
public
void
Sound
(
)
{
Console
.
WriteLine
(
$
“{Name} – Mot con Vit xoe ra hai cai canh, no keu rang cak cak cak cak cak cak.”
)
;
}
}
Như vậy là tất cả chúng ta đã khái niệm được con vịt trông như vậy nào rồi. Việc còn sót lại là viết hàm main và chạy thử thôi. Dùng thử từ ngữ Java anh em cho mình xin khất. Ai cần hãy comment trong nội dung, mình sẽ bổ sung sau.
Inheritance – Tính kế thừa
Tính kế thừa cần được hiểu như vậy nào?
Inheritance describes the ability to create new classes based on an existing class.
Đây là tính chất dễ hiểu nhất của hướng đối tượng. Nó cho phép tất cả chúng ta khái niệm một class mới dựa trên một class đã khái niệm trước đó. Nói cách khác là class con kế thừa lại những đặc tính đã có của class cha.
Tính kế thừa có ba cấp độ:
- Base class inheritance: Kế thừa toàn thể từ lớp nền tảng.
- Abstract class inheritance: Kế thừa từ lớp abstract. Gọi là kế thừa một phần vì phải khái niệm lại những hàm abstract.
- Interface inheritance: Kế thừa khuôn mẫu, phải khái niệm rất cả những gì interface yêu cầu.
Tuy nhiên, để đảm bảo tính kế thừa, ta cần lưu ý khá nhiều thứ:
- Lớp kế thừa được sử dụng những thành phần được cho phép của lớp nền tảng quy định bở Access Modifier (public, protected, v.v…).
- Nói cho chuẩn: Kế thừa class gọi là
extends
và kế thừa interface gọi làimplements
. Java sử dụng 2 từ khóa đó để thực hiện kế thừa. So với C#, chỉ cần dùng dấu:
cho cả class và interface. - Từ khóa
this
: Đại diện cho lớp chứa cục code hiện tại. base class
đại diện cho lớp nền tảng, lớp cha. Từ ngữ Java sử dụng phương thứcsuper()
, C# sử dụngbase
. So với C++ là Google.com.- Kế thừa 1 cấp & kế thừa nhiều cấp.
- Lớp kế thừa có thể ép kiểu về lớp nền tảng mà không làm mất tính đúng đắn của chương trình.
- Hầu như các từ ngữ không cho phép đa kế thừa class. Tránh việc 2 class kế thừa có cùng các thuộc tính giống nhau, nhưng có thể đã implement khác nhau.
- Có thể đa kế thừa Interface.
Phân tích và xây dựng sở thú dựa trên tính kế thừa
Nếu như tất cả chúng ta xây dựng sở thú chỉ dựa vào tính bao đóng như ở ví dụ 1. Thì nghĩa là cứ 10 loài ta phải tạo 10 class, sau đó khái niệm lại thuộc tính, hành động một lần nữa. Nếu đặc điểm giống nhau giữa chúng có điểm cần thay đổi lại phải sửa thủ công 10 class khác nhau. Rất là mất thời gian, công sức và hơi khổ râm. Này là nguyên nhân tất cả chúng ta phải vận dụng tính kế thừa.
Lúc này, tất cả chúng ta cần gom những đặc tính chung của động vật vào một lớp gọi là Animal. Sau đó muốn tạo ra loài mới, ta chỉ cần kế thừa từ lớp động vật là nó đã có những đặc tính của động vật rồi. Chương trình của tất cả chúng ta cần nâng cấp như sau:
Example 02: Khái niệm lớp nền tảng Animal để dùng chung
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Lớp Động vật
class
Animal
{
private
string
Species
{
get
;
set
;
}
// Loài
public
string
Name
{
get
;
private
set
;
}
// Tên
public
Animal
(
string
species
,
string
name
)
{
this
.
Species
=
species
;
this
.
Name
=
name
;
}
// Lấy thông tin loài vật
public
string
GetInfo
(
)
=
>
$
“{Species} name: {Name}”
;
// Hành động chung của các loài vật
public
void
Eat
(
)
{
Console
.
WriteLine
(
$
“{GetInfo()} is Eating!”
)
;
}
public
void
Sound
(
)
{
Console
.
WriteLine
(
$
“{GetInfo()} is Chirped”
)
;
}
}
Lúc này, class Duck cần kế thừa hay nói đúng hơn là mở rộng (extends) class Animal. Và nó được tinh gọn như sau:
Example 03: Class Duck kế thừa từ class Animal
1
2
3
4
5
6
7
8
9
10
11
12
13
// Loài Vịt
class
Duck
:
Animal
{
// Gọi lại constructor của lớp cha bằng hàm base
public
Duck
(
string
name
)
:
base
(
“Duck”
,
name
)
{
/* Some code here */
}
// Hành động riêng của con Vịt
public
void
Swim
(
)
{
Console
.
WriteLine
(
$
“{GetInfo()} is Swimming!”
)
;
}
}
Khi cần khái niệm một loài mới, ta chỉ cần tạo class mới, kế thừa class Animal, sau đó thêm các thuộc tính của riêng loài mới là xong. Tất cả chúng ta sẽ khái niệm thêm loài khỉ cho sở thú.
Example 04: Khái niệm loài khi từ class Animal
1
2
3
4
5
6
7
8
9
10
11
12
13
// Loài Khỉ
class
Monkey
:
Animal
{
// Gọi lại constructor của lớp cha bằng hàm base
public
Monkey
(
string
name
)
:
base
(
“Monkey”
,
name
)
{
/* Some code here */
}
// Hành động riêng của con Khỉ
public
void
Climb
(
)
{
Console
.
WriteLine
(
$
“{GetInfo()} is Climbing!”
)
;
}
}
Sau khi khái niệm 3 class trên, anh em tạo hàm main để chạy thử và xem kết quả:
Example 05: Main method
1
2
3
4
5
6
7
static
void
Main
(
string
[
]
args
)
{
var
duck
=
new
Duck
(
“Donal”
)
;
// Tạo con vịt Donal
var
monkey
=
new
Monkey
(
“Wukong”
)
;
// Tạo con khỉ wukong
duck
.
Eat
(
)
;
// Cho vịt ăn
monkey
.
Eat
(
)
;
// Cho khỉ ăn
}
Vậy là tất cả chúng ta đã vận dụng tính kế thừa cho sở thú rồi đấy. Bạn thử phán đoán kết quả in ra màn hình như vậy nào nhé. ?
Khái niệm tính đa hình
Polymorphism means that you can have multiple classes that can be used interchangeably, even though each class implements the same properties or methods in different ways.
Ngay trong từ đa hình nó đã mang ý nghĩa là một thứ gì đó mang nhiều hình thái khác nhau. Trong lập trình hướng đối tượng. Nếu tính kế thừa cho phép ta thừa hưởng một phương thức
từ một class. Thì tính đa hình cho phép ta triển khai lại phương thức đó theo những cách khác nhau. Việc này kéo theo việc các class kế thừa từ chung một class cha có thể được sử dụng thay thế cho nhau mà không làm tác động đến tính đúng đắn của chương trình.
Tính đa hình có thể đạt được bằng cách sử dụng:
Method Overloading – Nạp chồng phương thức
Method Overloading cho phép triển khai cùng một tính năng với nhiều loại tham số khác nhau. Nạp chồng được gọi là
compiletime polymorphism
.
Ví dụ phương thức cho vịt ăn ngoài việc cho ăn mặc định, đôi lúc ta cần cho cho biết thêm loại thức ăn, số lượng, thời gian, v.v… .
Phương thức nạp chồng phải cùng tên và cùng kiểu trả về và thỏa mãn một trong các điều kiện sau:
- Khác số lượng tham số truyền vào (parameters).
- Khác kiểu dữ liệu của các tham số truyền vào.
- Khác thứ tự của tham số truyền vào.
Method Overriding – Ghi đè phương thức
Method Overriding là một phương pháp cho phép lớp kế thừa tái khái niệm một phương thức đã khái niệm ở lớp cha. Ghi đè được gọi là
runtime polymorphism
.
Ghi đè phương thức phải thỏa mãn ba điều kiện sau:
- Phải có quan hệ kế thừa giữa hai class.
- Cùng tên và cùng kiểu trả về (hoặc sub-type).
- Cùng các tham số truyền vào.
Thực tiễn, ghi đè phương thức của Java và C# có nhiều điểm khác nhau. Ngày xưa còn tay mơ mình rất hay nhầm lẫn mấy vấn đề này.
Java Method Overriding
So với Java, chỉ cần khái niệm phương thức ghi đè trong class con thì nó được ngầm hiểu là phương thức ghi đè. Từ khóa @Override
có dùng hay không cũng được, nhưng anh em nên dùng để lúc đọc code đỡ lú.
Ví dụ: Ghi đè phương thức Sound()
của con Vịt.
Example 06: Main.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package
blog
.
hieuda
.
com
;
class
Animal
{
private
String
species
;
private
String
name
;
public
Animal
(
String
species
,
String
name
)
{
this
.
species
=
species
;
this
.
name
=
name
;
}
public
String
getName
(
)
{
return
name
;
}
private
void
setName
(
String
name
)
{
this
.
name
=
name
;
}
public
String
getInfo
(
)
{
return
species
+
” name: “
+
name
;
}
public
void
eat
(
)
{
System
.
out
.
println
(
getInfo
(
)
+
” is eating”
)
;
}
// Overloading
public
void
eat
(
String
food
)
{
System
.
out
.
println
(
getInfo
(
)
+
” is eating “
+
food
)
;
}
public
void
sound
(
)
{
System
.
out
.
println
(
getInfo
(
)
+
” is Chirped”
)
;
}
}
class
Duck
extends
Animal
{
// Gọi lại constructor của animal
public
Duck
(
String
name
)
{
super
(
“Duck”
,
name
)
;
}
// Ghi đè hàm sound, ko cần sử dụng annotation vẫn được
// @Override
public
void
sound
(
)
{
System
.
out
.
println
(
getInfo
(
)
+
” – Mot con vit xoe ra hai cai canh. No keu rang: cắk cắk cắk cằk cằk cằk.”
)
;
}
// Hành động riêng của con Vịt
public
void
swim
(
)
{
System
.
out
.
println
(
getInfo
(
)
+
” is swimming.”
)
;
}
}
public
class
Main
{
public
static
void
main
(
String
[
]
args
)
{
Animal
a
=
new
Duck
(
“Donal Trung”
)
;
a
.
Sound
(
)
;
// Output:
// Duck name: Donal Trung – Mot con vit xoe ra hai cai canh. No keu rang: cắk cắk cắk cằk cằk cằk.
}
}
C# Method Overriding
So với C#, khi ghi đè phương thức, ta bắt buộc phải sử dụng từ khóa virtual
cho phương thức của lớp cha. Và sử dụng từ khóa override
khi khái niệm phương thức ghi đè. Nếu không, compiler sẽ quăng cho bạn một cái warning bảo là this method bị hide gì đó. Khi đó, ta cần dùng từ khóa new
để khái niệm phương thức này là Method Hiding. Đây không phải là bug, đây là cơ chế sẽ được đề cập ở nội dung Method Hidding.
Ví dụ C# Method Overriding mình đính kèm vào code trừu tượng bên dưới luôn nha.
Trước khi khởi đầu, cần thẳng thắn với nhau rằng “Không có tính trừu trượng trong OOP, chỉ có Data Abstraction”. Các nội dung trên mạng đang ra rả rằng Hướng đối tượng có bốn tính chất. Như vậy là không đúng bản chất. Trong các thời kỳ phát triển software, Data Abstraction nằm trong thời kỳ thiết kế. Còn OOP nằm ở thời kỳ triển khai. Do đó, nó phải giải quyết được các yêu cầu nghiệp vụ, là tầng trung gian kết nối business logic với các thiết kế software. Data Abstraction là mục tiêu mà lập trình hướng đến. OOP sử dụng các object, class, interface, và ba tính chất đóng gói, kế thừa, đa hình cũng để đạt đến trạng thái Abstraction. Đó cũng là nguyên nhân mình giới thiệu Data Abstraction sau khi đã nói về những thứ khác của OOP.
Nghe có vẻ trừu tượng, nhưng nghĩ lại thì rất trừu tượng ?
Data Abstraction means hiding the unnecessary details from type consumers.
Trừu tượng hóa dữ liệu nghĩa là che giấu những thành phần không cần thiết khỏi người dùng. Các bạn tránh nhầm lần với việc “ẩn những dữ liệu nhạy cảm khỏi khả năng truy xuất của người dùng” của tính đóng gói. Điều này cho phép người dùng có thể triển khai những logic phức tạp dựa trên một lớp trừu tượng có sẵn mà không cần quan tâm bên trong thực sự làm gì.
Trừu tượng có thể đạt được bằng cách sử dụng:
- Abstract class
- Interface
Phân tích và trừu tượng hóa sở thú
Ở ba phần trên, tất cả chúng ta đã lần lượt xây dựng những thứ nền tảng nhất. Sở thú đã có thể đi vào hoạt động bình thường. Lần này, ta sẽ nâng cấp bằng cách trừu tượng hóa chúng. Làm sao khi sở thú thuê nhân viên mới, họ vẫn có thể cho thú ăn, ngủ, chạy mà không cần quan tâm rõ ràng công việc đó phải làm gì.
Interface – Chia tách các tính năng thành các Interface
Mindset của interface là quy định một số tính năng cho đối tượng. Nói đơn giản là interface báo cho người dùng biết class đang được sử dụng có thể làm gì. Thực tiễn, các interface có sẵn trong Java thường được đặt kiểu: Runnable
, Enumerable
,… đại diện cho khả năng mà class đó có thể phục vụ. Ở nội dung này, tất cả chúng ta sẽ tạo 3 interface là IAnimal
, IRunnable
, ISwimmable
đại diện cho loài vật và khả năng di chuyển của chúng.
Example 07: Interface
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface
IAnimal
{
void
Eat
(
)
;
void
Eat
(
string
food
)
;
void
Sound
(
)
;
}
interface
IRunnable
{
void
Run
(
)
;
}
interface
ISwimmable
{
void
Swim
(
)
;
}
Abstract class – Implement một số tính năng
Trong thực tiễn, động vật chỉ là một khái niệm, không phải là loài vật rõ ràng nên được gọi là trừu tượng. Tất cả chúng ta khởi đầu xây dựng lớp trừu tượng Animal. Trong số đó có các property và implement sẵn một vài tính năng. Lúc này tất cả chúng ta không thể tạo object kiểu Animal ani = new Animal();
được mà nó sẽ được dùng để đại diện cho các object khác kế thừa từ nó.
Ví dụ: Animal ani = new Duck("Donal")
hoặc Animal ani = new Monkey("Wukong");
Hoặc vjp pro hơn: IListvàlt;IAnimalvàgt; zoo = new Listvàlt;IAnimalvàgt;();
Sau đó zoo.Add(new Duck("Donal"));
Example 08: Animal abstract class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
abstract
class
Animal
:
IAnimal
{
private
string
Species
{
get
;
set
;
}
public
string
Name
{
get
;
private
set
;
}
public
Animal
(
string
species
,
string
name
)
{
this
.Species
=
species
;
this
.Name
=
name
;
}
//
L
ấ
y
th
ô
ng
tin
con
v
ậ
t
public
string
GetInfo
(
)
=
>
$
“{Species} name: {Name}”
;
//
H
à
nh
độ
ng
ă
n
c
ó
th
ể
ghi
đè
ho
ặ
c
kh
ô
ng
public
virtual
void
Eat
(
)
{
Console
.WriteLine
(
$
“{GetInfo()} is eating!”
)
;
}
public
virtual
void
Eat
(
string
food
)
{
Console
.WriteLine
(
$
“{GetInfo()} is eating {food}!”
)
;
}
//
C
á
c
l
ớ
p
k
ế
th
ừ
a
ph
ả
i
implement
ti
ế
ng
k
ê
u
public
abstract
void
Sound
(
)
;
}
Tạo lớp Duck và Monkey
Example 09: Implement class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class
Duck
:
Animal
,
ISwimmable
{
// Constructor để tạo con Vịt
// Gọi lại constructor của lớp cha bằng hàm base
public
Duck
(
string
name
)
:
base
(
“Duck”
,
name
)
{
/* Some code here */
}
// Ghi đè hành động ăn
public
override
void
Eat
(
)
{
Console
.
WriteLine
(
“Override eating from Duck.Eat()”
)
;
base
.
Eat
(
)
;
}
public
override
void
Eat
(
string
food
)
{
Console
.
WriteLine
(
“Override eating from Duck.Eat(food)”
)
;
base
.
Eat
(
food
)
;
}
// Triển khai abstract method
public
override
void
Sound
(
)
=
>
Console
.
WriteLine
(
$
“{GetInfo()} – Mot con vit xoe ra hai cai canh. No keu rang: cắk cắk cắk cằk cằk cằk.”
)
;
// Khả năng bơi của con Vịt từ ISwimmable
public
void
Swim
(
)
=
>
Console
.
WriteLine
(
$
“{GetInfo()} is Swimming!”
)
;
}
class
Monkey
:
Animal
,
IRunnable
{
public
Monkey
(
string
name
)
:
base
(
“Monkey”
,
name
)
{
/* Some code here */
}
public
override
void
Sound
(
)
=
>
Console
.
WriteLine
(
$
“{GetInfo()} – Con khỉ kêu éc éc éc”
)
;
public
void
Run
(
)
=
>
System
.
Console
.
WriteLine
(
$
“{GetInfo()} is running.”
)
;
}
Viết hàm Main để tạo sở thú
Tất cả chúng ta sẽ tạo một danh sách các loài vật và cho chúng ăn mà không cần quan tâm chúng là con gì.
Example 10: Cho thú ăn
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static
void
Main
(
string
[
]
args
)
{
IList
<
IAnimal
>
zoo
=
new
List
<
IAnimal
>
(
)
;
zoo
.
Add
(
new
Duck
(
“Donal”
)
)
;
zoo
.
Add
(
new
Duck
(
“Trung”
)
)
;
zoo
.
Add
(
new
Monkey
(
“Son”
)
)
;
zoo
.
Add
(
new
Monkey
(
“Wukong”
)
)
;
foreach
(
IAnimal
animal
in
zoo
)
{
animal
.
Sound
(
)
;
animal
.
Eat
(
“Special food”
)
;
}
}
Và đây là kết quả khi chạy hàm main trên. Kết quả có giống những gì bạn phán đoán không? Nếu không, trả lời thắc mắc vì sao nhé!
Cũng chính vì thế, mình không đặt tiêu đề nội dung là “Bốn tính chất của hướng đối tượng” mà đổi thành “Bốn tính chất cần lưu tâm khi học…”.
Tổng kết
Đây là nội dung thứ 2 của series Hướng đối tượng bỏ túi. Và cũng là trọng yếu nhất trong ba bài. Ở phần này, tất cả chúng ta đã nói về bốn tính chất của lập trình hường đối tượng. Có thể các bạn không để ý, mình sắp xếp theo thứ tự tính đóng gói, kế thừa, đa hình và trừu tượng là vì tính chất về sau yêu cầu nắm tính chất trước mới ứng dụng được.
Series hướng đối tượng bỏ túi – OOP in basic:
Nội dung nặng lý thuyết quá phải không anh em ? Để tổng hợp và viết bài này mình cũng mất mấy ngày lận. Cũng không thể tránh khỏi sai sót. Nếu anh em phát hiện nơi đâu không ổn hoặc giải thích khó hiểu. Hãy comment bên dưới để mình chỉnh sửa lại thích hợp hơn nhen. Cuối cùng, mình đính kèm bức ảnh tổng quan của 3 nội dung về hướng đối tượng.
Refs
Difference between Compile-time and Run-time Polymorphism in Java | Method Hiding in C# | Method Overriding in Java | C# | Method Overriding | Difference between Method Overriding and Method Hiding in C# | Abstraction is not a principle of Object-Oriented Programming