1. Abstraction in real applications: why you need it
In previous lectures we already introduced the concept of abstraction and looked at simple examples. Now let’s see how this approach works in more realistic tasks. In real projects you almost always have to deal with different but similar objects. For example, different payment methods, different types of transport, different shapes in a graphics editor. Without abstraction, code quickly turns into a set of “if-else” and copy-paste. With abstraction — everything is strict, elegant and, most importantly, convenient for evolution and maintenance.
Abstraction lets you:
- Hide implementation details: work with objects through a common interface without caring how they are implemented inside.
- Avoid code duplication: shared behavior and fields are moved to a base class.
- Easily extend the system: adding new kinds of objects does not require rewriting old code.
- Make the code flexible: you can swap one implementation for another without changing the rest of the system.
Let’s go through a few examples from different domains.
2. Example 1: Payment systems
Problem statement
Suppose you are writing a module for an online store. Your task is to implement the processing of different types of payments: bank card, PayPal, cryptocurrency. All of them must be able to “process a payment,” but the details differ.
Abstraction: class Payment
public abstract class Payment {
protected double amount;
public Payment(double amount) {
this.amount = amount;
}
// Abstract method: how exactly to process a payment is decided by subclasses
public abstract void process();
// Common method for all payments
public void printAmount() {
System.out.println("Payment amount: " + amount + " RUB.");
}
}
Concrete implementations
public class CreditCardPayment extends Payment {
private String cardNumber;
public CreditCardPayment(double amount, String cardNumber) {
super(amount);
this.cardNumber = cardNumber;
}
@Override
public void process() {
System.out.println("Processing card payment: " + cardNumber);
// Here could be an integration with a bank :)
}
}
public class PaypalPayment extends Payment {
private String email;
public PaypalPayment(double amount, String email) {
super(amount);
this.email = email;
}
@Override
public void process() {
System.out.println("Processing PayPal payment for account: " + email);
// And here - a call to the PayPal API
}
}
public class CryptoPayment extends Payment {
private String walletAddress;
public CryptoPayment(double amount, String walletAddress) {
super(amount);
this.walletAddress = walletAddress;
}
@Override
public void process() {
System.out.println("Processing crypto payment to wallet: " + walletAddress);
// Here there could be some blockchain magic
}
}
Using the abstraction
import java.util.*;
public class PaymentDemo {
public static void main(String[] args) {
List<Payment> payments = new ArrayList<>();
payments.add(new CreditCardPayment(1500.0, "1234 5678 9012 3456"));
payments.add(new PaypalPayment(500.0, "user@example.com"));
payments.add(new CryptoPayment(0.05, "0xABCD..."));
for (Payment payment : payments) {
payment.printAmount();
payment.process();
System.out.println("---");
}
}
}
Output:
Payment amount: 1500.0 RUB.
Processing card payment: 1234 5678 9012 3456
---
Payment amount: 500.0 RUB.
Processing PayPal payment for account: user@example.com
---
Payment amount: 0.05 RUB.
Processing crypto payment to wallet: 0xABCD...
---
Advantages:
- You can add a new payment method without changing old code (e.g., Apple Pay).
- Code that works with payments does not depend on their concrete type.
- Common logic (for example, printing the amount via printAmount()) is implemented in one place.
3. Example 2: Transport
Problem statement
In a game or simulator you have different types of transport: cars, bicycles, trains. They all can “move,” but they do it differently. Some need refueling, others do not.
Abstraction: class Transport
public abstract class Transport {
protected String name;
public Transport(String name) {
this.name = name;
}
public abstract void move();
// Not all types of transport need refueling, but by default - no
public void fuelUp() {
System.out.println(name + ": no refueling required.");
}
}
Concrete implementations
public class Car extends Transport {
public Car(String name) {
super(name);
}
@Override
public void move() {
System.out.println(name + " drives on the road.");
}
@Override
public void fuelUp() {
System.out.println(name + ": refueling with gasoline.");
}
}
public class Bicycle extends Transport {
public Bicycle(String name) {
super(name);
}
@Override
public void move() {
System.out.println(name + " pedals.");
}
// We don't override fuelUp - a bicycle doesn't need refueling
}
public class Train extends Transport {
public Train(String name) {
super(name);
}
@Override
public void move() {
System.out.println(name + " speeds along the rails.");
}
@Override
public void fuelUp() {
System.out.println(name + ": refueling with diesel or electricity.");
}
}
Using the abstraction
import java.util.*;
public class TransportDemo {
public static void main(String[] args) {
List<Transport> vehicles = Arrays.asList(
new Car("Toyota"),
new Bicycle("Stels"),
new Train("Sapsan")
);
for (Transport t : vehicles) {
t.move();
t.fuelUp();
System.out.println("---");
}
}
}
Output:
Toyota drives on the road.
Toyota: refueling with gasoline.
---
Stels pedals.
Stels: no refueling required.
---
Sapsan speeds along the rails.
Sapsan: refueling with diesel or electricity.
---
Advantages:
- You can handle any transport uniformly without checking its type.
- It’s easy to add a new type of transport (e.g., an electric scooter).
4. Example 3: Graphics editor
Problem statement
You are writing a mini graphics editor. It has lines, ellipses, polygons — and all of these are “figures” that can be drawn and resized. At the same time, each figure implements these actions in its own way.
Abstraction: class Figure
public abstract class Figure {
protected String color = "black";
public abstract void draw();
public abstract void resize(double factor);
public void setColor(String color) {
this.color = color;
}
}
Concrete implementations
public class Line extends Figure {
private double length;
public Line(double length) {
this.length = length;
}
@Override
public void draw() {
System.out.println("Drawing a line of length " + length + " with color " + color);
}
@Override
public void resize(double factor) {
length *= factor;
System.out.println("New line length: " + length);
}
}
public class Ellipse extends Figure {
private double a, b;
public Ellipse(double a, double b) {
this.a = a;
this.b = b;
}
@Override
public void draw() {
System.out.println("Drawing an ellipse with axes " + a + " and " + b + " with color " + color);
}
@Override
public void resize(double factor) {
a *= factor;
b *= factor;
System.out.println("New ellipse dimensions: " + a + " x " + b);
}
}
public class Polygon extends Figure {
private int sides;
public Polygon(int sides) {
this.sides = sides;
}
@Override
public void draw() {
System.out.println("Drawing a polygon with " + sides + " sides with color " + color);
}
@Override
public void resize(double factor) {
System.out.println("Resizing a polygon with " + sides + " sides by " + factor);
}
}
Using the abstraction
import java.util.*;
public class EditorDemo {
public static void main(String[] args) {
List<Figure> figures = new ArrayList<>();
figures.add(new Line(10));
figures.add(new Ellipse(5, 3));
figures.add(new Polygon(6));
for (Figure f : figures) {
f.setColor("green");
f.draw();
f.resize(2);
System.out.println("---");
}
}
}
Output:
Drawing a line of length 10.0 with color green
New line length: 20.0
---
Drawing an ellipse with axes 5.0 and 3.0 with color green
New ellipse dimensions: 10.0 x 6.0
---
Drawing a polygon with 6 sides with color green
Resizing a polygon with 6 sides by 2.0
---
Advantages:
- All shapes can be stored in a single list and processed uniformly.
- It’s easy to add a new shape (e.g., a star or a heart).
- Common methods (for example, setting the color via setColor()) are implemented once.
5. How abstraction helps simplify code
In each example above there is a common pattern:
- A base abstract class defines the contract (what the object can do).
- Concrete subclasses implement the details.
- Code that works with the abstraction does not depend on the object type, making the system flexible and extensible.
Comparison table of approaches
| Without abstraction (if-else) | With abstraction (OOP) |
|---|---|
| Many type-based conditionals | New type — change existing code |
| Duplicated logic | Logic in one place |
| Hard to extend | Easy to add |
| Hard to test | Easy to swap implementations |
6. Typical mistakes when designing abstractions
Mistake No. 1: Abstraction for the sake of abstraction.
If you have only one type of object and no extension is planned, an abstract class is unnecessary. Don’t overcomplicate the code without reason.
Mistake No. 2: An abstraction that is too generic.
If the base class is too “blurry,” subclasses may have nothing in common except the name. For example, an abstraction “Thing” for everything. This makes maintenance and understanding the code harder.
Mistake No. 3: Code duplication in subclasses.
If all subclasses have the same implementation of a method, move it to the base class (make it non-abstract).
Mistake No. 4: Violating the “from general to specific” principle.
If the abstract class acquires details that are needed by only one subclass, the abstraction is chosen incorrectly.
Mistake No. 5: Forgot to implement abstract methods.
If you don’t implement all abstract methods in a subclass, the compiler will force the class to be abstract as well. Sometimes that’s unexpected :)
GO TO FULL VERSION