8.1: Refactoring Book. Self Encapsulate Field
In object-oriented programming, encapsulation is a fundamental principle that involves bundling the data (fields) and the methods that operate on the data into a single unit, typically a class. This practice restricts direct access to some of an object's components, which is a means of preventing unintended interference and misuse.1
A specific refactoring technique that enhances encapsulation is Self Encapsulate Field. This technique involves replacing direct access to a private field within its own class with getter and setter methods. By doing so, you gain greater control over how the field is accessed and modified, allowing for additional logic such as validation, lazy initialization, or logging.
Why Refactor?
Directly accessing a private field within its own class can limit flexibility and maintainability. By introducing getter and setter methods, you can:
Implement Validation: Ensure that only valid data is assigned to the field.
Enable Lazy Initialization: Delay the creation of the field's value until it's actually needed, optimizing resource usage.
Add Logging or Monitoring: Track when and how the field's value is accessed or modified.
Facilitate Subclassing: Allow subclasses to override the getter and setter methods to change the behavior of the field access.
Practical Examples
1. Lazy Initialization
Imagine a Customer class that has an orders field, which is a list of orders. You want to delay the creation of this list until it's actually needed to save resources.
Before Refactoring:
Java
public class Customer {
private List<Order> orders = new ArrayList<>(); // orders list created immediately
}
Here, the orders list is created as soon as a Customer object is created, even if it's never used.
After Refactoring:
Java
public class Customer {
private List<Order> orders; // orders list is now private
public List<Order> getOrders() {
if (orders == null) { // Lazy initialization
orders = new ArrayList<>();
}
return orders;
}
}
In this refactored version, the orders list is initialized within the getOrders() method, but only when the getOrders() method is called for the first time. This is called lazy initialization. Now, the list only gets created if someone actually needs to access it.
2. Validation
Consider a BankAccount class with a balance field. You want to ensure that the balance cannot be set to a negative value.
Before Refactoring:
Java
public class BankAccount {
private double balance;
public void setBalance(double balance) {
this.balance = balance;
}
}
Here, there's no validation, so the balance can be set to any value, including negative amounts.
After Refactoring:
Java
public class BankAccount {
private double balance;
public void setBalance(double balance) {
if (balance < 0) {
throw new IllegalArgumentException("Balance cannot be negative");
}
this.balance = balance;
}
}
In this refactored version, the setter method includes validation to ensure that the balance cannot be set to a negative value, maintaining the integrity of the BankAccount object.
3. Logging
Suppose you have a Product class with a price field, and you want to log every time the price is updated.
Before Refactoring:
Java
public class Product {
private double price;
public void setPrice(double price) {
this.price = price;
}
}
Here, there's no logging, so you can't track when the price is updated.
After Refactoring:
Java
public class Product {
private double price;
public void setPrice(double price) {
this.price = price;
System.out.println("Price updated to: " + price);
}
}
In this refactored version, every time the setPrice method is called, a log message is printed, allowing you to track when the price is updated.
4. Facilitating Subclassing
Imagine a Shape class with a color field, and you want to allow subclasses to change the behavior of how the color is set.
Before Refactoring:
Java
public class Shape {
private String color;
public void setColor(String color) {
this.color = color;
}
}
Here, the setColor method is not easily extensible by subclasses.
After Refactoring:
Java
public class Shape {
private String color;
public void setColor(String color) {
this.color = color;
onColorChanged();
}
protected void onColorChanged() {
// Default behavior
}
}
In this refactored version, the onColorChanged method is called every time the color is set. Subclasses can override this method to change the behavior when the color changes.
5. Centralizing Logic
Consider a Rectangle class with width and height fields, and you want to calculate the area.
Before Refactoring:
Java
public class Rectangle {
private double width;
private double height;
public double getArea() {
return width * height;
}
}
Here, the getArea method directly accesses the width and height fields.
After Refactoring:
Java
public class Rectangle {
private double width;
private double height;
public double getWidth() {
return width;
}
public void setWidth(double width) {
this.width = width;
}
public double getHeight() {
return height;
}
public void setHeight(double height) {
this.height = height;
}
public double getArea() {
return getWidth() * getHeight();
}
}
In this refactored version, the width and height fields are accessed and modified through their respective getter and setter methods. This approach allows you to:
Validate Input: Ensure that the width and height cannot be set to negative values, maintaining the integrity of the
Rectangleobject.Centralize Logic: If you need to change how the area is calculated, you can modify the
getAreamethod without affecting other parts of the code.Enhance Flexibility: If you later decide to add additional behavior when setting the width or height (e.g., logging changes), you can do so in the setter methods without altering other methods that use these fields.
6. Improving Maintainability
Suppose you have a Person class with name and age fields, and you want to ensure that the age cannot be set to a negative value.
Before Refactoring:
Java
public class Person {
private String name;
private int age;
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
}
Here, there's no validation for the age field.
After Refactoring:
Java
public class Person {
private String name;
private int age;
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
if (age < 0) {
throw new IllegalArgumentException("Age cannot be negative");
}
this.age = age;
}
}
In this refactored version, the setter method for age includes validation to ensure that the age cannot be set to a negative value, maintaining the integrity of the Person object.
When to Apply Self Encapsulate Field:
This refactoring is particularly useful when:
You need to add validation or additional logic when accessing or modifying a field.
You anticipate that the implementation of a field may change in the future, and you want to isolate those changes to getter and setter methods.
You want to provide a controlled interface for accessing and modifying a field, which can be useful for debugging or monitoring.
By applying the Self Encapsulate Field refactoring, you enhance the maintainability, flexibility, and robustness of your code, leading to a more modular and adaptable codebase.

