OOP principles
In short: Object-oriented programming rests on four principles: encapsulation, inheritance, polymorphism and abstraction. They guard the data, write what is shared once and replace the if-chains on the kind of object.
Problem
Payroll without principles
At the end of the month the company pays its people. The program was written in a hurry: one class with public fields, the role is text, and the pay is worked out in two places. The monthlyPay method makes the payslip, and transfer sends the money to the bank.
class Employee { public String name; public String role; public int salary; public int bonus;
public Employee(String name, String role, int salary, int bonus) { this.name = name; this.role = role; this.salary = salary; this.bonus = bonus; }}static int monthlyPay(Employee e) { if (e.role.equals("manager")) { return e.salary + e.bonus; } else if (e.role.equals("intern")) { return e.salary / 2; } return e.salary;}
static void transfer(Employee[] staff) { int total = 0; for (Employee e : staff) { int amount = e.salary; if (e.role.equals("manager")) { amount += e.bonus; } System.out.println("Transfer: " + e.name + " €" + amount); total += amount; } System.out.println("Total: €" + total);}Today accounting enters a raise for Ivan. But the form says -10 instead of 10:
Employee[] staff = { new Employee("Ivan", "employee", 2000, 0), new Employee("Maria", "manager", 3000, 500), new Employee("Ana", "intern", 1600, 0)};int percent = -10;staff[0].salary += staff[0].salary * percent / 100;for (Employee e : staff) { System.out.println("Payslip: " + e.name + " €" + monthlyPay(e));}transfer(staff);Payslip: Ivan €1800Payslip: Maria €3500Payslip: Ana €800Transfer: Ivan €1800Transfer: Maria €3500Transfer: Ana €1600Total: €6900The program does not stop and does not complain. But the output has three cracks:
- A bad write. Ivan's salary dropped to €1800. The field is open to every line in the program, and nobody asks whether -10% is a raise.
- Two copies of one rule. Ana's payslip says €800, and the bank sends her €1600. The rule "an intern gets half" is in the payslip, but someone forgot it in the transfer. The copy drifted away from the original, and nobody noticed.
- A new kind, many fixes. Tomorrow a freelancer joins. You need a new
else ifin the payslip, in the transfer, in tax, in leave, everywhere someone asks about the role. And every employee already carries abonusfield, though only the manager has a bonus.
Object-oriented programming is the answer to exactly these three things.
The idea
The four principles
Object-oriented programming (OOP) organizes a program around objects: data together with the methods that touch it. It rests on four principles. You have already worked with each of them in the modules "Classes and objects" and "Inheritance and polymorphism":
| Principle | What it says | Which crack it fixes |
|---|---|---|
| Encapsulation | The object guards its own data. From outside you reach it only through methods that check. | The bad write: -10% is refused. |
| Inheritance | What several classes share is written once, in a parent. | The bonus lives only in Manager, and what is shared (name, salary, the check in the raise) is written once in Employee. |
| Polymorphism | One call, and every object answers in its own way. | The if on the role: every class works out its own pay, with no second copy. |
| Abstraction | Code depends on what an object can do, not on how it does it. | A new kind of payment joins without touching the transfer. |
In short: encapsulation guards, inheritance shares, polymorphism chooses, abstraction promises.
From here on, each principle fixes one crack in the payroll program. At the end you will see them together, in the same program.
The idea
Encapsulation: the object guards its data
The salary belongs to the employee. Let only the employee change it. The field becomes private, and the raise is a method that checks:
class Employee { private String name; private int salary;
public Employee(String name, int salary) { this.name = name; this.salary = salary; }
public String getName() { return name; }
public int getSalary() { return salary; }
public void raise(int percent) { if (percent > 0) { salary += salary * percent / 100; } }}Employee ivan = new Employee("Ivan", 2000);ivan.raise(-10);System.out.println(ivan.getSalary());ivan.raise(10);System.out.println(ivan.getSalary());20002200-10% is refused, 10% goes through. The check is in one place, the class, and any code that wants to change the salary goes through it. This is encapsulation: the data and the rules for it sit together in the object, and from outside you see only what the class chooses to show.
A line that writes straight into the salary no longer compiles:
ivan.salary = -2000;error: salary has private access in EmployeeThe salary is read with the getter getSalary() and changed only with raise(). In Java, getters are written by hand.
When it hurts: getters and setters for everything. If every field has a getter and a setter that writes whatever you give it, the field is still open, only with more lines. Encapsulation is not "a hidden field plus get and set". It is: the object decides what may change and how. That is why Employee has no method that writes the salary, only a raise with a check. Start with everything hidden and open only what you must.
More precisely, if you are curious
privateguards the field from code outside the class, not from the class itself: every method ofEmployeecan touch it. That is why the class's own methods keep the rule.- A field that does not change after the constructor (the name) can also be
final. Then even the class cannot touch it later.
Try it
What hides the salary?
class Employee {
private String name;
int salary;private: the field is visible only to the code in Employee. From outside the salary changes only with raise().The idea
Inheritance: what is shared, once
A manager is an employee with a bonus. Instead of a copy of Employee with one more field, Manager inherits everything and adds only the bonus:
class Manager extends Employee { private int bonus;
public Manager(String name, int salary, int bonus) { super(name, salary); this.bonus = bonus; }
public int getBonus() { return bonus; }}Manager maria = new Manager("Maria", 3000, 500);maria.raise(-10);maria.raise(10);System.out.println(maria.getName() + ": " + maria.getSalary() + " + " + maria.getBonus());Maria: 3300 + 500The manager has no raise check of its own. It takes the one from Employee: -10% is refused, and 10% lifts 3000 to 3300. If the rule for raises changes tomorrow, you change it in one place and it holds for every child class. This is inheritance: what is shared sits in the parent, and the child writes only its own part.
The salary is private in Employee, so Manager cannot see it directly either. It hands the name and the salary to the parent's constructor with super(name, salary), and then reads them like anyone else, through getSalary(). In Java and C# a class has exactly one parent.
The test for whether inheritance makes sense is the sentence "X is a Y". "A manager is an employee" is true.
When it hurts: long chains and inheriting for ready-made code. Employee → Manager → SeniorManager → RegionalSeniorManager: to understand what the bottom class does, you read four classes, and a change at the top moves everything below it. Another common mistake is to inherit a class only to get a ready-made method: an invoice that extends Employee so that it has a name. "An invoice is an employee" is not true, and it would get a salary and a raise. What to do instead is in the card "What people mix up" below.
The idea
Polymorphism: each class answers for itself
Now the crack with the copies. Instead of an if on the role, every class works out its own monthly pay. The parent has a general version, and the child classes override it:
class Employee { // ... name, salary, getSalary() and raise() from before
public int monthlyPay() { return salary; }}
class Manager extends Employee { // ... bonus and the constructor
@Override public int monthlyPay() { return getSalary() + bonus; }}
class Intern extends Employee { // ... the constructor
@Override public int monthlyPay() { return getSalary() / 2; }}Both the payslip and the transfer call the same method:
static void transfer(Employee[] staff) { int total = 0; for (Employee e : staff) { System.out.println("Transfer: " + e.getName() + " €" + e.monthlyPay()); total += e.monthlyPay(); } System.out.println("Total: €" + total);}Employee[] staff = { new Employee("Ivan", 2000), new Manager("Maria", 3000, 500), new Intern("Ana", 1600)};for (Employee e : staff) { System.out.println("Payslip: " + e.getName() + " €" + e.monthlyPay());}transfer(staff);Payslip: Ivan €2000Payslip: Maria €3500Payslip: Ana €800Transfer: Ivan €2000Transfer: Maria €3500Transfer: Ana €800Total: €6300Ana gets €800 both on the payslip and in the transfer. The intern rule lives only in Intern, and there is no second copy to drift away. A new kind of employee is a new class with its own method, and the loop and the transfer stay untouched. This is polymorphism: one call, e.monthlyPay(), and every object answers in its own way.
In Java a method can be overridden by default, with no special word in the parent (unless it is final or private). The object decides which version runs, while the program runs, not the type of the variable. @Override makes the compiler check that you really override a method of the parent, and are not writing a new one with a misspelled name.
When it hurts. Two cases that will never become three do not need a class hierarchy: one if is shorter and clearer. Also: a child class that overrides the method but breaks the parent's promise (a pay that comes out negative, or a method that stops the program) breaks every loop that trusts the parent.
Try it
How much will the bank send?
The classes are from the cards above, together with the payment contract from the next card: amountDue() is in Employee and returns monthlyPay().
Employee e = new Intern("Ana", 1600);e.raise(10);System.out.println(e.amountDue());The raise is from Employee: 1600 + 10% = 1760. amountDue() is also from Employee, but it calls monthlyPay() on the object, and the object is an intern. The Intern version runs: 1760 / 2 = 880. Three principles in three lines.
The idea
Abstraction: what, not how
Accounting wants the suppliers' invoices in the same transfer. An invoice is not an employee. But the transfer does not care what kind of thing each one is. It needs two things: to whom, and how much. That is the contract:
interface Payable { int amountDue();
String payee();}
class Employee implements Payable { // ... everything from before
@Override public int amountDue() { return monthlyPay(); }
@Override public String payee() { return name; }}
static void transfer(Payable[] payments) { int total = 0; for (Payable p : payments) { System.out.println("Transfer: " + p.payee() + " €" + p.amountDue()); total += p.amountDue(); } System.out.println("Total: €" + total);}class Invoice implements Payable { private String supplier; private int amount;
public Invoice(String supplier, int amount) { this.supplier = supplier; this.amount = amount; }
@Override public int amountDue() { return amount; }
@Override public String payee() { return supplier; }}Payable[] payments = { new Employee("Ivan", 2000), new Manager("Maria", 3000, 500), new Intern("Ana", 1600), new Invoice("Power company", 340)};transfer(payments);Transfer: Ivan €2000Transfer: Maria €3500Transfer: Ana €800Transfer: Power company €340Total: €6640transfer knows nothing about salaries, bonuses and invoices. It knows only the contract. This is abstraction: you show what an object can do and hide how it does it. The transfer depends on the abstraction, so anything that fulfills it joins without a change to the transfer.
In Java the contract is an interface. Employee and Invoice have no common parent other than Object, but both say implements Payable, and the compiler checks that the methods are written. Manager and Intern get the contract from Employee.
A contract is one form of abstraction. The other is the abstract class: Shape promises an area, and each shape says how it is worked out ("Abstract classes"). When to use which is in the lesson "Interfaces".
When it hurts: abstraction too early. An interface with a single implementation, "just in case", is one more name, one more file and one more jump while you read. Abstraction pays off when there are at least two implementations, or when you need to swap the one you have, for example in a test. Payable appeared when the invoice came, not before.
See it
The four together
The scene walks through the program from the start: each principle fixes its own crack, and the numbers below come from the same program.
Employee[] staff = { new Employee("Ivan", 2000), new Manager("Maria", 3000, 500), new Intern("Ana", 1600)};staff[0].raise(-10);for (Employee e : staff) { System.out.println("Payslip: " + e.getName() + " €" + e.monthlyPay());}Payable[] payments = { staff[0], staff[1], staff[2], new Invoice("Power company", 340) };transfer(payments);static int monthlyPay(Employee e) { if (e.role.equals("manager")) { return e.salary + e.bonus; } else if (e.role.equals("intern")) { return e.salary / 2; } return e.salary;}
static void transfer(Employee[] staff) { int total = 0; for (Employee e : staff) { int amount = e.salary; if (e.role.equals("manager")) { amount += e.bonus; } System.out.println("Transfer: " + e.name + " €" + amount); total += amount; } System.out.println("Total: €" + total);}
int percent = -10;staff[0].salary += staff[0].salary * percent / 100;class Employee { private String name; private int salary;
public Employee(String name, int salary) { this.name = name; this.salary = salary; }
public String getName() { return name; }
public int getSalary() { return salary; }
public void raise(int percent) { if (percent > 0) { salary += salary * percent / 100; } }}class Manager extends Employee { private int bonus;
public Manager(String name, int salary, int bonus) { super(name, salary); this.bonus = bonus; }
public int getBonus() { return bonus; }}class Employee { // ... name, salary, getSalary() and raise() from before
public int monthlyPay() { return salary; }}
class Manager extends Employee { // ... bonus and the constructor
@Override public int monthlyPay() { return getSalary() + bonus; }}
class Intern extends Employee { // ... the constructor
@Override public int monthlyPay() { return getSalary() / 2; }}interface Payable { int amountDue();
String payee();}
class Employee implements Payable { // ... everything from before
@Override public int amountDue() { return monthlyPay(); }
@Override public String payee() { return name; }}
static void transfer(Payable[] payments) { int total = 0; for (Payable p : payments) { System.out.println("Transfer: " + p.payee() + " €" + p.amountDue()); total += p.amountDue(); } System.out.println("Total: €" + total);}Employee[] staff = { new Employee("Ivan", 2000), new Manager("Maria", 3000, 500), new Intern("Ana", 1600)};staff[0].raise(-10);for (Employee e : staff) { System.out.println("Payslip: " + e.getName() + " €" + e.monthlyPay());}Payable[] payments = { staff[0], staff[1], staff[2], new Invoice("Power company", 340) };transfer(payments);Step by step, in words
- Before: the role is text, the pay rule is copied into two chains of if, and anyone can write the salary. Ivan gets €1800, and Ana's payslip and transfer disagree.
- Encapsulation: the salary is hidden, and raise() refuses -10%. Ivan stays at €2000.
- Inheritance: Manager takes the name, the salary and the raise check from Employee and adds only the bonus. The objects now have classes, not a role in text.
- Polymorphism: every class works out its own monthlyPay(). The transfer calls the same method as the payslip, so Ana gets €800 in both.
- Abstraction: the transfer only knows the Payable interface. The power company invoice joins without being an employee.
- All four together: the same -10% is refused, every payslip matches its transfer, and the total is €6640.
Here is the program before and after, with 10% for Ivan. The procedure "before" has been fixed by hand: the intern rule is added to the transfer as well.
static int monthlyPay(Employee e) { if (e.role.equals("manager")) { return e.salary + e.bonus; } else if (e.role.equals("intern")) { return e.salary / 2; } return e.salary;}
static void transfer(Employee[] staff) { int total = 0; for (Employee e : staff) { int amount = e.salary; if (e.role.equals("manager")) { amount += e.bonus; } else if (e.role.equals("intern")) { amount = e.salary / 2; } System.out.println("Transfer: " + e.name + " €" + amount); total += amount; } System.out.println("Total: €" + total);}Employee[] staff = { new Employee("Ivan", "employee", 2000, 0), new Employee("Maria", "manager", 3000, 500), new Employee("Ana", "intern", 1600, 0)};int percent = 10;staff[0].salary += staff[0].salary * percent / 100;for (Employee e : staff) { System.out.println("Payslip: " + e.name + " €" + monthlyPay(e));}transfer(staff);Payslip: Ivan €2200Payslip: Maria €3500Payslip: Ana €800Transfer: Ivan €2200Transfer: Maria €3500Transfer: Ana €800Total: €6500static void transfer(Payable[] payments) { int total = 0; for (Payable p : payments) { System.out.println("Transfer: " + p.payee() + " €" + p.amountDue()); total += p.amountDue(); } System.out.println("Total: €" + total);}Employee[] staff = { new Employee("Ivan", 2000), new Manager("Maria", 3000, 500), new Intern("Ana", 1600)};staff[0].raise(10);for (Employee e : staff) { System.out.println("Payslip: " + e.getName() + " €" + e.monthlyPay());}transfer(staff);Payslip: Ivan €2200Payslip: Maria €3500Payslip: Ana €800Transfer: Ivan €2200Transfer: Maria €3500Transfer: Ana €800Total: €6500The output is the same. The difference is in what happens tomorrow:
- Someone enters -10 →
raise()refuses it (encapsulation). - A new rule for raises → it changes in one place,
Employee(inheritance). - A new kind of employee → a new class with its own
monthlyPay(); the payslip and the transfer stay untouched (polymorphism). - A new thing to pay → a new class that fulfills the contract; the transfer stays untouched (abstraction).
The same program with the wrong -10% from the problem and with the invoice:
Employee[] staff = { new Employee("Ivan", 2000), new Manager("Maria", 3000, 500), new Intern("Ana", 1600)};staff[0].raise(-10);for (Employee e : staff) { System.out.println("Payslip: " + e.getName() + " €" + e.monthlyPay());}Payable[] payments = { staff[0], staff[1], staff[2], new Invoice("Power company", 340) };transfer(payments);Payslip: Ivan €2000Payslip: Maria €3500Payslip: Ana €800Transfer: Ivan €2000Transfer: Maria €3500Transfer: Ana €800Transfer: Power company €340Total: €6640All the classes in one place
interface Payable { int amountDue();
String payee();}
class Employee implements Payable { private String name; private int salary;
public Employee(String name, int salary) { this.name = name; this.salary = salary; }
public String getName() { return name; }
public int getSalary() { return salary; }
public void raise(int percent) { if (percent > 0) { salary += salary * percent / 100; } }
public int monthlyPay() { return salary; }
@Override public int amountDue() { return monthlyPay(); }
@Override public String payee() { return name; }}
class Manager extends Employee { private int bonus;
public Manager(String name, int salary, int bonus) { super(name, salary); this.bonus = bonus; }
public int getBonus() { return bonus; }
@Override public int monthlyPay() { return getSalary() + bonus; }}
class Intern extends Employee { public Intern(String name, int salary) { super(name, salary); }
@Override public int monthlyPay() { return getSalary() / 2; }}
class Invoice implements Payable { private String supplier; private int amount;
public Invoice(String supplier, int amount) { this.supplier = supplier; this.amount = amount; }
@Override public int amountDue() { return amount; }
@Override public String payee() { return supplier; }}Try it
Put the manager in order
The lines of Manager are shuffled. Put them in the usual order: the field, the constructor, then the pay method.
class Manager extends Employee {public Manager(String name, int salary, int bonus) {}@Overridepublic int monthlyPay() {private int bonus;this.bonus = bonus;}}return getSalary() + bonus;super(name, salary);
Try it
Find the bug
A colleague "simplified" the contract in Employee. The program is the one from "The four together", and it prints:
Payslip: Ivan €2200Payslip: Maria €3500Payslip: Ana €800Transfer: Ivan €2200Transfer: Maria €3000Transfer: Ana €1600Total: €6800Maria's payslip is €3500, and the bank sends her €3000. Which line is the bug on?
Pick the line with the bug.
amountDue() returns the salary directly instead of asking monthlyPay(). This is the second copy of the rule from the problem again, only shorter: the manager's bonus and the intern's half are left out. A parent method that calls an overridden method is where polymorphism works for you. The fix: return monthlyPay();.
The idea
What people mix up
Abstraction or encapsulation? Both hide something, but different things. Encapsulation hides the data of one object so that nobody outside can break it: the salary is hidden, the raise checks. Abstraction hides the differences between many classes behind a shared promise: the transfer sees only amountDue() and payee(). Encapsulation looks at the object from inside, abstraction looks at the code that uses it from outside.
Inheritance or composition? Inheritance says "is a": a manager is an employee. Composition says "has a": an employee has a bank account. If you make the employee a child of the account from the lesson "Encapsulation" to get deposit() for free,
class Employee extends Account { // ... name and salary}it will also get withdrawals, a balance and everything the account learns tomorrow. And "an employee is an account" is not true. Instead, the account is a field of the employee:
class Employee { private String name; private int salary; private Account account;
public Employee(String name, int salary, Account account) { this.name = name; this.salary = salary; this.account = account; }
public void payday() { account.deposit(salary); }}Account account = new Account(100);Employee ivan = new Employee("Ivan", 2000, account);ivan.payday();System.out.println(account.getBalance());2100That is why the common advice is "prefer composition over inheritance". Inherit only when "X is a Y" is true and will stay true. Otherwise let the object have the other one as a field.
Overloading or overriding? Both are sometimes called polymorphism. Overloading is several methods with one name and different parameters: the compiler picks by the arguments, before the program runs. Overriding is the same method in a child class: the choice is made by the object, while the program runs. When people say "polymorphism" in OOP, they almost always mean the second.
More precisely, if you are curious
- Some books list three pillars: encapsulation, inheritance and polymorphism. Abstraction is then the shared idea under them. In an interview, four is what is usually expected.
- The principles are not rules to follow "100%". Each has a cost: hidden fields mean more methods, hierarchies mean more classes, contracts mean one more name. You use them where the crack is real.
- After these four come the five SOLID principles for code that survives change, and design patterns: ready-made solutions built from the same four. They are in the "Software engineering" course: SOLID and Design patterns.
Exercise
Which principle is broken?
A classic interview question. For each snippet, answer out loud: which principle is broken, what will break, and how you fix it.
1.
class Cart { public double total;}
// ... in 12 places in the program:cart.total -= discount;2.
static double area(Shape s) { if (s instanceof Circle c) return 3.14 * c.r * c.r; if (s instanceof Rectangle r) return r.w * r.h; return 0;}3.
class Invoice extends Employee { // gets name and payee() for free}4.
interface SalaryProvider { int provide(Employee e);}
// the only implementation:class DefaultSalaryProvider implements SalaryProvider { public int provide(Employee e) { return e.monthlyPay(); }}The answers
- Encapsulation. The total is open and gets reduced from 12 places. A discount bigger than the total makes it negative, and nobody asks. The total becomes private, and the discount is a method of the cart that checks.
- Polymorphism (and abstraction). An if on the kind of shape: a new shape means a new line here and everywhere someone asks about the kind. The area becomes an abstract method of
Shape, and each shape writes its own. - Inheritance in the wrong place. "An invoice is an employee" is not true: it gets a salary, a raise and a monthly pay. What they share is that they get paid: a
Payablecontract. - Abstraction too early. A contract with one implementation that only forwards to
monthlyPay()is one more name and one more jump while reading, and it guards against nothing. Remove it and call the method directly. The contract will make sense when a second implementation appears.
Exercise
A contractor and a terrace
Solve these in your editor.
1. A contractor. Contractor (Peter, 40 hours at €25) is not an employee: there is no payslip, but he must be in the transfer. Add him so that transfer does not change by a single line. Put a rent invoice in the transfer as well, €1800 for the landlord.
Transfer: Ivan €2000Transfer: Maria €3500Transfer: Ana €800Transfer: Peter €1000Transfer: Landlord €1800Total: €91002. The terrace without if. This procedure works out the area by the kind of shape:
static int area(String kind, int a, int b) { if (kind.equals("rectangle")) return a * b; if (kind.equals("triangle")) return a * b / 2; if (kind.equals("square")) return a * a; return 0;}Write it with an abstract Shape and the classes Rectangle, Triangle and Square. The terrace is a 4 × 3 rectangle, a triangle with base 2 and height 3, and a square with side 2. A square is a rectangle with equal sides: write it as a child of Rectangle, with no area formula of its own.
Rectangle: 12 m²Triangle: 3 m²Square: 4 m²Total: 19 m²More tasks
- A payslip for the contractor too. If the contractor wants a payslip after all, without becoming an employee: what has to be in the contract, and who fulfills it?
- Pay into an account. Give the employee an account (composition, as in "What people mix up") and a method that, on payday, deposits
monthlyPay(), not the salary. The manager must get the bonus too, without touchingmonthlyPay()inManager. - A raise for everyone. Raise every employee's salary by 5% in one loop. Which classes do you touch?
Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: Polymorphism · Abstract classes · Interfaces · Type checks
- Tasks on this topic: Inheritance and polymorphism