Trong những dự án phần mềm dài hơi, bạn có bao giờ thấy mình viết đi viết lại những khối if-else na ná nhau, hay một switch-case dài như sớ, chỉ để phân biệt vài “loại” xử lý? Đó là lúc đa hình (polymorphism) xuất hiện như một con dao gọt bút chì cực bén: nó cắt giảm các rãnh điều kiện trùng lặp, gom những hành vi khác nhau vào một hợp đồng chung, và giúp code ngắn đi một cách tự nhiên. Không phải ngắn kiểu “hack” cho nhanh, mà ngắn theo cách bền vững: ít lặp, ít chỗ hỏng, ít chỗ chạm tay. Trong bài viết này, tôi sẽ giải thích chi tiết vì sao và bằng cách nào đa hình có thể giúp code của bạn ngắn hơn khoảng 30% — cùng nhiều mẹo, ví dụ thực chiến, và lộ trình refactor an toàn.
Nói đơn giản, đa hình là khả năng thay thế các đối tượng có chung một “hợp đồng” (interface/abstract type) mà không cần đổi logic ở nơi sử dụng. Điều kỳ diệu nằm ở chỗ bạn chuyển “logic chọn loại” từ chỗ gọi sang chính đối tượng được gọi. Thay vì nơi dùng phải hỏi “Bạn là loại A hay B để tôi xử lý thế này?”, bạn để mỗi “loại” tự biết cách cần làm gì. Hệ quả trực tiếp: đống if-else/switch “điều phối” biến mất, mỗi nhánh xử lý nằm đúng lớp của nó, và bạn giảm đáng kể số dòng lặp.
Từ góc độ số lượng dòng code (LoC):
Đặc biệt, khi mô hình nghiệp vụ có 3–5 loại trở lên, lợi ích bắt đầu bùng nổ: các đoạn xử lý đặc thù mỗi loại nằm riêng trong lớp, phần khung/luồng chung nằm ở interface hoặc template. Ở quy mô dự án thực tế, cắt giảm 20–40% LoC cho module “điều phối” là khả thi.
Đa hình đặc biệt hữu ích khi bạn thấy các mùi sau:
Những mùi này khiến code:
Đa hình làm gì?
Giả sử ta có hàm xử lý thanh toán theo phương thức: card, paypal, cod. Cách “thẳng tay” thường thấy là switch-case:
// Trước: if-else dài và lan rộng
function pay(order: Order, method: 'card' | 'paypal' | 'cod'): PaymentResult {
if (method === 'card') {
validateCard(order.cardInfo);
return chargeCard(order.total, order.cardInfo);
} else if (method === 'paypal') {
const token = createPaypalToken(order.user);
return chargePaypal(order.total, token);
} else if (method === 'cod') {
return markAsCOD(order);
} else {
throw new Error('Unknown payment method');
}
}
Vấn đề:
Chuyển qua Strategy + interface:
interface PaymentStrategy {
pay(order: Order): PaymentResult;
}
class CardPayment implements PaymentStrategy {
pay(order: Order): PaymentResult {
validateCard(order.cardInfo);
return chargeCard(order.total, order.cardInfo);
}
}
class PaypalPayment implements PaymentStrategy {
pay(order: Order): PaymentResult {
const token = createPaypalToken(order.user);
return chargePaypal(order.total, token);
}
}
class CODPayment implements PaymentStrategy {
pay(order: Order): PaymentResult {
return markAsCOD(order);
}
}
// Registry để chọn strategy theo cấu hình hoặc input
const strategies: Record<string, PaymentStrategy> = {
card: new CardPayment(),
paypal: new PaypalPayment(),
cod: new CODPayment(),
};
function pay(order: Order, method: string): PaymentResult {
const strategy = strategies[method];
if (!strategy) throw new Error('Unknown payment method');
return strategy.pay(order);
}
Lợi ích:
class WithMetrics implements PaymentStrategy {
constructor(private inner: PaymentStrategy) {}
pay(order: Order): PaymentResult {
const start = Date.now();
try {
return this.inner.pay(order);
} finally {
metrics.timing('payment', Date.now() - start);
}
}
}
strategies['card'] = new WithMetrics(new CardPayment());
Khi tổng hợp các chỗ gọi thanh toán và xử lý bổ sung, team của tôi từng giảm khoảng 28–35% số dòng liên quan đến “chọn phương thức” và “lặp logic chung”. Con số phụ thuộc độ phức tạp, nhưng lợi ích nhất quán.
Bạn có nhiều hãng vận chuyển (A/B/C), mỗi hãng có cách tính phí riêng nhưng đều cần các bước chung: chuẩn hóa địa chỉ, tính trọng lượng quy đổi, áp dụng khuyến mãi. Trước đây, nhóm của tôi có 3 lớp gần như copy-paste 70% nội dung, chỉ khác vài dòng.
Dùng Template Method (abstract class định nghĩa khung, subclass điền chi tiết):
abstract class ShippingCalculator {
public final Money calculate(Order order) {
Address normalized = normalize(order.getAddress());
double weight = volumetricWeight(order);
Money base = basePrice(weight);
Money carrier = carrierSpecific(base, order); // hook
return applyDiscounts(carrier, order);
}
protected abstract Money carrierSpecific(Money base, Order order);
protected Address normalize(Address a) { /* ... chung ... */ }
protected double volumetricWeight(Order o) { /* ... chung ... */ }
protected Money basePrice(double w) { /* ... chung ... */ }
protected Money applyDiscounts(Money m, Order o) { /* ... chung ... */ }
}
class CarrierA extends ShippingCalculator {
@Override
protected Money carrierSpecific(Money base, Order order) {
return base.multiply(1.10);
}
}
class CarrierB extends ShippingCalculator {
@Override
protected Money carrierSpecific(Money base, Order order) {
Money surcharge = order.isRemoteArea() ? Money.of(3) : Money.zero();
return base.add(surcharge);
}
}
Giờ đây, 80% logic chung tồn tại đúng một chỗ. Mỗi hãng vận chuyển chỉ vài dòng. Số dòng code tính phí giảm rõ rệt; quan trọng hơn, nơi cần sửa chung (ví dụ công thức volumetricWeight) chỉ sửa một chỗ.
Trong front-end, duplication thường đến từ việc tạo nhiều biến thể của cùng một component. Ví dụ một Button có thể render thành thẻ a, button, hay Link với kiểu style giống nhau. Nếu tạo 3 component riêng, bạn lặp prop validation, style, test.
Dùng pattern component đa hình với prop as:
import React from 'react';
type AsProp<T extends React.ElementType> = {
as?: T;
} & React.ComponentPropsWithoutRef<T> & {
variant?: 'primary' | 'secondary';
};
function Polymorphic<T extends React.ElementType = 'button'>(props: AsProp<T>) {
const { as, variant = 'primary', className, ...rest } = props as AsProp<any>;
const Component = as || 'button';
const cls = `btn ${variant} ${className || ''}`.trim();
return <Component className={cls} {...rest} />;
}
// Dùng
<Polymorphic onClick={() => {}}>Save</Polymorphic>
<Polymorphic as="a" href="/docs">Docs</Polymorphic>
<Polymorphic as={Link} to="/home">Go home</Polymorphic>
Kết quả:
Python khuyến khích duck typing: “Nếu đi như vịt và kêu như vịt, đó là vịt.” Thay vì kiểm tra kiểu, bạn chỉ cần yêu cầu đối tượng có phương thức cần thiết.
Trước:
def export_order(order, fmt):
if fmt == 'csv':
return export_csv(order)
elif fmt == 'json':
return export_json(order)
elif fmt == 'xml':
return export_xml(order)
else:
raise ValueError('Unknown format')
Sau – chuyển sang polymorphism + Protocol (typing) để vẫn an toàn kiểu tĩnh:
from typing import Protocol
class Exporter(Protocol):
def export(self, order) -> str: ...
class CsvExporter:
def export(self, order) -> str:
# ... implement CSV
return ','.join(str(f) for f in order.fields)
class JsonExporter:
def export(self, order) -> str:
import json
return json.dumps(order.to_dict())
class XmlExporter:
def export(self, order) -> str:
# ... implement XML
return f"<order id={order.id}/ >"
def export_order(order, exporter: Exporter) -> str:
return exporter.export(order)
Dùng:
export_order(order, JsonExporter())
Bạn loại bỏ chuỗi điều kiện, giảm chạm tay khi thêm định dạng mới. Kết hợp với dependency injection (cấu hình chọn exporter), code xử lý “điều phối” gần như không đổi.
Giảm dòng code là mục tiêu phụ, đích đến là tính gọn và dễ thay đổi. Tuy vậy, đo LoC giúp bạn minh chứng hiệu quả refactor.
Cách đo thực tế:
cloc src --exclude-dir=node_modules,dist > before.txt
# refactor...
cloc src --exclude-dir=node_modules,dist > after.txt
Một con số tham khảo: ở module thanh toán 4 phương thức, việc chuyển sang strategy + decorator giảm 35% LoC vùng dispatch; tổng thể module (bao gồm test) giảm ~22%. Con số này thường dao động 20–40% tùy mức độ trùng lặp ban đầu.
Trong mỗi bước, chạy test và phân tích diff LoC để đảm bảo giảm mà vẫn an toàn.
Chìa khóa là cân bằng: đa hình để xóa duplication và cô lập thay đổi, không phải để “OOP hóa” mọi thứ.
Khi logic tách thành lớp theo hợp đồng, test trở nên:
Ví dụ test Java với interface PaymentStrategy:
class PaymentServiceTest {
@Test
void usesStrategyToPay() {
PaymentStrategy fake = order -> new PaymentResult("ok");
PaymentService svc = new PaymentService(fake);
PaymentResult r = svc.pay(new Order());
assertEquals("ok", r.getStatus());
}
}
Code test ngắn vì không cần khởi tạo cả hệ thống thanh toán thật. Riêng mảng test, giảm 20–40% dòng là thường gặp sau khi đa hình hóa hợp lý.
Các ngôn ngữ hiện đại (Kotlin, Scala, Swift, Java 21+) có pattern matching mạnh. Một sealed class với vài case và match expression rất gọn và an toàn kiểu:
Kotlin pattern matching:
sealed interface Payment {
data class Card(val info: CardInfo): Payment
data class Paypal(val token: String): Payment
data object COD: Payment
}
fun pay(p: Payment): Result = when (p) {
is Payment.Card -> chargeCard(p.info)
is Payment.Paypal -> chargePaypal(p.token)
Payment.COD -> markAsCOD()
}
Khi số case nhỏ, cố định, và logic cho mỗi case ngắn, pattern matching có thể ngắn hơn đa hình truyền thống. Tuy nhiên, khi:
…thì đa hình (kèm factory/registry) thường giúp code ngắn và linh hoạt hơn về lâu dài, vì bạn không phải mở lại hàm match trung tâm.
public interface ILogger { void Log(string msg); }
public class ConsoleLogger : ILogger { public void Log(string msg) => Console.WriteLine(msg); }
public class NullLogger : ILogger { public void Log(string msg) { /* no-op */ } }
// Inject NullLogger thay vì null
Kết quả: hàng loạt if (logger != null) biến mất, giảm dòng code lặp và giảm rủi ro NullReference.
type Command interface { Execute(ctx Context) error }
type Registry map[string]Command
func (r Registry) Run(name string, ctx Context) error {
cmd, ok := r[name]
if !ok { return fmt.Errorf("unknown command") }
return cmd.Execute(ctx)
}
Thêm lệnh mới chỉ cần implement và đăng ký.
Ở cấp kiến trúc, đa hình giúp:
Hệ quả: số “điểm quyết định” tập trung giảm mạnh. Bạn không có một lớp God Class làm mọi việc. Mã ngắn lại do mỗi adapter/handler nhỏ, và khả năng mở rộng về sau không làm phình một điểm trung tâm.
Bối cảnh: Module hóa đơn có 5 loại “bên thu” khác nhau (nội địa, quốc tế, đối tác A/B, đặc thù nội bộ). Code ban đầu có 1 lớp InvoiceService ~650 dòng, 3 helper ~200 dòng/lớp, và ~30 test xoay quanh 12 đường rẽ nhánh.
Triệu chứng:
Refactor:
Kết quả sau 2 tuần:
Quan trọng hơn, tốc độ thay đổi tăng vì điểm chạm ít: không còn “mở bụng” God Class.
Nếu bạn nhìn đa hình đơn thuần là một kỹ thuật OOP, nó dễ bị xem là “cầu kỳ”. Nhưng khi nhìn qua lăng kính tối giản hóa và cắt giảm duplication, đa hình là lối tắt hợp pháp: dịch chuyển quyết định từ nơi gọi về nơi thực thi, để mỗi loại tự nói lên bản chất của nó. Kết quả không chỉ là code ngắn hơn 30% ở những điểm đúng, mà còn là độ tin cậy tăng, test gọn, và đường cong mở rộng dễ thở hơn. Hãy bắt đầu từ những chuỗi if-else dài nhất trong codebase của bạn, trích xuất một interface nhỏ, thêm một registry đơn giản. Chỉ vài bước, bạn sẽ thấy mã nhẹ đi hẳn — và đội ngũ của bạn thở nhẹ hơn mỗi lần ship tính năng mới.