It depends of the actual meaning of a
, b
and getProduct
.
The purpose of getters is to be able to change the actual implementation while keeping the interface of the object the same. For example, if one day, getA
becomes return a + 1;
, the change is localized to a getter.
Real scenario cases are sometimes more complicated than a constant backing field assigned through a constructor associated with a getter. For instance, the value of the field may be computed or loaded from a database in the original version of the code. In the next version, caching may be added to optimize performance. If getProduct
continues to use the computed version, it won't benefit from caching (or the maintainer will do the same change twice).
If it makes perfect sense for getProduct
to use a
and b
directly, use them. Otherwise, use getters to prevent maintenance issues later.
Example where one would use getters:
class Product {
public:
Product(ProductId id) : {
price = Money.fromCents(
data.findProductById(id).price,
environment.currentCurrency
)
}
Money getPrice() {
return price;
}
Money getPriceWithRebate() {
return getPrice().applyRebate(rebate); // ← Using a getter instead of a field.
}
private:
Money price;
}
While for the moment, the getter doesn't contain any business logic, it is not excluded that the logic in the constructor will be migrated to the getter in order to avoid doing database work when initializing the object:
class Product {
public:
Product(ProductId id) : id(id) { }
Money getPrice() {
return Money.fromCents(
data.findProductById(id).price,
environment.currentCurrency
)
}
Money getPriceWithRebate() {
return getPrice().applyRebate(rebate);
}
private:
const ProductId id;
}
Later, caching may be added (in C#, one would use Lazy<T>
, making the code short and easy; I don't know if there is an equivalent in C++):
class Product {
public:
Product(ProductId id) : id(id) { }
Money getPrice() {
if (priceCache == NULL) {
priceCache = Money.fromCents(
data.findProductById(id).price,
environment.currentCurrency
)
return priceCache;
}
Money getPriceWithRebate() {
return getPrice().applyRebate(rebate);
}
private:
const ProductId id;
Money priceCache;
}
Both changes were focused on the getter and the backing field, the remaining code being unaffected. If, instead, I had used a field instead of a getter in getPriceWithRebate
, I would have to reflect the changes there as well.
Example where one would probably use private fields:
class Product {
public:
Product(ProductId id) : id(id) { }
ProductId getId() const { return id; }
Money getPrice() {
return Money.fromCents(
data.findProductById(id).price, // ← Accessing `id` directly.
environment.currentCurrency
)
}
private:
const ProductId id;
}
The getter is straightforward: it is a direct representation of a constant (similar to C#'s readonly
) field which is not expected to change in the future: chances are, ID getter will never become a computed value. So keep it simple, and access the field directly.
Another benefit is that the getId
might be removed in the future if it appears that it is not used outside (like in the previous piece of code).