Getters وsetters و«أخبِر ولا تسأل» (tell, don't ask)
في هذا الدرس 11 موضوعات
هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.
Lesson Overview
Your Account now has private fields and checks in its constructor and methods. Callers, though, will want to see the balance and do things with it. The traditional Java answer is the getter (a method that reads a field) and the setter (a method that replaces it). Many IDEs can generate them for every field with a single command. That convenience is a trap.
This lesson shows why a generated setBalance undoes all your protection, when a getter is a good idea, and what to write instead: behavior methods such as deposit and withdraw. Behind that choice is a habit called "tell, don't ask": tell the object what you want done, rather than asking for its data and deciding outside.
In the bank picture, you do not ask the teller to read your balance out loud so that you can write a new one on the ledger. You say "withdraw 50", and the teller handles the ledger.
Why this matters: a class's methods are its design. A class made of only getters and setters is a data bag with the vault door removed.
This lesson builds directly on "Invariants: rules the object guarantees".
Learning Objectives
By the end of this lesson you will be able to:
- Write a getter and a setter and say what each is for.
- Show how a public setter can break an invariant.
- Decide, field by field, whether a getter, a setter, both, or neither is appropriate.
- Replace a setter with behavior methods.
- Rewrite "ask, then act outside" code as "tell" code.
- Explain why generating getters and setters for every field by reflex is a mistake.
Written Explanation
Getters and setters
A getter returns the value of a private field. A setter assigns a new value to it:
class Account {
private double balance;
Account(double balance) { this.balance = balance; }
double getBalance() { return balance; }
void setBalance(double balance) { this.balance = balance; }
}The names follow a convention: getX and setX for a field x (for a boolean, isX is common). Many tools and frameworks rely on this convention, which is why you will see it everywhere in Java code. But conventions are not a rule that every field needs a pair.
A setter is a public field in disguise
What does setBalance allow?
Account a = new Account(100);
a.setBalance(-500);
System.out.println(a.getBalance());-500.0The field is private, yet anyone can set it to any number. Compare this with lesson 1: a.balance = -500; was the original problem. A setter that does no checking offers the same hole with extra typing. You protected the constructor, deposit, and withdraw, and a generated setter walks around all of them.
You could add checks to the setter, which is better. But then ask: what is the setter for? A bank balance does not "get set". It changes because a deposit or a withdrawal happened. setBalance(70) loses that meaning: nobody can tell from the code whether 70 came from a withdrawal, a correction, or a mistake. And there may be rules for deposits (positive amount) that differ from rules for withdrawals (sufficient funds).
Prefer behavior methods
A behavior method is a method named for something that happens in the problem domain: deposit, withdraw, transferTo, closeAccount. It changes the object's state as a consequence, applying the rules that belong to that event.
| Instead of | Write |
|---|---|
setBalance(balance + 50) | deposit(50) |
setBalance(balance - 50) | withdraw(50) |
setStatus("CLOSED") | close() |
setItems(items - 1) | removeItem() |
Behavior methods give you a small public window with a meaning, and each one can enforce exactly the rules for its event.
"Tell, don't ask"
The habit behind this is "tell, don't ask". Compare:
// Ask: pull the data out, decide outside, push a new value back
if (a.getBalance() >= 50) {
a.setBalance(a.getBalance() - 50);
}
// Tell: say what you want; the object decides
a.withdraw(50);In the "ask" version, the rule "balance must cover the amount" lives in the caller, which is the situation lesson 1 warned about: every caller must repeat it, and one will get it wrong. In the "tell" version the rule lives in withdraw, once.
A useful sign: if you see a call chain like x.getY() followed by a decision followed by x.setY(...), the decision probably belongs inside x.
When a getter is fine
"Tell, don't ask" does not ban reading data. Reading is often necessary: to print a statement, to show a balance on a screen, to log. A getter is appropriate when a caller really needs the value for something the object should not do itself.
Ask for each field:
- Does any caller need to read it? If not, no getter.
- Does any caller need to replace it directly, bypassing the object's rules? Almost never. If the answer is "no rule applies", it may be a plain data object, but still consider a behavior method with a clear name.
- Can the answer be a question the object answers itself, such as
canWithdraw(50)orisEmpty(), instead of exposing the number?
A field that never changes after construction, like an account number or owner, needs a getter at most. Declare it final (see next lessons) so the compiler guarantees it never changes.
Naming note: this course sometimes writes the getter as balance() instead of getBalance(). Both work; the get prefix is the traditional JavaBeans style. Be consistent within a project, and follow the style your team or framework uses.
The accessor trap: do not return what you should protect
A getter hands out whatever the field refers to. For int, double, and String that is harmless, because the value cannot be modified through the copy you get. For a field that refers to a mutable object, such as an array, the getter hands out the live object. The immutable-objects lesson deals with this; keep it in mind.
Generated code is still code you own
IDEs offer a command to generate getters and setters for every field. Using it is not wrong, but doing it by reflex for every field creates a class where each field is readable and writable by anyone: effectively public fields. Generate only what you need, and delete the rest. The exact menu names depend on your IDE, so search its documentation for "generate getters and setters" if you want to use the feature.
Examples
Example 1: replacing setBalance with behavior (simple)
class Account {
private final String owner;
private double balance;
Account(String owner, double balance) {
if (owner == null || owner.isBlank()) throw new IllegalArgumentException("Owner required");
if (balance < 0) throw new IllegalArgumentException("Negative opening balance: " + balance);
this.owner = owner;
this.balance = balance;
}
String owner() { return owner; }
double balance() { return balance; }
void deposit(double amount) {
if (amount <= 0) throw new IllegalArgumentException("Deposit must be positive: " + amount);
balance += amount;
}
void withdraw(double amount) {
if (amount <= 0) throw new IllegalArgumentException("Withdrawal must be positive: " + amount);
if (amount > balance) throw new IllegalStateException("Insufficient funds");
balance -= amount;
}
}owner is final with a read-only accessor; balance has a read-only accessor too and changes only through deposit and withdraw. There is no setBalance, so the only ways to change the balance are the two methods that check their rules. With new Account("Sara", 100) and withdraw(50), printing owner and balance gives:
Sara: 50.0Example 2: a question instead of a number (practical)
Sometimes callers want to decide something based on the state. Offer a question method rather than the raw value:
boolean canWithdraw(double amount) {
return amount > 0 && amount <= balance;
}A user interface can call a.canWithdraw(80) to enable or disable a button (here it returns false for a balance of 50). The rule stays in Account: if the rules change (say, an overdraft limit is added), only this method and withdraw change, and every caller gets the new behavior. Note that withdraw must still check on its own; the question method is a convenience, not a replacement for the guard.
Example 3: from ask to tell in a loyalty program (realistic)
An "ask" version, with a getter and setter:
class Customer {
private int points;
Customer(int points) { this.points = points; }
int getPoints() { return points; }
void setPoints(int points) { this.points = points; }
}
Customer c = new Customer(120);
if (c.getPoints() >= 100) {
c.setPoints(c.getPoints() - 100);
}A "tell" version, where redeeming is the object's job:
class LoyaltyAccount {
private int points;
LoyaltyAccount(int points) { this.points = points; }
void earn(int amount) {
if (amount <= 0) throw new IllegalArgumentException("Points must be positive: " + amount);
points += amount;
}
boolean redeem(int cost) {
if (cost <= 0) throw new IllegalArgumentException("Cost must be positive: " + cost);
if (points < cost) return false;
points -= cost;
return true;
}
int points() { return points; }
}Calling redeem(100) twice on an object that starts with 120 points prints true 20 then false 20. Here redeem returns false for "not enough points" instead of throwing, because an unsuccessful redemption is a normal, expected outcome the caller wants to know about. Choose the style that fits: an exception for a programming error (negative cost), a return value for a normal alternative outcome.
Backend Insight
A very common backend pattern is the "anemic" class: fields plus getters and setters, with every rule written in a separate service class. It works, but rules are far from the data, and different services re-implement them differently. Putting behavior methods on the objects keeps each rule next to the data it protects.
Frameworks sometimes require getters and setters, for example to fill objects from JSON or database rows. That is a practical reason to have them on data-carrier classes at the system's edges. It is not a reason to put them on every domain object in the core. When you must follow a framework's convention, keep the objects at the edge simple and convert to protected domain objects inside.
Common Mistakes
Mistake 1: Generating getters and setters for every field by reflex
- Incorrect: every private field gets a public getter and setter, so the class is public fields in a longer form.
- Correct: add only the accessors a real caller needs; use behavior methods for changes.
Mistake 2: A setter without checks
A setter that assigns without validating reopens the hole. If a setter must exist, it has to enforce the same invariants as the constructor.
Mistake 3: Exposing state so the caller can decide
When callers fetch values only to compare and update, move the decision into the object as a method.
Mistake 4: Methods named after fields instead of events
setBalance(70) says nothing about why. withdraw(30) says exactly what happened and carries the right rules.
Exercises
Try each exercise yourself before opening the solutions.
Level A: Foundation
A1. What is the difference between a getter and a setter?
أظهر الحل A1
A1. A getter returns a field's value; a setter assigns a new value to the field.
A2. Why is setBalance(double) without checks dangerous?
أظهر الحل A2
A2. Anyone can set the balance to any number, including a negative one, skipping the rules in the constructor, deposit, and withdraw.
A3. Name two behavior methods that could replace setBalance on an Account.
أظهر الحل A3
A3. deposit(double amount) and withdraw(double amount). (transferTo(Account other, double amount) is a third.)
Level B: Application
B1. Rewrite if (c.getItems() > 0) c.setItems(c.getItems() - 1); as a method removeItem() on a Cart class that throws IllegalStateException("Cart is empty") when there is nothing to remove, plus an isEmpty() question method. Write the class and a main that creates a cart with one item, removes it, prints isEmpty(), then removes again.
أظهر الحل B1
B1.
class Cart {
private int items;
Cart(int items) {
if (items < 0) throw new IllegalArgumentException("Items cannot be negative: " + items);
this.items = items;
}
void removeItem() {
if (items == 0) throw new IllegalStateException("Cart is empty");
items--;
}
boolean isEmpty() { return items == 0; }
int items() { return items; }
}
public class Main {
public static void main(String[] args) {
Cart c = new Cart(1);
c.removeItem();
System.out.println(c.isEmpty());
c.removeItem();
}
}Output:
true
Exception in thread "main" java.lang.IllegalStateException: Cart is empty
at Cart.removeItem(Main.java:10)
at Main.main(Main.java:23)B2. A Student class has fields id (never changes), name (can be corrected), and credits (grows as courses are passed). Decide for each field: getter? setter? behavior method instead?
أظهر الحل B2
B2. id: getter only, and make the field final. name: a getter; a correction can be a setter-like method such as rename(String newName) that checks the name is not blank. credits: a getter, and a behavior method such as passCourse(int courseCredits) rather than setCredits.
B3. Rewrite in "tell" style: if (room.getGuests() < room.getCapacity()) room.setGuests(room.getGuests() + 1);. Which method name would you choose, and what should it do when the room is full?
أظهر الحل B3
B3. For example room.checkIn(). It increments the guest count, and when the room is full it throws IllegalStateException("Room is full") (or returns false if full is considered a normal outcome, as in the loyalty example). The comparison with capacity now lives inside Room.
Level C: Challenge
C1. A teammate adds setBalance "because the tests need it". Give two alternatives that let the tests reach a particular balance without a public setter.
أظهر الحل C1
C1. (1) Create the object through its constructor with the desired starting balance. (2) Reach the state through the public methods, for example deposit(500). Tests also usually should not need to bypass rules, since the rules are part of what is being tested.
C2. Account has getBalance(). A caller writes if (a.getBalance() >= 50) a.withdraw(50);. Is the invariant at risk? Is the code still a "tell, don't ask" smell?
أظهر الحل C2
C2. The invariant is not at risk, because withdraw still checks on its own. It is still a mild smell: the caller duplicates a rule the object owns. If withdraw later allows an overdraft, this caller's check would be wrong. A question method such as canWithdraw(50) would keep the rule in one place.
Mini Challenge
Take this class, which was generated by reflex, and redesign it.
class Ticket {
private String seat;
private boolean sold;
private double price;
// getters and setters for all three
}Decide which accessors to keep, which setters to delete, and what behavior methods to add (for example sell()), with the rules you choose (a ticket cannot be sold twice; price must be positive). Write the new class.
Check your work: no setter remains for sold, the constructor validates, sell refuses a second sale, and every remaining accessor can be justified by a real caller.
Summary & Next Lesson
Key takeaways
- A getter reads a field; a setter replaces it. Add one only when a caller truly needs it.
- A public setter without checks reopens the hole that private fields closed.
- Prefer behavior methods (
deposit,withdraw,removeItem) that name an event and apply its rules. - Tell, don't ask: tell the object what to do instead of reading its data and deciding outside.
- Do not generate getters and setters for every field by reflex.
Next lesson
So far the objects are changeable but protected. The next lesson, "Immutable objects", goes one step further: objects that cannot change at all after they are created.
Self-check
- Why is
setBalancerisky? - What does "tell, don't ask" mean?
- Name a field that usually needs a getter but no setter.
- What is a behavior method?
- When is a getter perfectly fine?
أظهر الإجابات
- It lets callers bypass the rules inside
depositandwithdraw. - Tell the object what to do rather than reading its data and making the decision outside.
- An account number or an owner; it is set once in the constructor.
- A method named after a domain event (such as
withdraw) that changes state while applying that event's rules. - When a caller really needs the value for something the object should not do itself, such as displaying it.
References
- Oracle, Controlling Access to Members of a Class. It supports the advice to avoid public fields, which is the same reasoning applied here to setters.
- Oracle, IllegalStateException (Java SE 21 API). It supports using this exception for a call made at an inappropriate time.
- "Tell, don't ask", the anemic-class remark, and the
get/isnaming convention are standard industry practice described from general knowledge; they were not checked against a fetched source in this lesson. All code and outputs come from compiling and running with JDK 21.
التطبيق
طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.
تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.