Let us tackle both points separately.
1. Class invariants
In programming, reasoning is made easier when you have invariants. For example, you may already have heard of loop invariants:
for (size_t i = 0; i < vec.size(); ++i) {
// something that does not alter i
}
In this loop, 0 <= i < vec.size()
is an invariant which guarantees that vec[i]
is always a valid expression.
Class can have invariants too, for example if you consider std::string
its size()
method returns the number of characters in its buffer. Always.
Now, suppose that you write your own string class:
// Invariant: size represents the number of characters in data.
struct String {
size_t size;
char* data;
};
It's good to document what you wish was the invariant, but I can perfectly do:
void reset(String& str) {
delete str.data;
str.data = 0;
}
and forget to reset str.size
, thus violating the invariant.
However, if you tuck away the class members:
// Invariant: size() returns the number of characters accessible via data()
class String {
public:
size_t size() const { return _size; }
char const* data() const { return _data; }
// methods which maintain the invariant
private:
size_t _size;
char* _data;
};
now only you can violate the invariant. Thus in case of bug you have less code to audit.
2. Implementation change insulation
The idea behind is that you should be able to switch the internal representation of information without adapting the users of the class. For example:
class Employee {
public:
std::string const& name() const { return _name; } // Bad
private:
std::string _name;
}; // class Employee
Now, if I realize that std::string
is not an appropriate representation for name (I would need wide characters, for example):
class Employee {
public:
std::string const& name() const { return _name; } // error!
private:
std::basic_string<char32_t> _name;
}; // class Employee
I am stuck. I cannot return a std::string const&
any longer (I don't have an internal std::string
any longer). I could alter the return of name()
to make a copy:
std::string Employee::name() const { return encodeUtf8(_name); }
unfortunately it may still break clients:
std::string const& name(Employee const& e) {
std::string const& n = e.name(); // Bind temporary variable to const&
return n; // Returns reference to local variable!!
}
Whereas if Employee
had been designed from the start with std::string name() const
we could make the change without issues.
Note: in real-world usage, you have to make external API with insulation in mind, but internal API may perfectly expose data-representation... at the cost of more changes on your plate when a change is made.