Interface vs abstract class: gom code chung cho các cổng thanh toán

5 câu hỏi~25 phútTừ khóa: template method, abstract class, interface, default method, diamond, static field, functional interface, lambda, sealed

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 đề

3. Giải pháp: abstract class cho code chung, interface cho khả năng

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)

InterfaceAbstract class
Kế thừaImplement nhiềuExtends một
FieldChỉ hằng public static finalField instance, có state
ConstructorKhông cóCó (con gọi qua super(...))
Methodabstract, default, static, privateMọi loại, kể cả final, protected
Access modifier của methodMặc định publicTù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 interfaceKhông

7. Khi nào nên dùng / không nê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.

Mấy câu hỏi nhỏ:

  1. "Subclass" là nói về class extends class, hay cả class implements interface?
  2. Class thường có extends được class thường không?
  3. 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();
    }
}
  1. Class thắng interface: method trong class (hoặc class cha) luôn ưu tiên hơn default.
  2. Interface cụ thể hơn thắng: B extends A, cả hai có default name() → lấy của B.
  3. 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:

InterfaceMethodInput → OutputVí dụ shop-backend
Predicate<T>test(T)T → booleanPredicate<Order> isPaid = o -> o.getStatus() == PAID
Function<T,R>apply(T)T → RFunction<Order, OrderDto> toDto = OrderDto::from
Consumer<T>accept(T)T → voidConsumer<Order> notify = noti::sendOrderConfirmed
Supplier<T>get()() → TSupplier<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 PaymentMethod làm contract cho DI và mock khi test, còn AbstractPayment gom validate, log, lưu transaction theo template method. Khả năng phụ như refund thì tách interface Refundable.

🇬🇧 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 PaymentMethod interface is the contract used for injection and mocking, while AbstractPayment holds validation, logging and saving the transaction as a template method. Optional capabilities like refunds live in a separate Refundable interface.

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, Supplier trong java.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, Consumer and Supplier in java.util.function.

Cách nhớ ngắn gọn


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.