Android

Getters và setters

Nhiều bạn khi code, nhất là các bạn coder tự học không qua trường lớp bài bản, hay thắc mắc là vì sao lại sử dụng getters và setters thay vì trực tiếp thay đổi giá trị của một field member hay property của class? Chẳng hạn, tôi có một Plain Old Java Object (gọi tắt là POJO) như bên dưới.

Thì câu hỏi bên trên sẽ là: Tại sao không đặt String name, int age và boolean gender là public rồi gọi trực tiếp chúng để đọc giá trị hiện tại của chúng cũng như gán giá trị mới, mà phải tạo và sử dụng các methods là getters và setters để làm gì cho tốn công sức. Điều này không chỉ xảy ra với Java như ví dụ bên trên, mà còn phổ biến ở khá nhiều ngôn ngữ khác có tính hướng đối tượng như C++, Dart, hay PHP. Đây là một câu hỏi rất hay, và cũng có rất nhiều câu trả lời khác nhau. Tuy nhiên, có nhiều câu trả lời khiến tôi không thỏa mãn, và tôi xin đưa ra phần giải thích của cá nhân mình, một amateur coder không qua bất kì trường lớp đào tạo nào. Vì đại đa số các độc giả của EitGuide là những chiến binh Android, nên tôi sẽ sử dụng Java để ví dụ cho dễ.

Câu trả lời của tôi, đơn giản, là có nhiều lí do ứng với nhiều trường hợp như sau:

1. Chặn việc thay đổi giá trị (cá nhân tôi hay sử dụng từ đường một chiều):

Bây giờ, tôi bỏ hết các setters trong class Student bên trên, nó sẽ trở thành:

Như vậy, bạn chỉ có thể đặt các giá trị name, age và gender một lần duy nhất từ/trong constructor. Một khi các giá trị đó đã được gán xong, bạn không có cách nào thay đổi được chúng cả. Tuy nhiên, nếu không có getters thì bạn không thể lấy các giá trị đó ra để sử dụng được. Vì vậy, bạn cần có getters cho các props của class Student. Như vậy, tóm lại, bạn chỉ có thể gán giá trị cho các props một lần duy nhất và chỉ có thể đọc chúng mà thôi. Con đường thay đổi giá trị đã bị chặn lại hoàn toàn.

Trong Java SDK, có rất, rất nhiều những class có tính khống chế như trên, đơn cử là class File với constructor File(String path). Giá trị path chỉ nhập vào một lần duy nhất trong constructor, muốn lấy giá trị thì dùng getPath.

2. Khống chế giá trị của members:

Bây giờ, tôi muốn đảm bảo age phải không âm, vì tuổi đâu thể nào là -10 hay -60 được. Vì vậy, khi nhập số tuổi, tôi phải chặn những giá trị bị âm. Tuy nhiên, nếu để age là public và tự do thay đổi nó, thì không có cách nào bảo đảm tuổi sẽ luôn không âm, vì trong quá trình nhập liệu, nhiều khi có sai sót không đáng có. Do đó, tôi sẽ sử dụng setter như sau:

Tôi có thay đổi hai chỗ: Thứ nhất, trong setAge, nếu giá trị được nhập vào là một số âm, thì sẽ “quăng, ném, thảy, tống” ra một ngoại lệ tên IllegalArgumentException. Cái tên nói lên tất cả, ngoại lệ này cho biết tham số truyền vào là không hợp lệ, với nguyên nhân là phần nằm trong cặp ngoặc tròn. Như vậy, nếu gặp trường hợp này, thì chương trình sẽ dừng ngay lập tức (nếu không có try catch) để không ảnh hưởng về sau. Ngoài ra, tôi cũng muốn khống chế tham số trong constructor. Vì vậy, thay vì tôi gán giá trị cho age như kiểu name và gender trong constructor, tôi gọi method setAge.

Ngoài IllegalArgumentException, bạn có thể vô tư sử dụng bất kì Exception nào cũng được. Chẳng hạn, trong trường hợp trên, tôi cho throw new FileNotFoundException cũng chẳng ảnh hưởng gì hòa bình thế giới, nhưng tên của Exception kia không phản ánh đúng trường hợp về tham số này. Ngoài ra, trong Java SDK cũng có rất nhiều setters kiểu này. Chẳng hạn như ArrayList khá quen thuộc với chúng ta. Ở đây, setter nằm hẳn trong constructor:

3. Setter còn thực hiện thêm các công việc khác ngoài việc gán giá trị:

Thực tế, bạn gặp trường hợp này như cơm bữa. Chẳng hạn như hàm setText của TextView trong Android. Ngoài việc gán giá trị mới cho mText – tức giá trị mà TextView.getText trả về, hàm này còn phải thực hiện rất nhiều công đoạn khác để vẽ lại phần text mà bạn nhập vào. Bạn có thể tham khảo thêm trong mã nguồn của class android.widget.TextView, với phần thân hàm bắt đầu từ dòng 5259 tới tận dòng 5410, tức hơn 150 dòng cho hàm setText(CharSequence text, BufferType type, boolean notifyBefore, int oldlen)setText(CharSequence) thực chất gọi tới hàm 4 tham số vừa rồi. Do đó, không phải setter chỉ đơn thuần là gán giá trị mới, mà thực tế, phần gán giá trị chỉ là một phần nhỏ trong thân setter mà thôi.

Ở đây, tôi sẽ tạm minh họa ý tưởng trên bằng cách thêm thắt “vài muỗng muối” như sau:

Thực tế, đa phần các class nằm trong các bộ SDK đều có các setters kiểu này, thay vì chỉ đơn thuần là gán giá trị như trong các data class thuần, chẳng hạn như POJOs. Vì vậy, việc tạo các setters cho một member khá quan trọng, dù hiện tại chúng chỉ có mỗi dòng là gán giá trị. Vì sau này, biết đâu bạn cần thực hiện thêm các thao tác khác trước hoặc sau khi gán giá trị mới cho member?

4. Các setters gián tiếp:

Tiếp tục với class TextView, bạn có hai phương thức setText: Một phương thức trực tiếp là setText(CharSequence), và phương thức thứ hai là setText(@StringRes int). Phương thức thứ hai sẽ gọi tới phương thức thứ nhất với tham số đã được xử lí (tức từ int resource sang CharSequence).

5. Lazy getters:

Tôi đã trình bày 3 kiểu setters sau kiểu getter đầu tiên, nên nãy giờ có lẽ bạn đang nghĩ getter chỉ có mỗi công việc trả ra member mà thôi. Tuy nhiên, vì getter là một hàm nên bạn cứ thoải mái làm thực hiện bao nhiêu tác vụ tùy ý trước khi trả kết quả về. Và phổ biến nhất trong số đó là lazy initialization, tức là khởi tạo “theo kiểu lười”.

Kiểu getter này thường áp dụng cho các field members hơn là properties. Chẳng hạn như AppCompatDelegate trong AppCompatActivity mà tôi sẽ giới thiệu trích đoạn như bên dưới:

Và khi sử dụng, không bao giờ họ sử dụng trực tiếp mDelegate, mà gọi gián tiếp qua getDelegate, vì nó luôn đảm bảo mDelegate không bao giờ null, và chỉ khởi tạo một lần duy nhất. Cách này cũng tối ưu hóa bộ nhớ, vì mDelegate chỉ được gán giá trị khi hàm getDelegate được gọi, còn nếu hàm này không được gọi thì mDelegate vẫn null và dễ dàng bị dọn dẹp để giải phóng tài nguyên khi instance của class không còn được sử dụng nữa.

Một chút chia sẻ ở từ “lazy” là thay vì gọi if null equal như trên trong mỗi hàm cần gọi tới member, ta tách phần khởi tạo ra một hàm riêng để sử dụng cho gọn và như vậy là “lười biếng”. Mặc dù vậy, đây là một getter cực kì hữu hiệu và được sử dụng rất rộng rãi, đặc biệt là trong Java. Ngoài ra, bạn có thể đang nghĩ phạm vi sử dụng lazy getter chỉ là private hoặc tối đa lắm là protect. Nhưng không, nó có thể được gán phạm vi public để được sử dụng bất kì nơi đâu.

Bên cạnh đó, bạn cũng phân biệt với một static method có cấu trúc tương tự như bản chất của method và instance liên quan là static chứ không phải là dạng instance thường. Dạng này không được gọi là lazy getter, chẳng hạn:

 

6. Safe getter:

Ngoài lazy getter thì safe getter cũng khá thường gặp, mà công dụng của nó thường là tránh null, hay đúng hơn là tránh bị thảy NullPointerException. Dạng đơn giản nhất của nó, khi được áp dụng vào class Student của tôi là:

Có nghĩa là nếu property name đang bị null, thì kết quả trả về sẽ là một String trống, do đó nó không bao giờ null mặc dù member ứng với nó vẫn đang bị null như thường, và người code cũng không có ý định thay đổi giá trị của member đó.

7. Super getter:

Ngoài ra, chúng ta cũng có một kiểu getter khác với hình thái khá tương tự, nhưng sẽ gọi tới super class thay vì giá trị cứng định sẵn, chẳng hạn:

Kiểu getter này luôn có @Override và super class của nó cũng phải có getter tương ứng.

8. Inner Builder class

Đây là một cách thức khá phổ biến để dựng một Object không trực tiếp với constructor. Bạn đã làm quen với kiểu này với AlertDialog.Builder để dựng AlertDialog, nhưng bạn không bao giờ thao tác trực tiếp với AlertDialog constructor, vì phạm vi của nó là private. Ở đây, tôi biến class Student của mình theo hướng này:

Kiểu getter/setter này có các đặc điểm chung như sau:

  • Setters luôn nằm trong một static nested inner class, thường có tên là Builder,
  • Giá trị trả về của setter chính là instance hiện tại (của Builder), tức phần return cuối hàm bao giờ cũng là “return this”, nên bạn có thể gọi “liên hoàn” các methods nối đuôi nhau, chẳng hạn: Student.Builder studentBuilder = new Student.Builder().setName(“Trần Văn Anh”).setAge(50).setGender(false);
  • Trong class Builder bao giờ cũng có một method tổng kết trả về instance của outer class chứa nó, và thường có tên là build và không chứa bất kì tham số nào. Đi kèm với nó là constructor của outer class bao giờ cũng có phạm vi private, còn constructor của inner class có phạm vi không private.
  • Outer class bọc ngoài luôn chỉ có các getters.

Getters trong kiểu này giống với trong trường hợp 1, còn setters giúp bạn không nhất thiết phải thiết lập đầy đủ các fields trong constructor, mà chỉ cần set những giá trị có các biến mà bạn muốn, các biến bạn không định giá trị sẽ vẫn có giá trị mặc định (thường là null). Chẳng hạn, bạn có thể chỉ cần setName và setAge mà thôi.

9. Một lí do khác để tạo getter và setter là để các subclass về sau có thể override để thêm các phương thức khác hoặc định nghĩa lại nếu cần.

Tổng kết

Như vậy, tôi đã giới thiệu cho các bạn khá nhiều kiểu getter và setter khác nhau để trả lời câu hỏi “Tại sao không cho các members là dạng public rồi trực tiếp đọc/ghi từ chúng thay vì đặt thêm các getters và setters cho phiền phức?”. Tất nhiên, đó chỉ là những kiểu g/s mà tôi đã gặp và thống kê, ngoài ra còn có các kiểu khác với các mục đích khác. Tuy nhiên, đối với những class thuần về chứa dữ liệu theo kiểu POJO mà bản thân bạn chắc chắn không bao giờ cần phải thêm thắt rào cản nào và hoàn toàn không cần g/s thì cứ thoải mái đặt các member dưới dạng public rồi trực tiếp thao tác với chúng. Thực tế, trong Android SDK cũng có không ít các trường hợp như vậy, chẳng hạn các members trong class android.util.DisplayMetrics hay android.content.res.Configuration đều là public, không có getters và setters nào cả, và đặt ra chúng là điều không cần thiết. Ngoài ra, trường hợp phổ biến nhất không dùng getter là constants: Các hằng số dù phạm vi của nó là như nào, thì bạn cũng không nên đặt getter cho nó, chẳng hạn như Toast.LENGTH_LONG. Cuối cùng, tôi hi vọng bạn đã khá thỏa mãn cho thắc mắc đã nêu trong câu hỏi lớn của bài viết, để khi phỏng vấn xin việc, dù bạn không thể trả lời được bằng lí thuyết thuần thì chí ít cũng nêu được vài ví dụ. Bởi theo anh Bucky Roberts là chủ kênh YouTube TheNewBoston, “If you don’t know why and how to create getters and settes, you won’t get a job”.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.