Lớp sealed
Thuộc phần Lập trình hướng đối tượng trong hành trình C# của Coddy. Bài học 22 trên 70.
Đôi khi bạn muốn dừng chuỗi kế thừa. Từ khóa sealed ngăn lớp bị kế thừa, khóa chặt phần triển khai để không lớp nào khác có thể mở rộng lớp đó.
public sealed class FinalDog : Animal
{
public override void Speak()
{
Console.WriteLine("Woof!");
}
}
// Điều này sẽ gây ra lỗi biên dịch:
// public class SuperDog : FinalDog { }Vì sao bạn muốn niêm phong một lớp (class)? Bảo mật và tính toàn vẹn trong thiết kế là những lý do phổ biến. Khi bạn đã thiết kế cẩn thận một lớp và không muốn ai thay đổi hành vi của nó thông qua tính kế thừa (inheritance), việc niêm phong lớp sẽ đảm bảo cách triển khai của bạn vẫn nguyên vẹn.
Bạn cũng có thể niêm phong từng phương thức thay vì cả lớp. Cách này ngăn việc ghi đè thêm, đồng thời vẫn cho phép kế thừa chính lớp đó:
public class Dog : Animal
{
public sealed override void Speak()
{
Console.WriteLine("Woof!");
}
}
public class Puppy : Dog
{
// Không thể ghi đè Speak() vì phương thức này đã được khai báo sealed
// Nhưng có thể thêm các phương thức mới
public void Whimper()
{
Console.WriteLine("Whimper...");
}
}Lưu ý rằng sealed trên một phương thức phải đi cùng với override. Bạn chỉ có thể niêm phong một phương thức vốn đã là phương thức ảo trong lớp cha. Điều này giúp bạn kiểm soát chính xác: cho phép tùy chỉnh ở một số cấp trong hệ thống kế thừa, rồi khóa lại tại một điểm cụ thể.
Thử thách
DễHãy xây dựng một hệ thống xử lý thanh toán để minh họa cách dùng sealed nhằm khóa các phần triển khai quan trọng. Bạn sẽ tạo một hệ thống phân cấp trong đó có thể mở rộng một số phương thức thanh toán, nhưng các phương thức an toàn nhất sẽ được niêm phong để ngăn mọi sửa đổi.
Bạn sẽ tạo bốn tệp để sắp xếp code:
PaymentMethod.cs: Định nghĩa lớpPaymentMethodtrong namespacePayments. Lớp cơ sở này có thuộc tínhName(chuỗi) và một hàm khởi tạo để thiết lập thuộc tính đó. Thêm một phương thứcvirtualcó tênProcessPayment(decimal amount), trả về"Processing ${amount} via {Name}".CreditCardPayment.cs: Định nghĩa lớpCreditCardPaymenttrong namespacePayments, kế thừa từPaymentMethod. Thêm thuộc tínhCardNumber(chuỗi). Hàm khởi tạo nhận tên và số thẻ, dùngbaseđể truyền tên. Ghi đèProcessPaymentbằng từ khóasealedđể các lớp dẫn xuất sau này không thể thay đổi cách hoạt động này. Phương thức sẽ trả về"Charging ${amount} to card ending in {last 4 digits of CardNumber}".BankTransfer.cs: Định nghĩa một lớpsealedcó tênBankTransfertrong namespacePayments, kế thừa từPaymentMethod. Toàn bộ lớp này được niêm phong, không ai có thể kế thừa từ nó. Thêm thuộc tínhBankName(chuỗi). Hàm khởi tạo nhận tên và tên ngân hàng. Ghi đèProcessPaymentđể trả về"Transferring ${amount} from {BankName}".Program.cs: Trong tệp chính, tạo các instance của cả ba kiểu thanh toán bằng các giá trị đầu vào. Lưu chúng dưới dạng tham chiếuPaymentMethodvà gọiProcessPaymenttrên từng đối tượng để minh họa tính đa hình với các phần triển khai được niêm phong.
Bạn sẽ nhận bốn đầu vào:
- Số thẻ tín dụng (16 chữ số)
- Tên ngân hàng
- Số tiền thanh toán
In ra kết quả xử lý khoản thanh toán bằng từng phương thức:
{base PaymentMethod result}
{CreditCardPayment result}
{BankTransfer result}Ví dụ, nếu đầu vào là 1234567890123456, First National và 150.00, đầu ra sẽ là:
Processing $150.00 via Generic
Charging $150.00 to card ending in 3456
Transferring $150.00 from First NationalDùng "Generic" làm tên cho instance PaymentMethod cơ sở, "Credit Card" cho khoản thanh toán bằng thẻ tín dụng và "Bank Transfer" cho khoản chuyển khoản ngân hàng.
Phương thức được niêm phong trong CreditCardPayment đảm bảo rằng nếu ai đó tạo lớp con, họ không thể thay đổi cách xử lý các khoản thanh toán bằng thẻ tín dụng. Lớp được niêm phong BankTransfer ngăn chặn hoàn toàn việc kế thừa. Cả hai đều là những mẫu bảo mật quan trọng trong hệ thống thanh toán!
Tự làm thử
using System;
using Payments;
class Program
{
public static void Main(string[] args)
{
// Đọc các giá trị đầu vào
string cardNumber = Console.ReadLine();
string bankName = Console.ReadLine();
decimal amount = Convert.ToDecimal(Console.ReadLine());
// TODO: Tạo một instance PaymentMethod cơ sở có tên "Generic"
// Lưu instance đó dưới dạng tham chiếu PaymentMethod
// TODO: Tạo một instance CreditCardPayment có tên "Credit Card" và số thẻ
// Lưu instance đó dưới dạng tham chiếu PaymentMethod
// TODO: Tạo một instance BankTransfer có tên "Bank Transfer" và tên ngân hàng
// Lưu instance đó dưới dạng tham chiếu PaymentMethod
// TODO: Gọi ProcessPayment trên từng instance và in ra kết quả
// In mỗi kết quả trên một dòng riêng
}
}
Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.
Tất cả bài học trong Lập trình hướng đối tượng
1Các khái niệm cơ bản về lập trình hướng đối tượng
Tệp bên ngoàiKhông gian tên và chỉ thịGiới thiệu về lớp và đối tượngTừ khóa thisPhương thức và tham sốTrường và thuộc tínhHàm khởi tạoKhởi tạo đối tượngÔn tập: Máy tính đơn giản4Kế thừa
Cú pháp kế thừa cơ bản (:)Từ khóa 'base'Từ khóa virtual và overrideLớp sealedLớp cơ sở 'object'Ôn tập: hệ phân cấp nhân viên7Tính năng nâng cao
Nạp chồng toán tửBộ lập chỉ mục (this[])Ghi đè ToString()Phương thức mở rộngÔn tập: Danh sách tùy chỉnh10Các mẫu thiết kế phần 1
Giới thiệu về mẫu thiết kếSingleton an toàn với luồngMẫu FactoryMẫu Observer (sự kiện)Mẫu Strategy2Thuộc tính và thành viên tĩnh
Thuộc tính tự độngThuộc tính chỉ đọc hoặc chỉ ghiTrường và phương thức tĩnhLớp tĩnhThành viên có thân biểu thức5Đa hình và interface
Đa hình lúc biên dịch và lúc chạyInterface và lớp trừu tượngNhiều interfaceInterface tường minhUpcasting và downcastingÔn tập: bộ tính toán hình học8Các khái niệm OOP nâng cao
Ưu tiên composition hơn kế thừaGenerics (lớp và phương thức)Delegate và sự kiệnAttribute và reflectionGiao diện IDisposable và câu lệnh usingKiến thức cơ bản về dependency injection11Các mẫu thiết kế phần 2
Mẫu lệnhMẫu bộ chuyển đổiMẫu trang tríMẫu phương thức khuôn mẫuMẫu trạng tháiMẫu tổ hợpTự luyện tập: Trình biên dịch C# online