OOP Mastery: Toàn bộ kiến thức lập trình Hướng đối tượng cho newbie
Chào mừng bạn đến với chuỗi bài viết kiến thức lập trình Hướng đối tượng (OOP) của DanTech0xFF. Trong chuỗi bài này mình sẽ tập trung truyền đạt kiến thức Lập trình hướng đối tượng một cách gần gũi, dễ hiểu nhất nhằm 1 mục đích duy nhất giúp mọi người hiểu rõ, nắm vững kiến thức và áp dụng tốt cho việc học tập cũng như công việc làm một Lập trình viên trong tương lai.
Nếu bạn đã có kiến thức về OOP rồi, có thể tham khảo chuỗi bài viết về OOP trong Kotlin để học nhanh hơn.
- Part 1: OOP Mastery: Các loại Class, Interface trong Kotlin
- Part 2: OOP Mastery: Encapsulation trong Kotlin
- Part 3: OOP Mastery: Inheritance trong Kotlin
- Part 4: OOP Mastery: Tính đa hình – Polymorphism trong Kotlin
- Part 5: OOP Mastery: Tính Trừu tượng – Abstraction trong Kotlin
Trước khi có OOP người ta lập trình thế nào?
Công việc Lập trình của các Lập trình viên đã trải qua nhiều cuộc cách mạng, nhiều sự thay đổi để có thể đi đến một phương pháp là cơ sở, cốt lõi cho tất cả các phần mềm hiện tại – Phương pháp Lập trình hướng đối tượng. Trong topic đầy thú vị này hãy để tôi giới thiệu cho các bạn các phương pháp lập trình mà tiền nhân đã sử dụng khi chưa có sự xuất hiện của phương pháp lập trình Hướng đối tượng (OOP)
Lập trình hướng Lệnh
Lập trình hướng lệnh được cho là cách lập trình đơn giản, sơ khai nhất của Lập trình.
Trong giai đoạn này một chương trình sẽ gồm Input (dữ liệu đầu vào) – Chuỗi Lệnh xử lý – Output (dữ liệu đầu ra). Các chương trình trong giai đoạn phát triển này hầu hết sẽ là các phần mềm dùng để tính toán các phép toán, các công thức để giúp các nhà khoa học tìm ra đáp án nhanh hơn. Những chương trình này đã giúp cho các ngành khoa học có liên quan đến toán học có bước đột phá lớn vì tiết kiệm được thời gian thực nghiệm, tính toán cho các nhà khoa học (Vật lý, Hóa học, Thiên văn, …)
Một chương trình được viết bằng Lập trình hướng Lệnh có thể được hình dung như sau:
: Chương trình A
- input
- Lệnh 1 xử lý input -> output 1
- Lệnh 2 xử lý output 1 -> output 2
- Lệnh 3 xử lý output 2 -> output 3
- output
/*Trong thực tế chương trình sẽ chứa nhiều logic phức tạp hơn (loop, if - else, ...) */
Rõ ràng có thể thấy với cách tiếp cận này các chương trình trở nên đặc thù cho từng bài toán. Các lệnh code được viết ra không có khả năng tái sử dụng giữa các bài toán với nhau.
Lúc này người ta đã phát minh ra một hương pháp lập trình mới -> Lập trình hướng Hàm
Lập trình hướng Hàm
Trong phương pháp lập trình hướng Hàm, cơ bản các lập trình viên vẫn biên soạn phần mềm dựa trên việc thiết kế các chuỗi lệnh cho những bài toán đặc thù. Tuy nhiên khi tiếp cận với lập trình hướng Hàm lập trình viên sẽ chọn xử lý những vấn đề theo hướng tổng quát, rồi mới đến cụ thể. Việc này sẽ giúp gom nhóm các logic cần tái sử dụng vào các hàm, và chia sẻ chúng giữa nhiều bài toán với nhau.
Cải tiến này giúp giảm thiểu nguồn lực dành cho các công việc lặp đi lặp lại, họ phát minh ra các thư viện giúp cho việc thiết kế và thực thi phần mềm trở nên dễ dàng hơn.
Có thể hình dung đến các thư viện như math.h
– hỗ trợ việc tái sử dụng logic các phép toán cần sử dụng nhiều lần trong quá trình tính toán.
Một chương trình được viết bằng cách lập trình hướng Hàm có thể được mô phỏng như sau
: Chương trình A
include library-1
include library-2
declare self-function-1
declare self-function-2
- input
- library-1$function // mô tả việc sử dụng hàm của library-1 cho 1 mục đích
- library-2$function // mô tả việc sử dụng hàm của library-2 cho 1 mục đích
- self-function-1 // mô tả việc sử dụng self-function-1 cho 1 mục đích
- self-function-2 // mô tả việc sử dụng self-function-2 cho 1 mục đích
- output
/*Trong thực tế chương trình sẽ chứa nhiều logic phức tạp hơn (loop, if - else, ...) */
Ở cách tiếp cận này, chương trình đã trở nên tổng quát hơn, tái sử dụng được nhiều hơn. Chứng tỏ đây là 1 cách tiếp cận tốt hơn so với hướng thủ tục.
Khi xã hội phát triển, nhu cầu xã hội tăng lên – con người sáng tạo nhiều hơn và cũng yêu cầu cao hơn. Phần mềm lúc này không chỉ đơn thuần là các phép tính nữa – lúc này phần mềm trở thành một hệ thống giải pháp cho con người. Việc tổ chức chương trình thông qua các hàm không còn là phù hợp với nhu cầu xã hội nữa.
Việc phát triển phần mềm lúc này cũng cần nhiều người hơn, nhiều công việc được chia ra hơn. Để tối ưu được nguồn lực của các Lập trình viên, họ đã đi đến một cách tiếp cận mới là Lập trình hướng đối tượng – bằng cách cô lập, đối tượng hóa các tính năng trong chương trình để việc phát triển và tái sử dụng code trở nên dễ dàng hơn.
Định nghĩa Lập trình hướng đối tượng
Lập trình Hướng đối tượng (OOP) là phương pháp tiếp cận vấn đề dựa trên khái niệm “Đối tượng”. OOP sẽ mô hình hóa phần mềm thành các thực thể (có thể là hữu hình, hoặc vô hình); các thực thể sẽ luôn có 2 dạng tài nguyên cơ bản là đặc tính – attribute và hành vi – method.
- Đặc tính – attribute: Là các giá trị mà đối tượng đó nắm giữ, có thể cập nhật thay đổi theo thời gian.
Ví dụ:
- Thiết kế phần mềm để vẽ lên màn hình máy tính một cửa sổ với chiều ngang 200px, chiều dọc 80px, viền cửa sổ màu xám, nền màu trắng.
- Trong đặc tả này ta có thể thiết kế đối tượng Window có các thuộc tính để phục vụ cho chương trình
- width: 200px
- height: 80px
- borderColor: GRAY
- backgroundColor: WHITE
- Hành vi – method: Là các hành vi mà đối tượng đó có thể thực hiện. Việc thực hiện các method có thể thay đổi giá trị của các attribute.
- Kế thừa đặc tả phía trên, cửa sổ lúc này có tính năng thay đổi kích thước
- Trong đặc tả này ta có thể thiết kế đối tượng Window có các hành vi phục vụ cho chương trình
- function draw() -> dùng để hiển thị lên màn hình máy tính
- function setSize(width, height) -> dùng để cập nhật giá trị width, height của cửa sổ
Sau khi đã có các đặc tả và giải pháp Hướng đối tượng cho một chương trình ta tiến hành triển khai nó vào mã nguồn.
Để hiểu rõ về cách triển khai mã nguồn Hướng đối tượng bạn cần nắm vững các khái niệm cơ sở của lập trình Hướng đối tượng.
Khái niệm cơ sở trong Hướng đối tượng
Khái niệm Lớp – Class
Lớp – Class trong Lập trình Hướng đối tượng là 1 bản thiết kế khuôn mẫu cho 1 Thực thể. Bạn hãy hình dung Class chính là bản thiết kế của một ngôi nhà, trong đó bao gồm đầy đủ các chỉ sổ: vị trí cửa, màu sắc bàn ghế, chiều cao nha, hướng gió, phong thủy … tất cả gói bọn trong 1 bản thiết kế. Tương tự như vậy, trong lập trình khi ta muốn gom nhóm những đặc tính, hành vi cụ thể nào đó về một thực tể ta cần tạo ra Class.
Ở đây bạn cần hình dung rằng chúng ta không chỉ Object hóa những thực thể hữu hình như Ngôi nhà, con vật, con người mà còn có thể Object hóa những thực thể vô hình. Ví dụ như Logic sao chép dữ liệu từ File A sang File B, Logic truy cập dữ liệu từ Database, Cấu trúc của 1 bài toán phương trình mũ n, …
import java.io.File
class FileCopier {
fun copy(sourcePath: String, destinationPath: String) {
File(sourcePath).inputStream().use { input ->
File(destinationPath).outputStream().use { output ->
input.copyTo(output)
}
}
}
} // Ví dụ về Object hóa một logic Copy File
Khái niệm Đối tượng – Object
Object là sản phẩm của bản thiết kế – Class ở trên đã nhắc đến.
Mỗi Class có thể cho ra nhiều sản phẩm (Object), mỗi Object trong quá trình vận hành chương trình có thể được thay đổi chỉ số, trạng thái và hành vi để mang bên trong những dữ liệu khác nhau cho các mục đích khác nhau.
fun main(args: Array<String>) {
// Create an object of FileCopier class
val fileCopier = FileCopier()
// Define source and destination paths
val source = "source.txt"
val destination = "destination.txt"
// Call the copy function with object reference
fileCopier.copy(source, destination)
println("File copied successfully!")
}
Tính đóng gói – Encapsulation
Tính đóng gói được hiểu là khả năng che giấu các attribute và method đang có bên trong một Class.
Tùy vào ngôn ngữ lập trình mà tính đóng gói được thể hiện mạnh hay yếu. Hầu hết các ngôn ngữ lập trình hỗ trợ 3 mức độ đóng gói private, protected, public.
Level | Internal Use | External Use | Inheritance |
public | YES | YES | YES |
protected | YES | NO | YES |
private | YES | NO | NO |
class BankAccount {
private var balance: Double = 0.0
fun deposit(amount: Double) {
if (amount > 0) {
balance += amount
println("Gửi tiền thành công. Số dư hiện tại: $balance")
} else {
println("Số tiền gửi phải lớn hơn 0.")
}
}
fun withdraw(amount: Double) {
if (amount > 0 && amount <= balance) {
balance -= amount
println("Rút tiền thành công. Số dư hiện tại: $balance")
} else {
println("Số tiền rút không hợp lệ hoặc số dư không đủ.")
}
}
fun getBalance(): Double {
return balance
}
} // Ví dụ về tính đóng gói
/*
- Không thể trực tiếp thay đổi số dư - balance được
- Phải thực thi qua hàm deposit, withdraw để kiểm soát logic
*/
Tính Kế thừa – Inheritance
Kế thừa là khả năng 1 Class sử dụng lại các thuộc tính (properties) và hàm (method, behavior) từ 1 Class khác.
Tùy vào từng ngôn ngữ lập trình mà ta có các loại kế thừa khác nhau, tuy nhiên hầu hết các ngôn ngữ lập trình hiện đại đều sẽ có 3 loại kế thừa chính: public, protected, private
Base Class’s Access Modifier | Public Inheritance’s Access Modifier | Protected Inheritance’s Access Modifier | Private Interitance’s Access Modifier |
private | NO Inheritance | NO Inheritance | NO Inheritance |
protected | private | protected | private |
public | public | protected | private |
Ý nghĩa của các mode kế thừa?
- Đảm bảo quyền truy cập chặt chẽ đến các property / method của đối tượng (Object) phục vụ các nhu cầu riêng trong thiết kế phần mềm, thư viện.
Fun fact: Trong Java, Kotlin không có kế thừa protected hay private. Bạn sẽ được học kỹ hơn trong chuỗi bài viết riêng về OOP trong Kotlin sau nhé!
Tính Đa hình – Polymorphism
Poly: Nhiều
Morhp: Biến hình
Tính đa hình – Polymorphism trong OOP thể hiện qua việc các Class khác nhau, kế thừa chung 1 Class có thể cùng gọi đến 1 hàm (thuật ngữ gọi là cùng giao diện/Interface) nhưng lại có thể thực hiện các Logic khác nhau tùy vào từng cách khai báo.
Hiểu đơn giản là Đa hình – cùng 1 hình thức nhưng nội dung bên trong khác nhau.
Theo sách vở được học thì có 2 loại Đa hình: Overload, Override
- Overload: Định nghĩa các phương thức (hàm) trong Class có cùng tên nhưng khác nhau về các input params
class Calculator {
fun add(a: Int, b: Int): Int {
return a + b
}
fun add(a: Double, b: Double): Double {
return a + b
}
fun add(vararg numbers: Int): Int {
var sum = 0
for (number in numbers) {
sum += number
}
return sum
}
}
- Override: Định nghĩa các phương thức (hàm) trong Class con được kế thừa từ Class cha, và thay đổi hoàn toàn logic bên trong nó.
open class Animal {
open fun makeSound() {
println("Tiếng động")
}
}
class Dog : Animal() {
override fun makeSound() {
println("Gâu gâu")
}
}
class Cat : Animal() {
override fun makeSound() {
println("Meow meow")
}
}
// Ví dụ vê Override trong Kotlin
fun main() {
val animals = listOf(Dog(), Cat())
animals.forEach { animal ->
animal.makeSound()
}
}
open class Shape {
open fun calculateArea(): Double {
return 0.0
}
}
// Ví dụ vê Override trong Kotlin
class Circle(val radius: Double) : Shape() {
override fun calculateArea(): Double {
return Math.PI * radius * radius
}
}
class Rectangle(val width: Double, val height: Double) : Shape() {
override fun calculateArea(): Double {
return width * height
}
}
Tính Trừu tượng – Abstraction
Tính trừu tượng – 1 đặc tính thú vị trong cách tiếp cận của lập trình hướng đối tượng.
Tính trừu tượng được thể hiện thông qua việc khi thiết kế các tính năng của phần mềm các Lập trình viên sẽ tập trung vào việc thiết kế luồng làm việc của các hành vi (behaviour / method) rồi từ đó phân chia ra các Class mà chưa đi sâu vào các chi tiết cụ thể. Các Class được thiết kế ở giai đoạn này gọi là Abstract Class, hoặc cũng có thể gọi là Interface trong 1 vài trường hợp.
Ví dụ: Thiết kế giải pháp cho Bài toán quản lý danh sách học viên của một trung tâm đào tạo Lập trình.
Ở bước đầu tiên chúng ta không thể chọn ngay cơ sở dữ liệu là (SQL, hay MySQL) hoặc thậm chí là Google Sheet và bắt đầu thực hiện việc viết code được. Thay vào đó, ta bắt đầu suy nghĩ luồng làm việc của giải pháp này, các công việc cần được thực hiện của ứng dụng và từ từ phát triển nó.
data class Student(val id: Long)
interface StudentDataAccessLayer {
fun insertStudent(student: Student)
fun deleteStudent(student: Student): Boolean
fun deleteStudent(val ids: List<Long>): Int
fun updateStudent(student: Student): Boolean
} // Công việc này chính là thiết kế Abstract cho phần mềm
Khi đã có Abstract Layer cho các tính năng, việc hiện thực hóa (Implement) sẽ được thảo luận và triển khai sau.
SOLVE THE PROBLEM BEFORE CODE IT!
Giá trị cốt lõi của Lập trình hướng đối tượng
Lập trình Hướng đối tượng được tạo ra với mục đích tăng tính tái sử dụng mã nguồn, giúp việc thế kế phần mềm trở nên trực quan hơn trong quá trình giao tiếp giữa người với người. Hướng đối tượng còn có lợi cho việc mở rộng phần mềm dễ dàng hơn, khiến cho bảo trì phần mềm dễ dàng hơn cho Lập trình viên.