Giới thiệu về Getter/Setter trong lập trình hướng đối tượng
Trong thế giới lập trình hướng đối tượng (OOP), các khái niệm như đóng gói (encapsulation), kế thừa (inheritance) và đa hình (polymorphism) là nền tảng cốt lõi. Trong đó, đóng gói đóng vai trò quan trọng trong việc bảo vệ dữ liệu và kiểm soát quyền truy cập. Và khi nhắc đến đóng gói, chúng ta không thể không nhắc đến Getter và Setter – hai phương thức quen thuộc giúp chúng ta tương tác với các thuộc tính của đối tượng.
Tuy nhiên, liệu việc sử dụng Getter/Setter có phải lúc nào cũng là giải pháp tối ưu? Bài viết này của The Blogs News sẽ đi sâu vào định nghĩa, lợi ích, và đặc biệt là những trường hợp bạn nên cân nhắc kỹ lưỡng trước khi áp dụng Getter/Setter, nhằm xây dựng một mô hình đối tượng mạnh mẽ và linh hoạt hơn.

Getter và Setter là gì?
Về cơ bản, Getter (còn gọi là accessor) là một phương thức dùng để đọc giá trị của một thuộc tính (field) trong một đối tượng. Ngược lại, Setter (còn gọi là mutator) là một phương thức dùng để gán hoặc thay đổi giá trị của một thuộc tính.
Mục đích chính của chúng là cung cấp một giao diện công khai (public interface) để truy cập và sửa đổi các thuộc tính riêng tư (private fields) của một lớp, từ đó thực hiện nguyên tắc đóng gói. Thay vì cho phép truy cập trực tiếp vào dữ liệu nội bộ, Getter/Setter đóng vai trò như những “người gác cổng”, kiểm soát cách dữ liệu được đọc và ghi.

Lợi ích của việc sử dụng Getter/Setter
Khi được sử dụng đúng cách, Getter/Setter mang lại nhiều lợi ích đáng kể:
- Đóng gói dữ liệu (Data Encapsulation): Đây là lợi ích quan trọng nhất. Bằng cách giữ các thuộc tính ở chế độ private và cung cấp Getter/Setter, chúng ta có thể ẩn đi cấu trúc dữ liệu nội bộ của đối tượng, chỉ để lộ ra giao diện cần thiết để tương tác. Điều này giúp bảo vệ tính toàn vẹn của dữ liệu.
- Kiểm soát quyền truy cập: Chúng ta có thể quyết định thuộc tính nào có thể được đọc (chỉ Getter), thuộc tính nào có thể được ghi (chỉ Setter), hoặc cả hai. Điều này cho phép kiểm soát chi tiết hơn so với việc đặt thuộc tính là public.
- Xác thực dữ liệu (Data Validation): Trong phương thức Setter, bạn có thể thêm logic để kiểm tra tính hợp lệ của dữ liệu trước khi gán. Ví dụ, đảm bảo tuổi không âm, email đúng định dạng, v.v. Nếu dữ liệu không hợp lệ, Setter có thể từ chối gán hoặc ném ra ngoại lệ.
- Tính linh hoạt và khả năng mở rộng: Nếu sau này bạn muốn thay đổi cách một thuộc tính được lưu trữ hoặc tính toán, bạn chỉ cần sửa đổi logic bên trong Getter/Setter mà không ảnh hưởng đến mã bên ngoài đang sử dụng chúng. Điều này giúp giảm thiểu sự phụ thuộc và tăng tính linh hoạt cho hệ thống.

Khi nào nên dùng Getter/Setter?
Getter/Setter phát huy hiệu quả tốt nhất trong các trường hợp sau:
- Đối tượng giá trị (Value Objects): Các đối tượng chỉ chứa dữ liệu và không có hành vi phức tạp (ví dụ: một đối tượng Point với x, y; một đối tượng Money với amount, currency). Trong trường hợp này, việc cung cấp Getter để truy cập dữ liệu là hoàn toàn hợp lý.
- DTOs (Data Transfer Objects) hoặc View Models: Các đối tượng được thiết kế để truyền dữ liệu giữa các lớp hoặc giữa các tầng của ứng dụng (ví dụ: từ tầng dịch vụ đến tầng giao diện người dùng). Mục đích chính của chúng là mang dữ liệu, nên Getter/Setter là cần thiết.
- Khi cần kiểm soát chặt chẽ việc thay đổi dữ liệu: Nếu bạn cần thêm logic xác thực, ghi log, hoặc kích hoạt một sự kiện nào đó mỗi khi một thuộc tính được thay đổi, Setter là nơi lý tưởng để đặt logic này.
Mặt trái của Getter/Setter: Khi nào không nên dùng?
Mặc dù hữu ích, việc lạm dụng Getter/Setter có thể dẫn đến những vấn đề nghiêm trọng trong thiết kế OOP:
- Mô hình đối tượng thiếu sức sống (Anemic Domain Model): Đây là vấn đề phổ biến nhất. Khi một lớp chỉ toàn Getter/Setter và không có bất kỳ hành vi (phương thức) nào khác ngoài việc đọc/ghi dữ liệu, nó trở thành một “túi dữ liệu” rỗng tuếch. Logic nghiệp vụ sẽ bị đẩy ra ngoài, nằm rải rác trong các lớp dịch vụ (service classes), làm mất đi bản chất của OOP là kết hợp dữ liệu và hành vi.

- Phá vỡ tính đóng gói: Nghe có vẻ mâu thuẫn, nhưng việc cung cấp Getter/Setter cho mọi thuộc tính một cách mặc định thực chất lại làm suy yếu tính đóng gói. Nó cho phép mã bên ngoài truy cập và thao tác với trạng thái nội bộ của đối tượng một cách quá tự do, biến đối tượng thành một cấu trúc dữ liệu công khai.
- Tăng sự phụ thuộc: Khi bạn gọi Getter của một đối tượng để lấy dữ liệu, sau đó thực hiện một số logic và gọi Setter để cập nhật lại dữ liệu đó, bạn đang tạo ra sự phụ thuộc chặt chẽ giữa mã gọi và cấu trúc dữ liệu nội bộ của đối tượng. Nếu cấu trúc dữ liệu thay đổi, mã gọi cũng phải thay đổi.
- Mã nguồn dài dòng (Boilerplate Code): Trong nhiều ngôn ngữ, việc viết Getter/Setter cho nhiều thuộc tính có thể tạo ra một lượng lớn mã lặp lại, làm cho lớp trở nên khó đọc và khó bảo trì hơn.
Các giải pháp thay thế và cách tiếp cận tốt hơn
Để tránh những cạm bẫy của việc lạm dụng Getter/Setter, hãy tập trung vào việc thiết kế các đối tượng giàu hành vi:
- Đối tượng giàu hành vi (Behavior-rich Objects): Thay vì lấy dữ liệu ra ngoài để xử lý, hãy đặt logic nghiệp vụ vào chính đối tượng sở hữu dữ liệu đó. Ví dụ, thay vì
customer.getBalance()rồicustomer.setBalance(customer.getBalance() - amount), hãy có phương thứccustomer.withdraw(amount).
- Phương thức lệnh (Command Methods): Thiết kế các phương thức công khai thực hiện một hành động cụ thể, thay đổi trạng thái nội bộ của đối tượng một cách có kiểm soát. Ví dụ:
order.addItem(product, quantity),account.deposit(amount). - Sử dụng Constructor để khởi tạo: Đảm bảo đối tượng luôn ở trạng thái hợp lệ ngay từ khi được tạo ra bằng cách truyền tất cả các dữ liệu cần thiết qua constructor. Điều này giúp giảm thiểu nhu cầu sử dụng Setter.
- Nguyên tắc Demeter (Law of Demeter): Hạn chế sự tương tác của một đối tượng với các đối tượng khác. Một đối tượng chỉ nên nói chuyện với chính nó, các đối tượng nó tạo ra, các đối tượng được truyền vào phương thức của nó, hoặc các thuộc tính trực tiếp của nó. Tránh chuỗi Getter như
order.getCustomer().getAddress().getStreet().
Tối ưu hóa thiết kế đối tượng: Hướng tới sự mạnh mẽ và linh hoạt
Getter và Setter là những công cụ hữu ích trong lập trình hướng đối tượng, nhưng chúng không phải là giải pháp cho mọi vấn đề. Việc hiểu rõ khi nào nên sử dụng và khi nào nên tránh chúng là chìa khóa để xây dựng các hệ thống phần mềm mạnh mẽ, dễ bảo trì và mở rộng.
Hãy luôn ưu tiên thiết kế các đối tượng có trách nhiệm rõ ràng, kết hợp chặt chẽ dữ liệu và hành vi. Bằng cách này, bạn sẽ tạo ra một mô hình miền (domain model) phản ánh chính xác nghiệp vụ, giảm thiểu sự phụ thuộc và tối đa hóa lợi ích mà lập trình hướng đối tượng mang lại.








Leave a Comment