Java Bài 32: Tính Trừu Tượng (Abstraction)

Posted by

Chào mừng các bạn đã đến với bài học Java số 32, bài học về tính trừu tượng (abstraction). Đây là bài học trong chuỗi bài về lập trình ngôn ngữ Java của Yellow Code Books.

Với việc đi qua bài học hôm nay thì chúng ta cũng sẽ nói đủ bốn đặc tính cốt lõi trong lập trình hướng đối tượng. Bốn đặc tính đó là:

– Tính Gói ghém dữ liệu, hay còn gọi là Encapsulation. Bạn có thể xem lại tính chất này ở các bài: khả năng truy cập, getter/setter.
– Tính Kế thừa, hay còn gọi là Inheritance. Bạn có thể xem lại tính chất này ở các bài: kế thừaoverridingoverloading.
– Tính Đa hình, hay còn gọi là Polymorphism. Bạn có thể xem lại tính chất này ở bài về polymorphism.
– Và hôm nay. Tính Trừu tượng, hay còn gọi là Abstraction.

Nghe qua thì thấy rất trừu tượng. Liệu kiến thức này có khó hiểu không và nó giúp ích gì cho việc tổ chức code trong OOP? Chúng ta cùng đi vào bài học luôn nhé.

Tính Trừu Tượng (Abstraction) Là Gì?

Trừu tượng trong thực tế còn có thể hiểu là cái gì đó không có thực. Vậy tính Trừu tượng trong OOP ý muốn nói đến một lớp nào đó mang một đặc tính trừu tượng, không có thực. Và như vậy bạn có thể hiểu là lớp Trừu tượng sẽ là lớp không tồn tại đúng không nào?

Thực ra thì lớp Trừu tượng vẫn có tồn tại, vẫn là một lớp thôi. Nhưng nó trừu tượng ở chỗ, nó không thể được dùng để tạo ra các đối tượng như những lớp bình thường khác. Lớp Trừu tượng khi này chỉ là cái “xác không hồn”, hay bạn có thể hiểu nó chỉ là một cái sườn, để mà bạn có thể tạo ra các lớp con của nó dựa vào sự ràng buộc từ cái sườn này.

Nghe đến đây đủ thấy trừu tượng rồi ha. Chúng ta cùng xem khai báo một lớp là Trừu tượng là như thế nào, và các đặc điểm của một lớp Trừu tượng sẽ ra sao với mục kế tiếp.

Khai Báo Lớp Trừu Tượng Như Thế Nào?

Sau đây là cú pháp để bạn có thể khai báo một lớp Trừu tượng.

abstract  class  tên_lớp {
	các_thuộc_tính;
	các_phương_thức;
}

Bạn có thể thấy, để khai báo một lớp là Trừu tượng, chỉ cần thêm vào trước từ khóa class một từ khóa abstract mà thôi.

Nhưng như vậy thì mục đích của sự Trừu tượng này là gì, chúng ta cùng đi đến mục sau đây.

Tại Sao Phải Trừu Tượng?

Lý do tiên quyết mà bạn phải biết đến Trừu tượng là vì có trên 90% khả năng bạn đi xin việc, người phỏng vấn sẽ hỏi bạn câu này: “em hãy giúp phân biệt giữa lớp abstract và interface”!!! Mình đùa thôi! Nhưng họ hỏi thật đấy!

Thực ra lý do để khai báo một lớp Trừu tượng là vì trong lớp có ít nhất một phương thức Trừu tượng. Có thể nói ngược lại cho dễ hiểu như sau, nếu mà một lớp có ít nhất một phương thức được định nghĩa là Trừu tượng, thì lớp đó phải khai báo Trừu tượng. Đến đây bạn sẽ có rất nhiều thắc mắc, mình xin giải đáp từ từ.

Phương thức Trừu tượng là gì? Chính là phương thức có định nghĩa từ khóa abstract khi khai báo. Cú pháp khai báo một phương thức Trừu tượng cũng giống như khai báo một lớp Trừu tượng thôi.

[kả_năng_truy_cập]  abstract  kiểu_trả_về  tên_phương_thức  ([ các_tham_số_truyền_vào"]);

Phương thức Trừu tượng có tác dụng gì? Phương thức có khai báo abstract sẽ buộc phải không có thân hàm. Như bạn xem cú pháp ở trên, sẽ không có thông tin về khối lệnh của phương thức dạng này. Bạn có thể so sánh với cú pháp của phương thức bình thường ở đây. Bạn chỉ có thể khai báo tên, các tham số truyền vào (nếu có), kiểu trả về, và rồi kết thúc khai báo bằng (;) cho các phương thức Trừu tượng. Có thể thấy rõ sự ràng buộc của tính Trừu tượng bắt đầu ở ngay đây. Sở dĩ các phương thức Trừu tượng không có thân hàm, vì chúng không cần phải làm vậy. Một khi có một lớp nào đó kế thừa từ lớp Trừu tượng này, thì lớp kế thừa đó buộc phải hiện thực (tiếng Anh gọi là implement) nội dung cho tất cả các phương thức Trừu tượng này, bằng cách override lại chúng.

Đến đây bạn có thể thấy lớp Trừu tượng thực sự không quá cao siêu. Nó chỉ là một lớp không có thể hiện (không thể khởi tạo đối tượng từ nó). Nhưng nó lại ràng buộc các lớp con của nó buộc phải hiện thực nội dung cho các phương thức Trừu tượng bên trong nó. Thế thôi.

Bạn có thể bắt gặp tính Trừu tượng này ở đâu đó khi sử dụng các gói thư viện từ hệ thống (thỉnh thoảng có nơi gọi là Java platform) hoặc từ các nhà phân phối khác. Khi bạn kế thừa các lớp của họ, bỗng dưng hệ thống báo lỗi và bắt bạn phải override ngay phương thức nào đó của lớp đó, thì chắc chắn bạn sẽ hiểu ngay lớp đó chính là lớp Trừu tượng. Một lát nữa chúng ta cùng khảo sát xem trong hệ thống thì lớp nào là lớp Trừu tượng nhé, sau khi đi qua bài thực hành bên dưới.

Thực Hành Xây Dựng Ứng Dụng Tính Lương Cho Nhân Viên

Chúng ta cùng quay lại bài toán tính lương cho nhân viên, mà ở bài học số 30 chúng ta đã làm cho nó trọn vẹn. Theo lý thì bạn không cần phải bổ sung gì cho bài thực hành này. Phần này chỉ nhằm mục đích cho bạn thấy một lớp Trừu tượng sẽ được khai báo và sử dụng như thế nào thôi. Bạn không nhất thiết phải áp dụng Trừu tượng một cách máy móc như bài thực hành.

Mô Tả Lại Yêu Cầu Chương Trình

Yêu cầu của chương trình tính lương không hề thay đổi so với bài trước. Mình chỉ mô tả lại thôi.

– Công ty có hai loại nhân viên: nhân viên toàn thời gian và nhân viên thời vụ.
– Nhân viên toàn thời gian là lính sẽ hưởng lương 10 củ một tháng. Nhân viên toàn thời gian là sếp sẽ hưởng lương 20 củ một tháng.
– Nhân viên toàn thời gian nếu làm thêm ngày nào thì sẽ được cộng thêm 800k mỗi ngày, bất kể chức vụ.
– Nhân viên thời vụ cứ làm mỗi giờ được 100k, không phân biệt chức vụ gì cả. Làm nhiều thì hưởng nhiều.

Ứng dụng sẽ cho phép người dùng nhập vào số lượng nhân viên. Sau đó với từng nhân viên, người dùng phải nhập vào tên nhân viên, loại nhân viên toàn thời gian hay bán thời gian, nhân viên toàn thời gian thì là nhân viên lính hay nhân viên sếp, có làm thêm ngày nào không, nhân viên thời vụ thì làm được mấy giờ. Cuối cùng dựa vào các thông tin đó, sẽ xuất ra màn hình lương tương ứng cho tất cả nhân viên.

Nâng Cấp Ứng Dụng Bằng Việc Xây Dựng Lớp NhanVien Là Trừu Tượng

Đây là hình ảnh code của lớp NhanVien ở bài thực hành hôm đó.

Tính trừu tượng - Lớp NhanVien khi chưa có áp dụng trừu tượng

Bạn xem, ở hai phương thức loaiNhanVien()tinhLuong() được mình khoanh tròn, có phải bạn rất cần các lớp con buộc phải override hai phương thức này? Với cách code như thế này thì tính ràng buộc không có, và lỡ như ai đó thực hiện việc code các lớp con của NhanVien, liệu họ có nhớ mà override hai phương thức này hay không?

Vậy yêu cầu của bài hôm nay rõ ràng rồi nhé, chúng ta sẽ nâng cấp cho ứng dụng tính lương này bằng việc khai báo NhanVien là lớp Trừu tượng.

Sơ Đồ Lớp

Chúng ta vẫn dựa vào sơ đồ lớp của bài trước. Nhưng các phương thức loaiNhanVien()tinhLuong() sẽ là các phương thức Trừu tượng. Và dĩ nhiên lớp NhanVien cũng phải là lớp Trừu tượng nốt. Trong sơ đồ lớp, các khai báo Trừu tượng sẽ được in nghiêng (mình còn in đậm ra cho bạn dễ thấy).

Tính trừu tượng - Sơ đồ lớp

Xây Dựng Các Lớp

Lớp Configs không hề thay đổi.

package util;

public class Configs {

	// Loại nhân viên
	public static final int NHAN_VIEN_SEP = 1;
	public static final int NHAN_VIEN_LINH = 2;
	
	// Lương nhân viên
	public static final long LUONG_NHAN_VIEN_FULL_TIME_SEP = 20000000; // Lương tháng của sếp
	public static final long LUONG_NHAN_VIEN_FULL_TIME_LINH = 10000000; // Lương tháng của lình
	public static final long LUONG_LAM_THEM_MOI_NGAY = 800000; // Làm thêm mỗi ngày của nhân viên toàn thời gian được 800 k
	public static final long LUONG_NHAN_VIEN_PART_TIME_MOI_GIO = 100000; // Lương nhân viên thời vụ mỗi giờ 100 k
}

Lớp NhanVien khi này là lớp Trừu tượng. Và đây là code cho lớp NhanVien, bạn có thể so sánh với hình ảnh code trên kia của lớp này để xem sự thay đổi.

package model;

public abstract class NhanVien {

	protected String ten;
    protected long luong;
     
    public NhanVien() { 
    }
     
    public NhanVien(String ten) {
        this.ten = ten;
    }
     
    // Lớp con phải override để lo vụ loại nhân viên này
    protected abstract String loaiNhanVien();
     
    // Lớp con phải override để lo vụ tính lương này
    public abstract void tinhLuong();
     
    public void xuatThongTin() {
        System.out.println("===== Nhân viên: " + ten + " =====");
        System.out.println("- Loại nhân viên: " + loaiNhanVien());
        System.out.println("- Lương: " + luong + " VND");
    }
}

Các lớp con của NhanVien sẽ có một chút ràng buộc, mình sẽ đi chi tiết từng bước xây dựng lớp NhanVienFullTime để bạn xem. Khi bạn vừa mới khai báo lớp này kế thừa từ NhanVien, bạn sẽ thấy hệ thống báo lỗi bằng một icon hình bóng đèn bên cạnh dấu chéo màu đỏ (mình có nói đến tính năng báo lỗi dạng này của Eclipse ở bài này, bạn tham khảo nhé).Tính trừu tượng - Kế thừa từ lớp trừu tượng NhanVien

Để khắc phục lỗi này rất dễ, bạn chỉ cần override các phương thức Trừu tượng từ lớp NhanVien. Nếu bạn không biết có bao nhiêu phương thức Trừu tượng cần phải override, thì cứ đưa trỏ chuột vào icon bóng đèn đó, bạn sẽ thấy nó liệt kê tất cả các phương thức bạn cần.

Tính trừu tượng - Các gợi ý chính là các phương thức trừu tượng

Hoặc click chuột hẳn vào cái bóng đèn, bạn sẽ thấy gợi ý. Khi này hãy chọn Add unimplemented methods.

Tính trừu tượng - Chọn lựa để implement các phương thức trừu tượng

Sau khi chọn tùy chọn trên, hệ thống sẽ tạo ra tất cả các phương thức override lại các phương thức Trừu tượng từ NhanVien. Và khi này hệ thống cũng không còn báo lỗi nữa. Nếu NhanVienFullTime kế thừa từ NhanVien, mà không hiện thực hết tất cả các phương thức Trừu tượng từ NhanVien, thì bạn sẽ không bao giờ có thể thực thi ứng dụng được.

Tính trừu tượng - Code tự động implement các phương thức trừu tượng

Và đây là code cuối cùng của lớp NhanVienFullTime.

package model;

import util.Configs;

/**
 * NhanVienFullTime chính là nhân viên toàn thời gian
 */
public class NhanVienFullTime extends NhanVien {
	
	private int ngayLamThem; // Ngày làm thêm của nhân viên
	private int loaiChucVu; // Chức vụ là lính hay sếp
	
	public NhanVienFullTime(String ten, int ngayLamThem, int loaiChucVu) {
		super(ten);
		this.ngayLamThem = ngayLamThem;
		this.loaiChucVu = loaiChucVu;
	}
	
	@Override
	public String loaiNhanVien() {
		if (loaiChucVu == Configs.NHAN_VIEN_LINH) {
			return "Lính toàn thời gian" + (ngayLamThem > 0 ? " (có làm thêm ngày)":"");
		} else {
			return "Sếp toàn thời gian" + (ngayLamThem > 0 ? " (có làm thêm ngày)":"");
		}
	}
	
	@Override
	public void tinhLuong() {
		if (loaiChucVu == Configs.NHAN_VIEN_LINH) {
			luong = Configs.LUONG_NHAN_VIEN_FULL_TIME_LINH + ngayLamThem * Configs.LUONG_LAM_THEM_MOI_NGAY;
		} else if (loaiChucVu == Configs.NHAN_VIEN_SEP) {
			luong = Configs.LUONG_NHAN_VIEN_FULL_TIME_SEP + ngayLamThem * Configs.LUONG_LAM_THEM_MOI_NGAY;
		}
	}
}

Code của NhanVienPartTime.

package model;

import util.Configs;

/**
 * NhanVienPartTime chính là nhân viên thời vụ
 */
public class NhanVienPartTime extends NhanVien {
	
	private int gioLamViec; // Tổng số giờ làm việc của nhân viên
	
	public NhanVienPartTime(String ten, int gioLamViec) {
		this.ten = ten;
		this.gioLamViec = gioLamViec;
	}
	
	@Override
	public String loaiNhanVien() {
		return "Nhân viên thời vụ";
	}
	
	@Override
	public void tinhLuong() {
		luong = Configs.LUONG_NHAN_VIEN_PART_TIME_MOI_GIO * gioLamViec;
	}
}

Code ở phương thức main() vẫn sẽ như bài 30 hôm trước mà thôi.

package main;

import java.util.Scanner;

import model.NhanVien;
import model.NhanVienFullTime;
import model.NhanVienPartTime;

public class MainClass {

	public static void main(String[] args) {
		// Kêu người dùng nhập vào số lượng nhân viên trong công ty
		Scanner scanner = new Scanner(System.in);
		System.out.print("Hãy nhập số lượng nhân viên: ");
		int tongNhanVien = Integer.parseInt(scanner.nextLine());
		
		// Khai báo mảng các nhân viên
		NhanVien[] mangNhanVien = new NhanVien[tongNhanVien];
		for (int i = 0; i < tongNhanVien; i++) {
			// Khai báo từng loại nhân viên, và kêu người dùng nhập thông tin nhân viên
			System.out.print("Tên nhân viên " + (i + 1) + ": ");
			String ten = scanner.nextLine();
			System.out.print("Là nhân viên (1-Toàn thời gian; 2-Bán thời gian): ");
			int laNhanVien = Integer.parseInt(scanner.nextLine());
			if (laNhanVien == 1) {
				// Nhân viên toàn thời gian
				System.out.print("Chức vụ nhân viên (1-Sếp; 2-Lính): ");
				int chucVu = Integer.parseInt(scanner.nextLine());
				System.out.print("Ngày làm thêm (nếu có): ");
				int ngayLamThem = Integer.parseInt(scanner.nextLine());
				mangNhanVien[i] = new NhanVienFullTime(ten, ngayLamThem, chucVu);
			} else {
				System.out.print("Giờ làm: ");
				int gioLamViec = Integer.parseInt(scanner.nextLine());
				mangNhanVien[i] = new NhanVienPartTime(ten, gioLamViec);
			}
		}
		
		System.out.println("\nKết quả tính lương\n");
		
		// Tính lương và xuất thông tin nhân viên
		for (NhanVien nhanVien : mangNhanVien) {
			nhanVien.tinhLuong();
			nhanVien.xuatThongTin();
		}
	}

}

Qua bài thực hành này thì bạn đã hiểu được mục đính và cách thức ràng buộc của một lớp Trừu tượng rồi đúng không nào. Mục cuối cùng này mình sẽ dẫn chứng cho bạn xem một số lớp Trừu tượng được xây dựng sẵn trong thư viện của Java, để bạn có một sự hiểu biết rộng hơn về kiến thức này.

Trải Nghiệm Một Vài Lớp Trừu Tượng Của Java Platform

Việc trải nghiệm này chỉ để kiểm chứng một số lớp Trừu tượng có sẵn từ hệ thống. Chúng ta không hoàn toàn lúc nào cũng phải kế thừa từ các lớp này, trừ khi bạn muốn override một số phương thức của nó để mở rộng hơn khả năng của lớp gốc, người ta gọi hành động này là custom lại một đối tượng. Bạn sẽ bắt gặp nhiều hơn đến việc custom cho các lớp Trừu tượng của hệ thống này khi xây dựng ứng dụng Android.

Trải nghiệm đầu tiên. Nếu bạn thử xây dựng một lớp, rồi kế thừa từ lớp Graphics của hệ thống, bạn sẽ nhận được sự “mời gọi” phải hiện thực hàng tá phương thức Trừu tượng từ lớp này.

Tính trừu tượng - Kế thừa từ lớp trừu tượng Graphics

Điều này tương tự nếu bạn kế thừa từ lớp Number.

Tính trừu tượng - Kế thừa từ lớp trừu tượng Number

Hoặc với lớp DateFormat và với nhiều lớp khác trong Java platform.

Tính trừu tượng - Kế thừa từ lớp trừu tượng DateFormat

Vậy thôi, đủ để bạn thấy là Java platform đã xây dựng sẵn rất nhiều lớp Trừu tượng. Sau này nếu có gặp phải bất kỳ một đòi hỏi phải implement các phương thức khi bạn kế thừa một lớp lạ hoắc nào đó, thì bạn đã biết lớp đó chính là lớp Trừu tượng. Với việc khảo sát một số lớp Trừu tượng từ hệ thống như vậy thì chúng ta cũng đã kết thúc bài học hôm nay.

Cảm ơn bạn đã đọc các bài viết của Yellow Code Books. Bạn hãy ủng hộ blog bằng cách:

Đánh giá 5 sao bên dưới mỗi bài nếu thấy thích.
Comment bên dưới mỗi bài nếu có thắc mắc.
Để lại địa chỉ email của bạn ở thanh bên phải để nhận được thông báo sớm nhất khi có bài viết mới.
Chia sẻ các bài viết của Yellow Code Books đến nhiều người khác.

Bài Kế Tiếp

Chúng ta sẽ nói đến một anh em gần gũi với sự Trừu tượng, người ta thường mang hai anh em ra so sánh với nhau, đó là kiến thức về Interface.

Advertisements
Rating: 5.0/5. From 9 votes.
Please wait...

4 comments

Gửi phản hồi