Interface vs abstract class: gom code chung cho các cổng thanh toán
Tiếp đi: interface và abstract class.
Ở bài 1.1, shop-backend đã có PaymentMethod (interface), mỗi cổng một class, Refundable/Cancellable tách riêng. Bài này trả lời câu hỏi tiếp theo: code chung giữa các cổng để ở đâu, và lúc nào dùng abstract class, lúc nào dùng interface.
1. Bài toán: 3 class thanh toán copy-paste
class MomoPayment implements PaymentMethod {
public PaymentResult pay(Order order) {
if (order.getTotal().signum() <= 0) throw new IllegalArgumentException(); // validate
log.info("Pay {} via MOMO", order.getId()); // log
PaymentResult r = callMomoApi(order); // KHÁC
txRepo.save(Transaction.of(order, r)); // lưu transaction
return r;
}
}
// VnPayPayment, CardPayment: y hệt, chỉ khác dòng gọi API
2. Vấn đề
- Sửa rule validate phải sửa 3 chỗ, quên một chỗ là bug.
- Thứ tự bước (validate → gọi → lưu) có thể bị class mới viết sai.
CardPaymentcòn cần refund, nếu đã cóAbstractRefundablethìextendscả hai được không? Không: Java chỉ cho single inheritance với class.
3. Giải pháp: abstract class cho code chung, interface cho khả năng
- Abstract class: không
newđược, có field, constructor, method đã cài đặt và method abstract. Gom phần chung lên cha, phần khác đểabstract. Pattern này là Template Method. - Interface: contract "làm được gì", không giữ state. Một class implement được nhiều interface, nên khả năng phụ (refund, cancel) đặt ở interface.
4. Code từng bước
Bước 1: Abstract class với template method
public abstract class AbstractPayment implements PaymentMethod {
private static final Logger log = LoggerFactory.getLogger(AbstractPayment.class);
protected final TransactionRepository txRepo; // state dùng chung
protected AbstractPayment(TransactionRepository txRepo) {
this.txRepo = txRepo;
}
// template method: khung cố định, final để con không phá thứ tự
@Override
public final PaymentResult pay(Order order) {
validate(order.getTotal());
log.info("Pay {} via {}", order.getId(), type());
PaymentResult result = doPay(order); // bước con tự viết
txRepo.save(Transaction.of(order, result));
return result;
}
private void validate(BigDecimal amount) {
if (amount.signum() <= 0) throw new IllegalArgumentException("amount <= 0");
}
protected abstract PaymentResult doPay(Order order); // protected: chỉ con cần thấy
}
Bước 2: Class con chỉ viết phần khác
@Component
public class MomoPayment extends AbstractPayment {
public MomoPayment(TransactionRepository txRepo) { super(txRepo); }
@Override public String type() { return "MOMO"; }
@Override protected PaymentResult doPay(Order o) { /* gọi API Momo */ return PaymentResult.ok(); }
}
Bước 3: Thêm khả năng bằng interface
public class CardPayment extends AbstractPayment, AbstractRefundable { } // ❌ compile error
public interface Refundable { RefundResult refund(String txId, BigDecimal amount); }
public interface Cancellable { void cancel(String txId); }
@Component
public class CardPayment extends AbstractPayment implements Refundable, Cancellable { // ✅
public CardPayment(TransactionRepository txRepo) { super(txRepo); }
@Override public String type() { return "CARD"; }
@Override protected PaymentResult doPay(Order o) { /* gọi cổng thẻ */ return PaymentResult.ok(); }
@Override public RefundResult refund(String txId, BigDecimal amount) { /* ... */ return RefundResult.ok(); }
@Override public void cancel(String txId) { /* ... */ }
}
Vì sao cấm đa kế thừa class: nếu hai cha cùng có field status hoặc cùng method process() đã cài đặt, không biết lấy cái nào, state bị trộn. Java tránh hẳn.
Bước 4: Bên ngoài phụ thuộc interface
@Service
class CheckoutService {
private final PaymentService paymentService; // dùng PaymentMethod bên trong
private final List<Refundable> refundables; // Spring inject mọi bean có khả năng refund
...
}
5. Cách hoạt động
«interface» PaymentMethod «interface» Refundable, Cancellable
▲ (contract) ▲ (khả năng thêm)
│ implements │
«abstract» AbstractPayment │
pay() final: validate → log → doPay() → save
▲ extends (chỉ 1) │ implements (nhiều)
┌───────────┼──────────────┐ │
MomoPayment VnPayPayment CardPayment ──────────┘
doPay() doPay() doPay() + refund() + cancel()
Gọi pay() trên MomoPayment:
AbstractPayment.pay() ──► validate ──► MomoPayment.doPay() ──► txRepo.save
6. So sánh (tính tới Java 17)
| Interface | Abstract class | |
|---|---|---|
| Kế thừa | Implement nhiều | Extends một |
| Field | Chỉ hằng public static final | Field instance, có state |
| Constructor | Không có | Có (con gọi qua super(...)) |
| Method | abstract, default, static, private | Mọi loại, kể cả final, protected |
| Access modifier của method | Mặc định public | Tùy ý |
| Ý nghĩa | "can-do": khả năng, contract | "is-a": cùng họ, dùng chung code và state |
| Lambda | Được nếu là functional interface | Không |
7. Khi nào nên dùng / không nên
- Có state hoặc khung xử lý chung? Có field, constructor, các bước cố định → abstract class.
- "is-a" hay "can-do"?
MomoPaymentlà một payment → abstract class.CardPaymentcó thể refund,Ordercó thể so sánh (Comparable) → interface. - Phổ biến nhất là kết hợp: interface làm contract cho bên ngoài phụ thuộc, abstract class cài phần chung, class cụ thể extends abstract class.
- Mặc định bắt đầu bằng interface. Chỉ thêm abstract class khi thật sự thấy code trùng. Đừng dùng abstract class chỉ để tái dùng vài helper; composition thường tốt hơn.
Spring dùng đúng combo này: JpaRepository, UserDetailsService, HandlerInterceptor là interface; OncePerRequestFilter (bạn chỉ override doFilterInternal), AbstractRoutingDataSource là abstract class kiểu template method.
extends và implements. Vậy "subclass" là nói cái nào? Interface có biến không?Mấy câu hỏi nhỏ:
- "Subclass" là nói về class
extendsclass, hay cả classimplementsinterface? - Class thường có
extendsđược class thường không? - Interface có biến static không?
Đúng hướng rồi, nhưng có một điểm cần tách rõ: extends và implements không đều tạo ra subclass.
1. Subclass là gì? Subclass = class kế thừa class khác bằng extends. MomoPayment extends AbstractPayment → MomoPayment là subclass, AbstractPayment là superclass. Còn CardPayment implements Refundable thì CardPayment là implementation của Refundable, không gọi Refundable là superclass.
class --extends--> class (1 cha duy nhất)
class --implements--> interface (nhiều)
interface --extends--> interface (nhiều)
2. Class thường extends class thường được không? Được, hoàn toàn bình thường, không cần cha là abstract:
class Employee { public void work() { System.out.println("Working"); } }
class Developer extends Employee { }
new Developer().work(); // ✅ kế thừa từ Employee
class C extends A, B { } // ❌ chỉ một class cha
class Developer implements Coder, Tester { } // ✅ nhiều interface
Khác biệt: class thường thì new được và không bắt con override gì; abstract class thì không new được và ép con viết method abstract. Muốn cấm kế thừa thì khai báo final class.
3. Interface có biến static không? Có. Field trong interface mặc định luôn là public static final, tức là hằng số:
interface PaymentLimits {
int MAX_RETRY = 3; // == public static final int MAX_RETRY = 3;
}
PaymentLimits.MAX_RETRY; // ✅
PaymentLimits.MAX_RETRY = 5; // ❌ final
Interface còn có static method, nhưng static method của interface không được class implement kế thừa; phải gọi qua tên interface (Refundable.isRefundable(x)). Đây là chỗ khá dễ nhầm.
Interface có được viết code trong method không? Mình thấy chữ default trong interface mà không hiểu.
Bạn để ý đúng chỗ. Từ Java 8 thì được, qua default method. Lý do chính: mở rộng interface cũ mà không phá code. Java 8 thêm stream(), forEach() vào Collection; nếu là abstract thì mọi class collection tự viết ngoài kia đều không compile nữa.
public interface Refundable {
RefundResult refund(String txId, BigDecimal amount);
// default: có sẵn code, class implement kế thừa luôn, override được
default RefundResult refundAll(String txId, BigDecimal paid) {
return refund(txId, paid);
}
// static: gọi qua tên interface
static boolean isRefundable(BigDecimal amount) {
return amount.signum() > 0;
}
// private (Java 9): code dùng chung giữa các default, không lộ ra ngoài
private void audit(String txId) { /* ... */ }
}
Lưu ý: default method không truy cập được field instance vì interface không có state, chỉ gọi được method khác của interface.
Diamond problem: hai interface cùng default method trùng tên thì Java bắt bạn tự chọn:
interface Logger { default String name() { return "logger"; } }
interface Auditor { default String name() { return "auditor"; } }
class PaymentAudit implements Logger, Auditor {
// không override → compile error: inherits unrelated defaults
@Override
public String name() {
return Logger.super.name() + "+" + Auditor.super.name();
}
}
- Class thắng interface: method trong class (hoặc class cha) luôn ưu tiên hơn default.
- Interface cụ thể hơn thắng:
B extends A, cả hai có defaultname()→ lấy củaB. - Còn mơ hồ thì bắt buộc override, gọi
X.super.method()nếu muốn dùng lại.
Vì interface không có state nên đây chỉ là đa kế thừa hành vi, không phải đa kế thừa state, vẫn an toàn.
Lambda là gì? Java 8 có sẵn những functional interface nào?
Functional interface là interface có đúng một abstract method (SAM, Single Abstract Method). Lambda là cách viết ngắn implementation của method đó, không cần anonymous class.
(parameters) -> body
(a, b) -> a + b
Áp vào shop-backend: chính sách giảm giá.
@FunctionalInterface
public interface DiscountPolicy {
BigDecimal apply(BigDecimal price);
default DiscountPolicy then(DiscountPolicy next) { // default không làm mất tính "functional"
return p -> next.apply(apply(p));
}
}
// Trước Java 8: anonymous class
DiscountPolicy vip = new DiscountPolicy() {
@Override public BigDecimal apply(BigDecimal p) { return p.multiply(new BigDecimal("0.9")); }
};
// Java 8: lambda
DiscountPolicy vip2 = p -> p.multiply(new BigDecimal("0.9"));
DiscountPolicy voucher = p -> p.subtract(new BigDecimal("50000")).max(BigDecimal.ZERO);
BigDecimal finalPrice = vip2.then(voucher).apply(order.getTotal());
Functional interface có được nhiều method: default, static và các method public của Object (như toString()) không tính. @FunctionalInterface không bắt buộc, chỉ để compiler kiểm tra.
Java 8 có sẵn trong java.util.function, 4 nhóm quan trọng nhất:
| Interface | Method | Input → Output | Ví dụ shop-backend |
|---|---|---|---|
Predicate<T> | test(T) | T → boolean | Predicate<Order> isPaid = o -> o.getStatus() == PAID |
Function<T,R> | apply(T) | T → R | Function<Order, OrderDto> toDto = OrderDto::from |
Consumer<T> | accept(T) | T → void | Consumer<Order> notify = noti::sendOrderConfirmed |
Supplier<T> | get() | () → T | Supplier<String> txId = () -> UUID.randomUUID().toString() |
Ngoài ra còn BiFunction, UnaryOperator, BinaryOperator; và các interface cũ như Runnable, Callable, Comparator cũng là functional interface. Spring dùng khắp nơi: http.authorizeHttpRequests(auth -> ...) nhận một Customizer. Nhớ ở bài 1.1, new OrderService(order -> {}) chạy được chính vì OrderRepository chỉ có một method.
Lưu ý: lambda không tự tạo thread và không đồng nghĩa với async; nó chỉ là một object implement interface.
Java 17 thêm sealed interface: giới hạn đúng những class được implement, compiler biết hết các trường hợp (từ Java 21, switch pattern trên sealed type không cần default):
public sealed interface PaymentInfo permits Momo, VnPay, Card {}
public record Momo(String phone) implements PaymentInfo {}
public record VnPay(String bankCode) implements PaymentInfo {}
public record Card(String last4) implements PaymentInfo {}
Phỏng vấn hỏi interface và abstract class khác nhau gì, khi nào dùng cái nào thì trả lời sao?
Nói khác biệt kỹ thuật, rồi tiêu chí chọn, rồi ví dụ thực tế.
Câu 1: Interface và abstract class khác nhau gì, khi nào dùng cái nào?
🇻🇳 Tiếng Việt:
Abstract class dùng cho các class cùng họ, quan hệ "is-a": có field, constructor, state và code dùng chung, nhưng chỉ extends được một. Interface là contract "can-do": implement được nhiều, không có state, field chỉ là hằng số; từ Java 8 có default và static method, Java 9 có private method. Trong dự án, em để interface
PaymentMethodlàm contract cho DI và mock khi test, cònAbstractPaymentgom validate, log, lưu transaction theo template method. Khả năng phụ như refund thì tách interfaceRefundable.
🇬🇧 English:
An abstract class is for closely related classes, an "is-a" relationship: it can have fields, constructors, state and shared code, but a class can extend only one. An interface is a "can-do" contract: a class can implement many, it has no instance state and its fields are constants; since Java 8 it can have default and static methods, and private methods since Java 9. In my project the
PaymentMethodinterface is the contract used for injection and mocking, whileAbstractPaymentholds validation, logging and saving the transaction as a template method. Optional capabilities like refunds live in a separateRefundableinterface.
Câu 2: Hai interface cùng default method thì sao?
🇻🇳 Tiếng Việt:
Class sẽ compile error cho tới khi override method đó; muốn dùng lại thì gọi
A.super.method(). Quy tắc: class thắng interface, interface cụ thể hơn thắng, còn lại phải override.
🇬🇧 English:
The class won't compile until it overrides that method; it can reuse one with
A.super.method(). The rules are: class methods win over defaults, the more specific interface wins, otherwise you must override.
Câu 3: Functional interface là gì?
🇻🇳 Tiếng Việt:
Là interface có đúng một abstract method, nên có thể implement bằng lambda hoặc method reference. Default và static method không tính. Java 8 có sẵn
Predicate,Function,Consumer,Suppliertrongjava.util.function.
🇬🇧 English:
It is an interface with exactly one abstract method, so it can be implemented with a lambda or a method reference. Default and static methods don't count. Java 8 ships
Predicate,Function,ConsumerandSupplierinjava.util.function.
Cách nhớ ngắn gọn
- Abstract class = "is-a", có state, extends 1, template method.
- Interface = "can-do", không state, implement nhiều, field là
public static final. - Java 8 default/static, Java 9 private, Java 17 sealed.
- Subclass chỉ đi với
extends;implementstạo implementation. - Một abstract method = functional interface = viết được lambda.
Tự kiểm tra
Vì sao Java không cho extends nhiều class nhưng cho implement nhiều interface?
Class có state và code cài đặt, đa kế thừa dễ xung đột field/method. Interface không có state, còn xung đột default method thì compiler bắt class tự override.
Vì sao pay() trong AbstractPayment nên là final còn doPay() là protected abstract?
final giữ cố định thứ tự validate → gọi cổng → lưu transaction. doPay() protected để class con override nhưng bên ngoài không gọi thẳng, bỏ qua validate.
Default method được thêm vào Java 8 để làm gì?
Để mở rộng interface đã có (như Collection.stream()) mà không phá các class đang implement nó.
Field int MAX_RETRY = 3; trong interface thực chất là gì?
public static final: hằng số, gọi qua tên interface, không gán lại được. Interface không có field instance hay constructor.
Functional interface có default method còn dùng lambda được không?
Được. Chỉ cần đúng một abstract method; default, static và các method public của Object không tính.