Menu
CoddyTech

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ể.

challenge icon

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ớp PaymentMethod trong namespace Payments. Lớp cơ sở này có thuộc tính Name (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ức virtual có tên ProcessPayment(decimal amount), trả về "Processing ${amount} via {Name}".
  • CreditCardPayment.cs: Định nghĩa lớp CreditCardPayment trong namespace Payments, kế thừa từ PaymentMethod. Thêm thuộc tính CardNumber (chuỗi). Hàm khởi tạo nhận tên và số thẻ, dùng base để truyền tên. Ghi đè ProcessPayment bằng từ khóa sealed để 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ớp sealed có tên BankTransfer trong namespace Payments, 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ính BankName (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ếu PaymentMethod và gọi ProcessPayment trê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 National

Dù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
    }
}
quiz iconTự kiểm tra

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

Tự luyện tập: Trình biên dịch C# online