This lesson was translated automatically – it may contain errors.
Introduction
We have talked about hiding fields more than once already: when we looked at why only the
drive method changes the mileage, and when we looked at what properties are for. Every time
it was said in passing, with a note saying «more on this later». This is the later: why the
insides of an object are closed off, and what mechanisms different languages give for it.
Encapsulation is a mechanism that unites data and the methods working on that data into a single unit (a class) and hides the internal implementation from outside interference.
Put simply, encapsulation is:
- hiding the implementation – only what is needed is available;
- protecting the data from incorrect use;
- controlling access to the internal state of the object.
What languages close the insides with
Encapsulation is a decision made by the designer, not a feature of the language. The language only gives a tool to write that decision down, and those tools are built in fundamentally different ways. There are exactly three approaches.
Approach one: access modifiers
The most widespread option: every member of a class carries a mark saying who has access to it, and the compiler enforces that mark. C#, C++, Java and Kotlin work this way – and even their sets do not match.
| Level | C# | C++ | Java | Kotlin |
|---|---|---|---|---|
| Inside the class only | private |
private |
private |
private |
| The class and its subclasses | protected |
protected |
protected |
protected |
| Own assembly or module | internal |
– | – | internal |
| Own package | – | – | the default | – |
| Everyone | public |
public |
public |
public (the default) |
A few non-obvious differences people trip over when moving between languages:
- The default differs everywhere. In C# a member without a modifier is
private, in Kotlin it ispublic, in Java it is visible to the whole package, and in C++ it depends on whetherclassorstructwas declared. - «Own module» means different things.
internalin C# is an assembly,internalin Kotlin is a build module, and the Java equivalent is the package – which has no keyword at all. - C++ has
friend– the ability to grant access to private members to one specific class or function. None of the other languages has such a targeted grant. protectedin Java is wider than it looks: it opens access not only to subclasses but to the whole package.
Approach two: visibility by name
Go does without modifiers entirely. Whether a name is visible is decided by the case of its first letter: uppercase – available from other packages, lowercase – not.
type Account struct {
Owner string // visible outside the package
balance float64 // visible only inside the package
}
The difference from the first approach is not cosmetic. The unit of hiding here is the
package, not the class: any code inside the package sees balance of any Account. There
is simply no «closed even to the neighbouring type» in Go – a deliberate choice of the
language authors, not an oversight.
Approach three: convention
Python has no protection mechanism at all. What it has is a convention: a name with a single leading underscore means «this is internal, do not touch it from outside».
class Account:
def __init__(self):
self._balance = 0 # convention: do not touch
self.__secret = 0 # name mangling, still not protection
Two underscores turn on name mangling: the attribute becomes available as
_Account__secret. That protects against accidental name clashes in a hierarchy, not against
access – the value is still one line away.
The language deliberately trusts the programmer here. The community puts it as «we are all consenting adults».
A special case: TypeScript
TypeScript is interesting because it carries two mechanisms from different approaches at once.
class Account {
private balance = 0; // checked by the compiler, ordinary field at runtime
#secret = 0; // private in the JavaScript engine itself
}
private exists only at compile time: once built into JavaScript the mark is gone and the
field is available like any other. #, on the other hand, is a real private field – reaching
for it from outside the class neither compiles nor works at runtime.
What follows from this
The mechanisms differ, but none of them protects against a deliberate bypass. Reflection in C# and Java reaches private fields, in C++ pointer casts help, and in Python it is enough to know the name mangling rule.
Hence the right way to think about encapsulation: an access mark is addressed not to an attacker but to a colleague and to yourself six months from now. It says «do not rely on this, it may change» – and in that sense it works the same in C# with its five modifiers and in Python, which has none.
Practical examples of encapsulation
❌ The bad approach (without encapsulation)
public class BankAccount
{
public decimal balance; // Public field – dangerous!
public void Deposit(decimal amount)
{
balance += amount;
}
}
// Usage (the problems):
BankAccount account = new BankAccount();
account.balance = 1000; // Can change the balance directly
account.balance = -500; // Can set a negative value – a logic error!
class BankAccount {
public:
double balance = 0; // Public field – dangerous!
void Deposit(double amount) { balance += amount; }
};
// Usage (the problems):
BankAccount account;
account.balance = 1000; // Can change the balance directly
account.balance = -500; // Can set a negative value – a logic error!
type BankAccount struct {
Balance float64 // Exported field – dangerous!
}
func (a *BankAccount) Deposit(amount float64) {
a.Balance += amount
}
// Usage (the problems):
account := &BankAccount{}
account.Balance = 1000 // Can change the balance directly
account.Balance = -500 // Can set a negative value – a logic error!
class BankAccount:
def __init__(self):
self.balance = 0 # Public attribute – dangerous!
def deposit(self, amount):
self.balance += amount
# Usage (the problems):
account = BankAccount()
account.balance = 1000 # Can change the balance directly
account.balance = -500 # Can set a negative value – a logic error!
class BankAccount {
balance = 0; // Public field – dangerous!
deposit(amount: number): void {
this.balance += amount;
}
}
// Usage (the problems):
const account = new BankAccount();
account.balance = 1000; // Can change the balance directly
account.balance = -500; // Can set a negative value – a logic error!
public class BankAccount {
public BigDecimal balance = BigDecimal.ZERO; // Public field – dangerous!
public void deposit(BigDecimal amount) {
balance = balance.add(amount);
}
}
// Usage (the problems):
BankAccount account = new BankAccount();
account.balance = new BigDecimal("1000"); // Can change the balance directly
account.balance = new BigDecimal("-500"); // A negative value – a logic error!
class BankAccount {
var balance: Double = 0.0 // Public property – dangerous!
fun deposit(amount: Double) {
balance += amount
}
}
// Usage (the problems):
val account = BankAccount()
account.balance = 1000.0 // Can change the balance directly
account.balance = -500.0 // Can set a negative value – a logic error!
✅ The good approach (with encapsulation)
Example 1: a basic bank account
public class BankAccount
{
private decimal balance; // Private field – protected from direct access
public void Deposit(decimal amount)
{
if (amount > 0)
balance += amount;
else
throw new ArgumentException("The amount must be positive");
}
public void Withdraw(decimal amount)
{
if (amount > 0 && amount <= balance)
balance -= amount;
else
throw new InvalidOperationException("Not enough funds or an invalid amount");
}
// Method that returns the balance (read only)
public decimal GetBalance()
{
return balance;
}
}
// Usage:
BankAccount account = new BankAccount();
account.Deposit(1000);
account.Withdraw(500);
// account.balance = 1000; // Compile error – the field is private!
Console.WriteLine(account.GetBalance()); // 500
#include <stdexcept>
class BankAccount {
public:
void Deposit(double amount) {
if (amount <= 0) {
throw std::invalid_argument("The amount must be positive");
}
balance_ += amount;
}
void Withdraw(double amount) {
if (amount <= 0 || amount > balance_) {
throw std::logic_error("Not enough funds or an invalid amount");
}
balance_ -= amount;
}
// Method that returns the balance (read only)
double GetBalance() const { return balance_; }
private:
double balance_ = 0; // Private field – protected from direct access
};
type BankAccount struct {
balance float64 // Unexported field – unavailable outside the package
}
func (a *BankAccount) Deposit(amount float64) error {
if amount <= 0 {
return errors.New("the amount must be positive")
}
a.balance += amount
return nil
}
func (a *BankAccount) Withdraw(amount float64) error {
if amount <= 0 || amount > a.balance {
return errors.New("not enough funds or an invalid amount")
}
a.balance -= amount
return nil
}
// Method that returns the balance (read only)
func (a *BankAccount) Balance() float64 {
return a.balance
}
class BankAccount:
def __init__(self):
self.__balance = 0 # Two underscores turn on name mangling
def deposit(self, amount):
if amount <= 0:
raise ValueError("The amount must be positive")
self.__balance += amount
def withdraw(self, amount):
if amount <= 0 or amount > self.__balance:
raise ValueError("Not enough funds or an invalid amount")
self.__balance -= amount
# Method that returns the balance (read only)
def get_balance(self):
return self.__balance
# Usage:
account = BankAccount()
account.deposit(1000)
account.withdraw(500)
# account.__balance = 1000 # Will not change the internal field
print(account.get_balance()) # 500
class BankAccount {
#balance = 0; // Private field – protected by the language itself
deposit(amount: number): void {
if (amount <= 0) {
throw new Error("The amount must be positive");
}
this.#balance += amount;
}
withdraw(amount: number): void {
if (amount <= 0 || amount > this.#balance) {
throw new Error("Not enough funds or an invalid amount");
}
this.#balance -= amount;
}
// Method that returns the balance (read only)
getBalance(): number {
return this.#balance;
}
}
const account = new BankAccount();
account.deposit(1000);
account.withdraw(500);
console.log(account.getBalance()); // 500
public class BankAccount {
private BigDecimal balance = BigDecimal.ZERO; // Private field – protected
public void deposit(BigDecimal amount) {
if (amount.signum() <= 0) {
throw new IllegalArgumentException("The amount must be positive");
}
balance = balance.add(amount);
}
public void withdraw(BigDecimal amount) {
if (amount.signum() <= 0 || amount.compareTo(balance) > 0) {
throw new IllegalStateException("Not enough funds or an invalid amount");
}
balance = balance.subtract(amount);
}
// Method that returns the balance (read only)
public BigDecimal getBalance() {
return balance;
}
}
class BankAccount {
// Property with a private setter: anyone can read, only the class can change
var balance: Double = 0.0
private set
fun deposit(amount: Double) {
require(amount > 0) { "The amount must be positive" }
balance += amount
}
fun withdraw(amount: Double) {
check(amount > 0 && amount <= balance) { "Not enough funds or an invalid amount" }
balance -= amount
}
}
val account = BankAccount()
account.deposit(1000.0)
account.withdraw(500.0)
// account.balance = 1000.0 // Compile error – the setter is private
println(account.balance) // 500.0
Example 2: using properties
Basic properties with validation
public class Person
{
private string name;
private int age;
// Property with validation
public string Name
{
get { return name; }
set
{
if (!string.IsNullOrWhiteSpace(value))
name = value;
else
throw new ArgumentException("The name cannot be empty");
}
}
public int Age
{
get { return age; }
set
{
if (value >= 0 && value <= 150)
age = value;
else
throw new ArgumentException("Invalid age");
}
}
}
// Usage:
Person person = new Person();
person.Name = "John"; // the setter runs
person.Age = 25; // the setter runs
Console.WriteLine(person.Name); // the getter runs
C++ has no properties: the field is closed off and the check goes into a setter method like set_age. From outside it looks like a call, not an assignment.
Go has no properties: the field is written in lowercase and the methods Age and SetAge sit next to it, the latter returning an error instead of throwing.
class Person:
def __init__(self):
self._name = ""
self._age = 0
# Property with validation
@property
def name(self):
return self._name
@name.setter
def name(self, value):
if not value or not value.strip():
raise ValueError("The name cannot be empty")
self._name = value
@property
def age(self):
return self._age
@age.setter
def age(self, value):
if not 0 <= value <= 150:
raise ValueError("Invalid age")
self._age = value
# Usage:
person = Person()
person.name = "John" # the setter runs
person.age = 25 # the setter runs
print(person.name) # the getter runs
class Person {
#name = "";
#age = 0;
// Accessors play the role of a property with validation
get name(): string {
return this.#name;
}
set name(value: string) {
if (!value.trim()) {
throw new Error("The name cannot be empty");
}
this.#name = value;
}
get age(): number {
return this.#age;
}
set age(value: number) {
if (value < 0 || value > 150) {
throw new Error("Invalid age");
}
this.#age = value;
}
}
const person = new Person();
person.name = "John"; // the setter runs
person.age = 25; // the setter runs
console.log(person.name); // the getter runs
Java has no properties: getAge and setAge play that role by convention, and all the validation lives in the setter.
class Person {
private var _name = ""
private var _age = 0
// Property with validation: the field is kept separately
var name: String
get() = _name
set(value) {
require(value.isNotBlank()) { "The name cannot be empty" }
_name = value
}
var age: Int
get() = _age
set(value) {
require(value in 0..150) { "Invalid age" }
_age = value
}
}
val person = Person()
person.name = "John" // the setter runs
person.age = 25 // the setter runs
println(person.name) // the getter runs
Automatic properties
public class Product
{
// The compiler creates the private field itself
public string Name { get; set; }
public decimal Price { get; set; }
// Read-only property
public string ProductCode { get; }
public Product(string code)
{
ProductCode = code; // Can only be set in the constructor
}
}
C++ has no automatic properties: the field is declared explicitly and methods give access to it.
Go has no properties at all: there are fields and methods, and nothing is generated for you.
class Product:
def __init__(self, code):
# Ordinary attributes – the counterpart of automatic properties
self.name = ""
self.price = 0
self.__product_code = code # Can only be set in the constructor
# Read-only property
@property
def product_code(self):
return self.__product_code
class Product {
name = "";
price = 0;
// readonly – the value can only be set in the constructor
constructor(public readonly productCode: string) {}
}
Java has no properties: getters and setters are written by hand or generated by the IDE.
class Product(
// val – read only, set at creation
val productCode: String,
) {
// var – ordinary mutable properties, the field is created for you
var name: String = ""
var price: Double = 0.0
}
Example 3: the kinds of properties
public class Temperature
{
private double celsius;
// Read-only property
public double Celsius
{
get { return celsius; }
}
// Computed property
public double Fahrenheit
{
get { return celsius * 9 / 5 + 32; }
}
// Property with a private set
public string MeasurementDate { get; private set; }
public void SetTemperature(double celsius, string date)
{
if (celsius >= -273.15) // Absolute zero
{
this.celsius = celsius;
MeasurementDate = date;
}
else
throw new ArgumentException("The temperature cannot be below absolute zero");
}
}
// Usage:
Temperature temp = new Temperature();
temp.SetTemperature(25, "2024-01-15");
Console.WriteLine($"{temp.Celsius}°C = {temp.Fahrenheit}°F");
// temp.Celsius = 30; // Error – the property is read only
class Temperature {
public:
// Read only: there is no setter
double celsius() const { return celsius_; }
// Computed value
double fahrenheit() const { return celsius_ * 9 / 5 + 32; }
const std::string& measurement_date() const { return measurement_date_; }
void SetTemperature(double celsius, const std::string& date) {
if (celsius < -273.15) { // Absolute zero
throw std::invalid_argument("The temperature cannot be below absolute zero");
}
celsius_ = celsius;
measurement_date_ = date;
}
private:
double celsius_ = 0;
std::string measurement_date_;
};
type Temperature struct {
celsius float64
measurementDate string
}
// Read only
func (t *Temperature) Celsius() float64 { return t.celsius }
// Computed value
func (t *Temperature) Fahrenheit() float64 { return t.celsius*9/5 + 32 }
func (t *Temperature) MeasurementDate() string { return t.measurementDate }
func (t *Temperature) SetTemperature(celsius float64, date string) error {
if celsius < -273.15 { // Absolute zero
return errors.New("the temperature cannot be below absolute zero")
}
t.celsius = celsius
t.measurementDate = date
return nil
}
class Temperature:
def __init__(self):
self.__celsius = 0
self.__measurement_date = ""
# Read-only property
@property
def celsius(self):
return self.__celsius
# Computed property
@property
def fahrenheit(self):
return self.__celsius * 9 / 5 + 32
@property
def measurement_date(self):
return self.__measurement_date
def set_temperature(self, celsius, date):
if celsius < -273.15: # Absolute zero
raise ValueError("The temperature cannot be below absolute zero")
self.__celsius = celsius
self.__measurement_date = date
# Usage:
temp = Temperature()
temp.set_temperature(25, "2024-01-15")
print(f"{temp.celsius}°C = {temp.fahrenheit}°F")
# temp.celsius = 30 # Error – the property is read only
class Temperature {
#celsius = 0;
#measurementDate = "";
// Read only: there is no setter
get celsius(): number {
return this.#celsius;
}
// Computed property
get fahrenheit(): number {
return (this.#celsius * 9) / 5 + 32;
}
get measurementDate(): string {
return this.#measurementDate;
}
setTemperature(celsius: number, date: string): void {
if (celsius < -273.15) {
throw new Error("The temperature cannot be below absolute zero");
}
this.#celsius = celsius;
this.#measurementDate = date;
}
}
const temp = new Temperature();
temp.setTemperature(25, "2024-01-15");
console.log(`${temp.celsius}°C = ${temp.fahrenheit}°F`);
// temp.celsius = 30; // Error – there is no setter
public class Temperature {
private double celsius;
private String measurementDate;
// Read only – there is no setter
public double getCelsius() {
return celsius;
}
// Computed value
public double getFahrenheit() {
return celsius * 9 / 5 + 32;
}
public String getMeasurementDate() {
return measurementDate;
}
public void setTemperature(double celsius, String date) {
if (celsius < -273.15) { // Absolute zero
throw new IllegalArgumentException("The temperature cannot be below absolute zero");
}
this.celsius = celsius;
measurementDate = date;
}
}
class Temperature {
// Property with a private setter
var celsius: Double = 0.0
private set
var measurementDate: String = ""
private set
// Computed property: there is no field under it
val fahrenheit: Double get() = celsius * 9 / 5 + 32
fun setTemperature(celsius: Double, date: String) {
require(celsius >= -273.15) { "The temperature cannot be below absolute zero" }
this.celsius = celsius
measurementDate = date
}
}
val temp = Temperature()
temp.setTemperature(25.0, "2024-01-15")
println("${temp.celsius}°C = ${temp.fahrenheit}°F")
// temp.celsius = 30.0 // Error – the setter is private
Example 4: full encapsulation with business logic
public class Order
{
private List<OrderItem> items = new List<OrderItem>();
private decimal discount;
// Encapsulated collection (read only)
public IReadOnlyList<OrderItem> Items => items.AsReadOnly();
public decimal Subtotal => items.Sum(item => item.TotalPrice);
public decimal Discount
{
get { return discount; }
private set
{
if (value >= 0 && value <= 0.5m) // 50% discount at most
discount = value;
}
}
public decimal Total => Subtotal * (1 - Discount);
// Public methods for working with the order
public void AddItem(Product product, int quantity)
{
if (quantity <= 0)
throw new ArgumentException("The quantity must be positive");
var existingItem = items.FirstOrDefault(i => i.ProductId == product.Id);
if (existingItem != null)
existingItem.Quantity += quantity;
else
items.Add(new OrderItem(product, quantity));
}
public void RemoveItem(int productId, int quantity)
{
var item = items.FirstOrDefault(i => i.ProductId == productId);
if (item != null)
{
if (item.Quantity <= quantity)
items.Remove(item);
else
item.Quantity -= quantity;
}
}
public void ApplyDiscount(decimal discountPercent)
{
Discount = discountPercent / 100;
}
}
public class OrderItem
{
public int ProductId { get; }
public string ProductName { get; }
public decimal UnitPrice { get; }
public int Quantity { get; set; }
public decimal TotalPrice => UnitPrice * Quantity;
public OrderItem(Product product, int quantity)
{
ProductId = product.Id;
ProductName = product.Name;
UnitPrice = product.Price;
Quantity = quantity;
}
}
class Order {
public:
// Encapsulated collection: a const reference goes out
const std::vector<OrderItem>& items() const { return items_; }
double Subtotal() const {
double total = 0;
for (const auto& item : items_) {
total += item.TotalPrice();
}
return total;
}
double discount() const { return discount_; }
double Total() const { return Subtotal() * (1 - discount_); }
void AddItem(const Product& product, int quantity) {
if (quantity <= 0) {
throw std::invalid_argument("The quantity must be positive");
}
for (auto& item : items_) {
if (item.product_id() == product.id) {
item.set_quantity(item.quantity() + quantity);
return;
}
}
items_.emplace_back(product, quantity);
}
void ApplyDiscount(double discount_percent) {
const double value = discount_percent / 100;
if (value >= 0 && value <= 0.5) { // 50% discount at most
discount_ = value;
}
}
private:
std::vector<OrderItem> items_;
double discount_ = 0;
};
class OrderItem {
public:
OrderItem(const Product& product, int quantity)
: product_id_(product.id), product_name_(product.name),
unit_price_(product.price), quantity_(quantity) {}
int product_id() const { return product_id_; }
int quantity() const { return quantity_; }
void set_quantity(int value) { quantity_ = value; }
double TotalPrice() const { return unit_price_ * quantity_; }
private:
int product_id_;
std::string product_name_;
double unit_price_;
int quantity_;
};
type Order struct {
items []OrderItem
discount float64
}
// A copy of the slice goes out: the internal list cannot be changed from outside
func (o *Order) Items() []OrderItem {
return append([]OrderItem(nil), o.items...)
}
func (o *Order) Subtotal() float64 {
total := 0.0
for _, item := range o.items {
total += item.TotalPrice()
}
return total
}
func (o *Order) Discount() float64 { return o.discount }
func (o *Order) Total() float64 { return o.Subtotal() * (1 - o.discount) }
func (o *Order) AddItem(product Product, quantity int) error {
if quantity <= 0 {
return errors.New("the quantity must be positive")
}
for i := range o.items {
if o.items[i].ProductID == product.ID {
o.items[i].Quantity += quantity
return nil
}
}
o.items = append(o.items, NewOrderItem(product, quantity))
return nil
}
func (o *Order) ApplyDiscount(percent float64) {
value := percent / 100
if value >= 0 && value <= 0.5 { // 50% discount at most
o.discount = value
}
}
type OrderItem struct {
ProductID int
ProductName string
UnitPrice float64
Quantity int
}
func NewOrderItem(product Product, quantity int) OrderItem {
return OrderItem{
ProductID: product.ID,
ProductName: product.Name,
UnitPrice: product.Price,
Quantity: quantity,
}
}
func (i OrderItem) TotalPrice() float64 { return i.UnitPrice * float64(i.Quantity) }
class Order:
def __init__(self):
self.__items: list[OrderItem] = []
self.__discount = 0
# Encapsulated collection: a tuple goes out, not the list itself
@property
def items(self):
return tuple(self.__items)
@property
def subtotal(self):
return sum(item.total_price for item in self.__items)
@property
def discount(self):
return self.__discount
@property
def total(self):
return self.subtotal * (1 - self.__discount)
def add_item(self, product, quantity):
if quantity <= 0:
raise ValueError("The quantity must be positive")
existing = next((i for i in self.__items if i.product_id == product.id), None)
if existing:
existing.quantity += quantity
else:
self.__items.append(OrderItem(product, quantity))
def remove_item(self, product_id, quantity):
item = next((i for i in self.__items if i.product_id == product_id), None)
if item is None:
return
if item.quantity <= quantity:
self.__items.remove(item)
else:
item.quantity -= quantity
def apply_discount(self, discount_percent):
value = discount_percent / 100
if 0 <= value <= 0.5: # 50% discount at most
self.__discount = value
class OrderItem:
def __init__(self, product, quantity):
self.product_id = product.id
self.product_name = product.name
self.unit_price = product.price
self.quantity = quantity
@property
def total_price(self):
return self.unit_price * self.quantity
class Order {
#items: OrderItem[] = [];
#discount = 0;
// Encapsulated collection (read only)
get items(): readonly OrderItem[] {
return this.#items;
}
get subtotal(): number {
return this.#items.reduce((sum, item) => sum + item.totalPrice, 0);
}
get discount(): number {
return this.#discount;
}
get total(): number {
return this.subtotal * (1 - this.#discount);
}
addItem(product: Product, quantity: number): void {
if (quantity <= 0) {
throw new Error("The quantity must be positive");
}
const existing = this.#items.find((i) => i.productId === product.id);
if (existing) {
existing.quantity += quantity;
} else {
this.#items.push(new OrderItem(product, quantity));
}
}
removeItem(productId: number, quantity: number): void {
const index = this.#items.findIndex((i) => i.productId === productId);
if (index < 0) return;
const item = this.#items[index];
if (item.quantity <= quantity) {
this.#items.splice(index, 1);
} else {
item.quantity -= quantity;
}
}
applyDiscount(discountPercent: number): void {
const value = discountPercent / 100;
if (value >= 0 && value <= 0.5) {
this.#discount = value;
}
}
}
class OrderItem {
readonly productId: number;
readonly productName: string;
readonly unitPrice: number;
constructor(product: Product, public quantity: number) {
this.productId = product.id;
this.productName = product.name;
this.unitPrice = product.price;
}
get totalPrice(): number {
return this.unitPrice * this.quantity;
}
}
public class Order {
private final List<OrderItem> items = new ArrayList<>();
private BigDecimal discount = BigDecimal.ZERO;
// Encapsulated collection (read only)
public List<OrderItem> getItems() {
return Collections.unmodifiableList(items);
}
public BigDecimal getSubtotal() {
return items.stream()
.map(OrderItem::getTotalPrice)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
public BigDecimal getDiscount() {
return discount;
}
public BigDecimal getTotal() {
return getSubtotal().multiply(BigDecimal.ONE.subtract(discount));
}
public void addItem(Product product, int quantity) {
if (quantity <= 0) {
throw new IllegalArgumentException("The quantity must be positive");
}
items.stream()
.filter(i -> i.getProductId() == product.getId())
.findFirst()
.ifPresentOrElse(
i -> i.setQuantity(i.getQuantity() + quantity),
() -> items.add(new OrderItem(product, quantity)));
}
public void applyDiscount(BigDecimal discountPercent) {
BigDecimal value = discountPercent.divide(new BigDecimal("100"));
if (value.signum() >= 0 && value.compareTo(new BigDecimal("0.5")) <= 0) {
discount = value;
}
}
}
public class OrderItem {
private final int productId;
private final String productName;
private final BigDecimal unitPrice;
private int quantity;
public OrderItem(Product product, int quantity) {
productId = product.getId();
productName = product.getName();
unitPrice = product.getPrice();
this.quantity = quantity;
}
public int getProductId() { return productId; }
public int getQuantity() { return quantity; }
public void setQuantity(int quantity) { this.quantity = quantity; }
public BigDecimal getTotalPrice() {
return unitPrice.multiply(BigDecimal.valueOf(quantity));
}
}
class Order {
private val _items = mutableListOf<OrderItem>()
// Encapsulated collection: an immutable list goes out
val items: List<OrderItem> get() = _items
var discount: Double = 0.0
private set
val subtotal: Double get() = _items.sumOf { it.totalPrice }
val total: Double get() = subtotal * (1 - discount)
fun addItem(product: Product, quantity: Int) {
require(quantity > 0) { "The quantity must be positive" }
val existing = _items.find { it.productId == product.id }
if (existing != null) {
existing.quantity += quantity
} else {
_items += OrderItem(product, quantity)
}
}
fun removeItem(productId: Int, quantity: Int) {
val item = _items.find { it.productId == productId } ?: return
if (item.quantity <= quantity) _items -= item else item.quantity -= quantity
}
fun applyDiscount(discountPercent: Double) {
val value = discountPercent / 100
if (value in 0.0..0.5) discount = value // 50% discount at most
}
}
class OrderItem(product: Product, var quantity: Int) {
val productId = product.id
val productName = product.name
val unitPrice = product.price
val totalPrice: Double get() = unitPrice * quantity
}
Example 5: encapsulation through methods
public class SecuritySystem
{
private string alarmStatus = "Disarmed";
private bool isDoorLocked = true;
private DateTime lastActivity;
// Internal private method
private void UpdateActivity()
{
lastActivity = DateTime.Now;
LogActivity();
}
private void LogActivity()
{
Console.WriteLine($"Activity at {lastActivity}: Alarm {alarmStatus}, Door {(isDoorLocked ? "Locked" : "Unlocked")}");
}
// The public interface
public void ArmAlarm()
{
if (isDoorLocked)
{
alarmStatus = "Armed";
UpdateActivity();
}
else
{
throw new InvalidOperationException("The alarm cannot be armed while a door is open");
}
}
public void DisarmAlarm(string code)
{
if (ValidateCode(code))
{
alarmStatus = "Disarmed";
UpdateActivity();
}
}
public void LockDoor()
{
isDoorLocked = true;
UpdateActivity();
}
public void UnlockDoor(string code)
{
if (ValidateCode(code))
{
isDoorLocked = false;
UpdateActivity();
}
}
public string GetStatus()
{
return $"Alarm: {alarmStatus}, Door: {(isDoorLocked ? "Locked" : "Unlocked")}";
}
private bool ValidateCode(string code)
{
return code == "1234"; // Simplified check
}
}
#include <chrono>
class SecuritySystem {
public:
// The public interface
void ArmAlarm() {
if (!is_door_locked_) {
throw std::logic_error("The alarm cannot be armed while a door is open");
}
alarm_status_ = "Armed";
UpdateActivity();
}
void DisarmAlarm(const std::string& code) {
if (ValidateCode(code)) {
alarm_status_ = "Disarmed";
UpdateActivity();
}
}
void LockDoor() {
is_door_locked_ = true;
UpdateActivity();
}
void UnlockDoor(const std::string& code) {
if (ValidateCode(code)) {
is_door_locked_ = false;
UpdateActivity();
}
}
std::string GetStatus() const {
return "Alarm: " + alarm_status_ + ", Door: " + DoorState();
}
private:
std::string alarm_status_ = "Disarmed";
bool is_door_locked_ = true;
std::chrono::system_clock::time_point last_activity_;
std::string DoorState() const { return is_door_locked_ ? "Locked" : "Unlocked"; }
// Internal private methods
void UpdateActivity() {
last_activity_ = std::chrono::system_clock::now();
LogActivity();
}
void LogActivity() const {
std::cout << "Activity: Alarm " << alarm_status_
<< ", Door " << DoorState() << std::endl;
}
bool ValidateCode(const std::string& code) const {
return code == "1234"; // Simplified check
}
};
type SecuritySystem struct {
alarmStatus string
isDoorLocked bool
lastActivity time.Time
}
func NewSecuritySystem() *SecuritySystem {
return &SecuritySystem{alarmStatus: "Disarmed", isDoorLocked: true}
}
// Internal methods start with a lowercase letter and are invisible outside the package
func (s *SecuritySystem) updateActivity() {
s.lastActivity = time.Now()
s.logActivity()
}
func (s *SecuritySystem) logActivity() {
fmt.Printf("Activity at %s: Alarm %s, Door %s\n",
s.lastActivity.Format(time.RFC3339), s.alarmStatus, s.doorState())
}
func (s *SecuritySystem) doorState() string {
if s.isDoorLocked {
return "Locked"
}
return "Unlocked"
}
func (s *SecuritySystem) validateCode(code string) bool {
return code == "1234" // Simplified check
}
// The public interface
func (s *SecuritySystem) ArmAlarm() error {
if !s.isDoorLocked {
return errors.New("the alarm cannot be armed while a door is open")
}
s.alarmStatus = "Armed"
s.updateActivity()
return nil
}
func (s *SecuritySystem) DisarmAlarm(code string) {
if s.validateCode(code) {
s.alarmStatus = "Disarmed"
s.updateActivity()
}
}
func (s *SecuritySystem) LockDoor() {
s.isDoorLocked = true
s.updateActivity()
}
func (s *SecuritySystem) UnlockDoor(code string) {
if s.validateCode(code) {
s.isDoorLocked = false
s.updateActivity()
}
}
func (s *SecuritySystem) Status() string {
return fmt.Sprintf("Alarm: %s, Door: %s", s.alarmStatus, s.doorState())
}
from datetime import datetime
class SecuritySystem:
def __init__(self):
self.__alarm_status = "Disarmed"
self.__is_door_locked = True
self.__last_activity = None
# Internal private method
def __update_activity(self):
self.__last_activity = datetime.now()
self.__log_activity()
def __log_activity(self):
door = "Locked" if self.__is_door_locked else "Unlocked"
print(f"Activity at {self.__last_activity}: Alarm {self.__alarm_status}, Door {door}")
def __validate_code(self, code):
return code == "1234" # Simplified check
# The public interface
def arm_alarm(self):
if not self.__is_door_locked:
raise RuntimeError("The alarm cannot be armed while a door is open")
self.__alarm_status = "Armed"
self.__update_activity()
def disarm_alarm(self, code):
if self.__validate_code(code):
self.__alarm_status = "Disarmed"
self.__update_activity()
def lock_door(self):
self.__is_door_locked = True
self.__update_activity()
def unlock_door(self, code):
if self.__validate_code(code):
self.__is_door_locked = False
self.__update_activity()
def get_status(self):
door = "Locked" if self.__is_door_locked else "Unlocked"
return f"Alarm: {self.__alarm_status}, Door: {door}"
class SecuritySystem {
#alarmStatus = "Disarmed";
#isDoorLocked = true;
#lastActivity: Date | null = null;
// Internal private methods
#updateActivity(): void {
this.#lastActivity = new Date();
this.#logActivity();
}
#logActivity(): void {
const door = this.#isDoorLocked ? "Locked" : "Unlocked";
console.log(`Activity at ${this.#lastActivity}: Alarm ${this.#alarmStatus}, Door ${door}`);
}
#validateCode(code: string): boolean {
return code === "1234"; // Simplified check
}
// The public interface
armAlarm(): void {
if (!this.#isDoorLocked) {
throw new Error("The alarm cannot be armed while a door is open");
}
this.#alarmStatus = "Armed";
this.#updateActivity();
}
disarmAlarm(code: string): void {
if (this.#validateCode(code)) {
this.#alarmStatus = "Disarmed";
this.#updateActivity();
}
}
lockDoor(): void {
this.#isDoorLocked = true;
this.#updateActivity();
}
unlockDoor(code: string): void {
if (this.#validateCode(code)) {
this.#isDoorLocked = false;
this.#updateActivity();
}
}
getStatus(): string {
const door = this.#isDoorLocked ? "Locked" : "Unlocked";
return `Alarm: ${this.#alarmStatus}, Door: ${door}`;
}
}
public class SecuritySystem {
private String alarmStatus = "Disarmed";
private boolean isDoorLocked = true;
private LocalDateTime lastActivity;
// Internal private method
private void updateActivity() {
lastActivity = LocalDateTime.now();
logActivity();
}
private void logActivity() {
System.out.printf("Activity at %s: Alarm %s, Door %s%n",
lastActivity, alarmStatus, isDoorLocked ? "Locked" : "Unlocked");
}
private boolean validateCode(String code) {
return code.equals("1234"); // Simplified check
}
// The public interface
public void armAlarm() {
if (!isDoorLocked) {
throw new IllegalStateException("The alarm cannot be armed while a door is open");
}
alarmStatus = "Armed";
updateActivity();
}
public void disarmAlarm(String code) {
if (validateCode(code)) {
alarmStatus = "Disarmed";
updateActivity();
}
}
public void lockDoor() {
isDoorLocked = true;
updateActivity();
}
public void unlockDoor(String code) {
if (validateCode(code)) {
isDoorLocked = false;
updateActivity();
}
}
public String getStatus() {
return "Alarm: " + alarmStatus + ", Door: " + (isDoorLocked ? "Locked" : "Unlocked");
}
}
class SecuritySystem {
private var alarmStatus = "Disarmed"
private var isDoorLocked = true
private var lastActivity: LocalDateTime? = null
private val doorState: String get() = if (isDoorLocked) "Locked" else "Unlocked"
// Internal private methods
private fun updateActivity() {
lastActivity = LocalDateTime.now()
logActivity()
}
private fun logActivity() {
println("Activity at $lastActivity: Alarm $alarmStatus, Door $doorState")
}
private fun validateCode(code: String) = code == "1234" // Simplified check
// The public interface
fun armAlarm() {
check(isDoorLocked) { "The alarm cannot be armed while a door is open" }
alarmStatus = "Armed"
updateActivity()
}
fun disarmAlarm(code: String) {
if (validateCode(code)) {
alarmStatus = "Disarmed"
updateActivity()
}
}
fun lockDoor() {
isDoorLocked = true
updateActivity()
}
fun unlockDoor(code: String) {
if (validateCode(code)) {
isDoorLocked = false
updateActivity()
}
}
fun getStatus() = "Alarm: $alarmStatus, Door: $doorState"
}
What encapsulation gives you
| Benefit | What it means |
|---|---|
| Control over the data | Incorrect states of the object are prevented |
| Freedom to change | The internals can be rewritten without touching the code that uses the class |
| Safety | Critical data is protected from accidental modification |
| Simpler use | A user of the class works with a small public interface |
| Integrity | The object is guaranteed to always be in a valid state |
Practical advice
- Start with private fields – widen the access level only when you have to.
- Give access through properties or methods, not public fields – that leaves room for future change.
- Keep the public interface small – the fewer methods and properties are visible from outside, the easier the code is to maintain and change.
- Add validation where values come in: property setters and method parameters.
- Use methods for operations that change the state of the object.
In summary
Encapsulation is a decision made by the designer, not a feature of the language. Keywords, the case of the first letter, an underscore in a name – all of these are just ways to write down one and the same decision: what belongs to the construction of the object and what belongs to its promises to the outside code.
Hence the practical payoff. As long as the border is drawn, the internals can be rewritten without touching anyone who uses the object; an incorrect state can appear in one place instead of anywhere in the program; and somebody else’s class can be read through a short list of public methods instead of all of its code.
Remember: a good class is like a black box – the outside world knows what it does, but not how it does it.