S12: Getters and setters for every field
What it looks like
Section titled “What it looks like”pub struct Customer { name: String, email: String, loyalty_points: u32,}
impl Customer { pub fn new(name: String, email: String, loyalty_points: u32) -> Self { Self { name, email, loyalty_points, } } pub fn get_name(&self) -> String { self.name.clone() } pub fn set_name(&mut self, name: String) { self.name = name; } pub fn get_email(&self) -> String { self.email.clone() } pub fn set_email(&mut self, email: String) { self.email = email; } pub fn get_loyalty_points(&self) -> u32 { self.loyalty_points } pub fn set_loyalty_points(&mut self, loyalty_points: u32) { self.loyalty_points = loyalty_points; }}Why AI agents produce it
Section titled “Why AI agents produce it”- It is the Java convention. JavaBeans, Lombok, and many style guides say “private fields, public accessors”. The agent applies the rule to every type.
- It looks like encapsulation. The fields are private, so the code looks careful.
- The names come from Java.
get_nameandset_namearegetNameandsetNamein snake case.
Why it is a problem
Section titled “Why it is a problem”- It protects nothing. A setter that writes any value is the same as a public field, with more code. In this example,
set_email("not an email")is accepted. - Every getter clones.
get_name()returns a newStringeach time it is called: a heap allocation to read a field. - The type is harder to use. Callers outside the module cannot destructure it or build it with a struct literal.
customer.get_name()is also longer thancustomer.name. - Encapsulation in Rust is per module, not per type. A private field is visible to all code in its module. Accessors do not add a boundary that the module does not already have. See Structs and impl.
The better version
Section titled “The better version”WorsePrivate fields, a
newwith all fields, and a cloning getter and a setter for each one.pub struct Customer {name: String,email: String,loyalty_points: u32,}impl Customer {pub fn new(name: String, email: String, loyalty_points: u32) -> Self {Self {name,email,loyalty_points,}}pub fn get_name(&self) -> String {self.name.clone()}pub fn set_name(&mut self, name: String) {self.name = name;}pub fn get_email(&self) -> String {self.email.clone()}pub fn set_email(&mut self, email: String) {self.email = email;}pub fn get_loyalty_points(&self) -> u32 {self.loyalty_points}pub fn set_loyalty_points(&mut self, loyalty_points: u32) {self.loyalty_points = loyalty_points;}}BetterPublic fields, and derived traits. Five lines.
#[derive(Debug, Clone, PartialEq, Eq)]pub struct Customer {pub name: String,pub email: String,pub loyalty_points: u32,}
When it is acceptable
Section titled “When it is acceptable”When a field has a rule, keep it private and write methods that keep the rule:
/// The email must contain an `@`. The field is private so that no code can break that rule.#[derive(Debug)]pub struct Customer { pub name: String, email: String,}
#[derive(Debug, PartialEq, Eq)]pub struct InvalidEmail;
impl Customer { pub fn new(name: &str, email: &str) -> Result<Self, InvalidEmail> { let mut customer = Self { name: name.to_string(), email: String::new(), }; customer.set_email(email)?; Ok(customer) }
pub fn email(&self) -> &str { &self.email // a getter without `get_`, that returns a borrow }
pub fn set_email(&mut self, email: &str) -> Result<(), InvalidEmail> { if !email.contains('@') { return Err(InvalidEmail); } email.clone_into(&mut self.email); Ok(()) }}- The setter returns a
Result, because it can refuse a value. - The getter is named after the field (
email(), notget_email()), and returns a borrow (&str), not a clone. That is the Rust API Guidelines convention. nameis still a public field, because it has no rule.
Accessors are also correct in a library that must be able to change its internal representation without breaking callers, and for computed values (total_cents() that sums the lines).
How to detect it
Section titled “How to detect it”- No Clippy lint reports accessors as such.
- Search for
fn get_andfn set_. Setters that only assign, and getters that only return or clone a field, are the smell. - Look at the types of getters:
-> Stringor-> Vec<T>from a getter usually means a clone.
What to tell your agent
Section titled “What to tell your agent”Remove the getters and setters that only read or write a field, and makethose fields pub. Keep a field private only if a rule connects it to otherdata or limits its values. For those fields, keep a setter that returns aResult, and a getter named after the field (no get_ prefix) that returns aborrow such as &str instead of a clone. Update the callers and run the tests.Related: S38: pub on everything, and Structs and impl.