Introduction
Encapsulation is a technique used to enforce the separation of interface and implementation by hiding (making inaccessible) the implementation of a program-defined data type from users. We use access specifiers to achieve this objective.
Consider a classic OOP-based class
Since I’m hungry, let’s talk about a Pizza class:
#include <iostream>
#include <string>
#include <string_view>
class Pizza {
private:
std::string m_flavour { "Margherita" };
public:
void setFlavour(std::string_view flavour)
{
m_flavour = flavour;
}
const std::string& getFlavour() const
{
return m_flavour;
}
void print() const
{
std::cout << "Pizza flavour: " << m_flavour << '\n';
}
};
int main()
{
Pizza pizza {};
pizza.setFlavour("Pepperoni");
pizza.print();
}
// Terminal output:
Pizza flavour: Pepperoni
This works but notice how print() belongs in the class.
If the class is updated, print() must be updated since it interacts with the member variable. If we want to adjust print(), we need to change the class.
The issue is not that print() accesses m_flavour. Member functions are allowed to access private data. The issue is whether printing is truly part of the core responsibility of Pizza.
Second version: use the public interface internally
We enforce encapsulation by making print() use the public interface:
#include <iostream>
#include <string>
#include <string_view>
class Pizza {
private:
std::string m_flavour { "Margherita" };
public:
void setFlavour(std::string_view flavour)
{
m_flavour = flavour;
}
const std::string& getFlavour() const
{
return m_flavour;
}
// print() uses the public interface
void print() const
{
std::cout << "Pizza flavour: " << getFlavour() << '\n';
}
};
int main()
{
Pizza pizza {};
pizza.setFlavour("Hawaiian");
pizza.print();
}
// Terminal output:
Pizza flavour: Hawaiian
However, print() is still a member function. The Pizza class is still responsible for formatting output.
That may be unnecessary.
Third version: move printing outside the class
If printing does not need private access, it can be a non-member function:
#include <iostream>
#include <string>
#include <string_view>
class Pizza {
private:
std::string m_flavour { "Margherita" };
public:
void setFlavour(std::string_view flavour){
m_flavour = flavour;
}
const std::string& getFlavour() const{
return m_flavour;
}
};
void print(const Pizza& pizza){
std::cout << "Pizza flavour: " << pizza.getFlavour() << '\n';
}
int main(){
Pizza pizza {};
pizza.setFlavour("Supreme");
print(pizza);
}
Terminal output:
Pizza flavour: Supreme
This is the cleaner design.
The Pizza class now owns the data and exposes the public operations needed to interact with it:
setFlavour()
getFlavour()
The print() function lives outside the class because it does not need privileged access.
It uses the public interface:
pizza.getFlavour()
That means printing can change without changing the class itself.
Conclusion
Encapsulation is more than private. It’s about determining the responsibilities of the class and designing the public interface. The final version of the Pizza example conveyed clear responsibilities to each object:
Pizzamanages pizza state.print()formats a pizza for output.