Về Getter Và Setter
Người dùng “sjinny” đã bình luận trong bài viết trước:
“Anh Cloud Wind nghĩ gì về việc viết đầy đủ setter/getter cho mọi thuộc tính dữ liệu… Gần đây tôi đang cố gắng tối giản ba lớp code quá cồng kềnh, nhưng riêng số lượng setter/getter đã khiến giao diện trở nên quá tải…”
Tôi xin chia sẻ góc nhìn của mình.
Đầu tiên, hầu như mọi vấn đề thiết kế đều không có câu trả lời tuyệt đối. Nếu có, chúng ta đã không cần con người tham gia vào việc này nữa. Bất kỳ vấn đề phức tạp nào, chỉ cần bạn định nghĩa được giải pháp chính xác, máy móc đều có thể thực hiện thay bạn.
Dưới đây là những nguyên tắc thiết kế cơ bản của tôi. Xin lưu ý: mọi quy tắc đều có ngoại lệ, nhưng tôi sẽ ít đề cập đến chúng ở đây vì chúng đòi hỏi những cân nhắc phức tạp hơn nhiều.
KISS (Giản dị là chìa khóa) đương nhiên là nguyên tắc hàng đầu. Tuy nhiên, mỗi người lại có cách hiểu khác nhau về KISS.
Theo quan điểm hiện tại của tôi, chúng ta nên hạn chế tối đa việc đưa vào các khái niệm mới. Ví dụ, nếu bạn dùng C, đừng cố gắng mô phỏng bảng vtable bằng mảng con trỏ hàm; nếu dùng C++, đừng cố tạo khái niệm “thuộc tính” bằng template… Những thứ này không nằm trong bản chất ngôn ngữ, và với người dùng (có thể là đồng đội, chính bạn trong tương lai, hoặc người kế nhiệm), chúng đều là những điều mới mẻ.
Trong đa số trường hợp, các “framework” cũng là những thứ mới. Cách tốt nhất để định hướng người dùng viết code theo chuẩn mực là sử dụng các tính năng ngôn ngữ phổ biến, đã được cộng đồng kiểm chứng qua thời gian.
Chúng ta nên tin tưởng rằng: thiết kế tinh tế luôn độc lập với công cụ. Giao diện đẹp sẽ tự nó đơn giản.
Điều đầu tiên cần làm là tìm hiểu các quy ước của ngôn ngữ bạn chọn. Nếu ngôn ngữ đó đủ trưởng thành, nhu cầu trừu tượng hóa đã được vô số người gặp phải, và giải pháp tốt sẽ được sàng lọc qua thời gian - không cần chúng ta phát minh lại. Vấn đề setter/getter cũng không phải ngoại lệ.
Gần đây tôi chủ yếu dùng C. Quy ước của C là gì? C sinh ra cùng Unix và là ngôn ngữ bản địa của hệ điều hành này. Hãy nhìn vào các giao diện Unix để tìm câu trả lời.
Ví dụ quen thuộc: getsockopt/setsockopt. Đây là nhu cầu tương tự: đọc/ghi một thuộc tính của đối tượng.
Trong hệ thống C truyền thống, người ta ít khi tạo riêng hai hàm cho mỗi thuộc tính (đọc/ghi). Thay vào đó, trạng thái nội bộ của đối tượng thường được thao tác qua một cặp API thống nhất.
Ít hơn là nhiều hơn.
Yếu tố thứ hai là hiệu năng.
Lập trình viên không quan tâm hiệu năng không thể coi là tốt. Đây là quan điểm cá nhân tôi. Dù vậy, nhiều lập trình viên kỳ cựu sẽ phản đối, họ sẽ khuyên: “Hiệu năng không phải lúc nào cũng quan trọng, đánh đổi vì hiệu năng có thể khiến bạn mất nhiều thứ hơn, và cuối cùng ngay cả hiệu năng cũng không giữ được”.
Trong 10 năm đầu học lập trình, tôi từng cuồng nhiệt theo đuổi tốc độ. Tôi đọc hàng tá sách, viết hàng đống code, tỉ mỉ tối ưu từng đoạn code tôi cho là cần thiết, viết đi viết lại nhiều lần.
Dần dần, tôi học được vài điều:
- Tin tưởng vào compiler.
- Tránh dùng mẹo vặt thông minh.
- Không đánh đổi tính rõ ràng của code.
- Lập trình phòng thủ.
- Ưu tiên code đáng tin cậy trước.
- Chấp nhận dùng thuật toán đơn giản hơn dù độ phức tạp thời gian cao hơn…
Với một người cuồng hiệu năng như tôi, những nguyên tắc này thật khó nuốt trôi.
Tôi không định lặp lại giá trị của chúng. Tôi muốn nói rằng mỗi khi phải chọn giải pháp ngược với hiệu năng, tôi luôn do dự và nghi ngờ. Tôi vẫn tin rằng có những ngoại lệ phá vỡ quy tắc. Vấn đề là làm sao xác định khi nào nên tuân thủ, khi nào nên phá vỡ. Đến nay tôi vẫn chưa thể tổng kết thành quy tắc cứng nhắc, chỉ có thể dựa vào trải nghiệm dày dặn để cảm nhận.
Nếu có quy tắc rõ ràng, hãy để máy móc quyết định, không phải con người.
Tôi dám khẳng định: lập trình viên mù quáng tin vào giáo điều sẽ không trưởng thành.
Có người tin rằng C++ có thể tạo ra chương trình hiệu năng cao hơn C nhờ các tính năng ngôn ngữ. Họ thường dẫn chứng bằng ví dụ so sánh algorithm::sort() và qsort().
Template cho phép nhúng hàm so sánh, loại bỏ chi phí gọi hàm, qua đó đánh bại qsort() (dùng hàm callback C cho mỗi lần so sánh) trong các benchmark.
Hai tuần trước, tại lễ trao giải cuộc thi Youdao, tôi đã thảo luận vấn đề này với các thí sinh. Tôi mở đầu bằng một ví dụ khác: khi xử lý dữ liệu, hiệu năng đọc từ đầu đến cuối, cuối đến đầu hay nhảy cách phụ thuộc vào nhiều yếu tố ngoài thuật toán: cách bộ điều khiển bộ nhớ hoạt động, quản lý CPU Cache, lịch trình bộ nhớ ảo của OS…
Đôi khi chúng ta phải quan tâm đến chi tiết này, đôi khi không. Muốn hệ thống hiệu năng cao, quyết định “khi nào quan tâm” còn khó hơn “cách tối ưu”. Với người cuồng hiệu năng, họ không phân biệt điều gì quan trọng hay không, mà sẽ tối ưu mọi thứ bất chấp lời chê cười. Qua thời gian, họ không cần tối ưu sau khi viết xong, vì ngay từ đầu đã cân nhắc kỹ lưỡng, và với kinh nghiệm tích lũy, code vừa hiệu năng vừa rõ ràng đáng tin cậy.
Tuy nhiên, đôi khi chúng ta chủ động bỏ qua khác biệt hiệu năng ở tầng thấp. Cũng vì lý do hiệu năng. Vì khi đi sâu vào chi tiết, cục bộ và toàn cục có thể mâu thuẫn. Ngay cả code cũng là một phần của hệ thống. Kích thước code cũng ảnh hưởng đến hiệu năng máy móc.
Khi nhìn từ góc độ cao hơn, bạn sẽ hiểu hiệu năng sâu sắc hơn. Kẻ thù lớn nhất của lập trình là độ phức tạp. Hiệu năng cũng bị nó đe dọa. Kiểm soát độ phân mảnh ở mọi tầng của phần mềm chính là vũ khí để giảm độ phức tạp.
Quay lại ví dụ sort. Liệu chi phí gọi hàm có thật sự không thể tha thứ? Với mảng số nguyên, điều này rất quan trọng. Vì thao tác so sánh nguyên thủy có chi phí thấp, việc gọi hàm, push/pop thanh ghi sẽ tạo ra chi phí gấp nhiều lần.
Nhưng ngoài sách giáo khoa và đề thi, chúng ta có bao nhiêu cơ hội thực sự để sort mảng nguyên?
Sort thường để tổ chức lại tập hợp đối tượng (mục đích cuối cùng thường là tìm kiếm hiệu quả hơn). Ở tầng xử lý này,