Làm sao đa hình giúp code ngắn hơn 30 phần trăm

Làm sao đa hình giúp code ngắn hơn 30 phần trăm

29 phút đọc Khám phá cách đa hình rút ngắn code, tăng tái sử dụng, giảm trùng lặp, đơn giản hóa test và bảo trì trong dự án OOP lẫn FP.
(0 Đánh giá)
Bài viết phân tích cơ chế đa hình, kỹ thuật interface/abstract class, dynamic dispatch, pattern matching; so sánh trước–sau refactor; chỉ số LOC, cyclomatic complexity; hướng dẫn áp dụng trong Java, C#, TypeScript, Kotlin để cắt giảm 30% mã an toàn.
Làm sao đa hình giúp code ngắn hơn 30 phần trăm

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.

Đa hình qua lăng kính “cắt giảm dòng code”

polymorphism, code reduction, refactoring

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):

  • Trước: Bạn có 1 hàm trung tâm lớn, chứa nhiều điều kiện phân biệt loại. Mỗi lần thêm loại mới, bạn phải thêm case mới, kiểm tra, và đụng tay vào chỗ cũ.
  • Sau: Bạn có nhiều lớp nhỏ, mỗi lớp vài chục dòng, và một điểm nối (factory/DI container) để chọn đúng lớp. Thêm loại mới? Chỉ thêm lớp và đăng ký, rất ít va chạm.

Đặ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.

Mùi code khiến code phình to (và đa hình chữa được)

code smells, anti-patterns

Đa hình đặc biệt hữu ích khi bạn thấy các mùi sau:

  • Chuỗi if-else/switch dài phân loại theo type/enum.
  • Biến cờ (flag) điều chỉnh hành vi trong cùng một lớp, khiến phương thức phình to.
  • Lặp lại cùng một “khung xử lý” với khác biệt nhỏ cho từng loại.
  • Nhiều đoạn kiểm tra kiểu (instanceof) rải rác.

Những mùi này khiến code:

  • Dài do lặp điều kiện và lặp khung xử lý.
  • Dễ lỗi vì mỗi thay đổi phải chỉnh nhiều chỗ.
  • Khó test vì cần thiết lập nhiều tình huống điều kiện.

Đa hình làm gì?

  • Đẩy khác biệt vào từng lớp cụ thể, xóa điều kiện ở nơi gọi.
  • Gom phần khung vào interface/abstract class, chia sẻ thực thi chung.
  • Dùng Null Object để loại bỏ “nếu null thì…”.

Ví dụ 1: Xử lý thanh toán – từ if-else đến interface

payment processing, strategy pattern

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

  • Khi thêm “bank_transfer”, ta phải sửa hàm pay và thêm else if.
  • Luồng chung (validate dữ liệu, lưu log, đo thời gian) lặp lại nếu bạn thêm chúng.

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:

  • Hàm pay rất ngắn, không lặp logic nghiệp vụ.
  • Thêm phương thức mới? Chỉ tạo lớp mới và đăng ký vào strategies.
  • Có thể thêm cross-cutting concerns bằng decorator để tránh lặp, ví dụ log/metrics:
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.

Ví dụ 2: Bộ tính phí vận chuyển – Template Method cắt lặp

template method, shipping rates

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ỗ.

Ví dụ 3: Front-end React – component đa hình

React, TypeScript, polymorphic components

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ả:

  • Một component thay cho nhiều biến thể. Bớt duplicate về style, props, test.
  • Khi thêm biến thể mới, chủ yếu là mở rộng variant thay vì tạo component mới.
  • Dễ áp dụng hệ thống thiết kế thống nhất, codebase front-end thường giảm 15–30% LoC ở phần component “điều hướng UI”.

Ví dụ 4: Python duck typing – bớt isinstance, bớt rẽ nhánh

Python, duck typing, dynamic typing

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.

Mẹo đo lường: chứng minh giảm 30% bằng số

metrics, lines of code, measurement

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ế:

  • Dùng cloc để đo LoC trước và sau (bỏ qua thư viện vendor):
cloc src --exclude-dir=node_modules,dist > before.txt
# refactor...
cloc src --exclude-dir=node_modules,dist > after.txt
  • Đo số dòng của file “điều phối” cũ (hàm switch/if-else) so với các lớp strategy mới.
  • Đo số điểm chạm khi thêm loại mới: trước cần sửa bao nhiêu file? sau chỉ thêm lớp?
  • Đo số dòng test bị ảnh hưởng: test tập trung vào từng lớp thay vì n-hướng rẽ nhánh.

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.

Lộ trình refactor 5 bước an toàn

refactoring steps, safety, tests
  1. Khoanh vùng “điều phối”:
  • Tìm các hàm có switch/if-else dài theo type/enum.
  • Liệt kê các nhánh hành vi và khác biệt.
  1. Viết test bao phủ hiện trạng:
  • Snapshot input-output cho từng nhánh.
  • Thêm test cho lỗi cạnh (edge cases).
  1. Trích xuất interface/abstract class:
  • Định nghĩa hợp đồng (ví dụ PaymentStrategy.pay).
  • Di chuyển từng nhánh xử lý vào lớp cụ thể.
  1. Thay switch bằng factory/registry:
  • Dùng map từ key -> đối tượng.
  • Nếu cần cross-cutting, thêm decorator.
  1. Xóa điều kiện cũ và đơn giản hóa:
  • Loại bỏ biến cờ/enum không còn cần.
  • Dọn test trùng và gom test chung để ngắn gọn.

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.

Khi nào không nên dùng đa hình

trade-offs, overengineering
  • Số “loại” chỉ là 2 và khó có khả năng tăng. Đa hình có thể là quá tay.
  • Logic rất ngắn và hầu như không chia sẻ phần khung. Một switch gọn có khi dễ đọc hơn.
  • Runtime cực kỳ nhạy về hiệu năng và việc tạo đối tượng/gián tiếp hàm là chi phí đáng kể. Dù đa hình thường tối ưu được, có trường hợp cần tối ưu đặc biệt.
  • Hệ thống đang chuyển đổi sang pattern matching kiểu sealed class (Kotlin/Swift) với vài nhánh cố định. Khi số nhánh thực sự đóng lại, pattern matching có thể rõ ràng hơ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ứ.

Tinh gọn test nhờ đa hình

unit testing, mocks, interfaces

Khi logic tách thành lớp theo hợp đồng, test trở nên:

  • Ngắn hơn: mỗi lớp có phạm vi hẹp, ít thiết lập (setup).
  • Độc lập: thay vì test một hàm lớn với nhiều nhánh, bạn test từng lớp với dữ liệu tối thiểu.
  • Dễ giả lập (mock): bạn có thể inject một Strategy giả để kiểm tra luồng bao ngoài, giảm nhu cầu stub nhiều cờ/enum.

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ý.

Polymorphism và pattern matching: chọn khi nào

pattern matching, sealed classes

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:

  • Cần chia sẻ khung xử lý phức tạp.
  • Dự đoán sẽ thêm case thường xuyên, hoặc case do bên thứ ba plugin vào.
  • Cần đóng gói hành vi theo nguyên tắc “tiêm phụ thuộc” (DI) và mở rộng runtime.

…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.

Null Object, Command, và Plugin – xóa điều kiện, xóa lặp

null object, command pattern, plugins
  • Null Object: Thay vì kiểm tra null ở 10 nơi trước khi log, tạo một Logger mặc định không làm gì:
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.

  • Command Pattern: Thay switch lệnh bằng map tên -> đối tượng:
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ý.

  • Plugin Architecture: Cho phép nạp lớp mới qua cấu hình/module. Dispatch trung tâm gần như không đổi. Tuy setup phức tạp hơn, phần code cốt lõi ngắn và sạch, nhất là khi số biến thể tăng dần.

Polymorphism ở biên kiến trúc: REST, message bus

architecture, microservices

Ở cấp kiến trúc, đa hình giúp:

  • Chuẩn hóa adapter cho nhà cung cấp bên ngoài (payment gateway, shipping, SMS). Mỗi provider là một implementation của cùng interface.
  • Gói tương tác IO vào cổng (port) theo Hexagonal Architecture. Use-case gọi qua interface, không biết cụ thể adapter nào đang chạy.
  • Trên message bus, handler đa hình cho các sự kiện khác nhau, đăng ký theo topic/type, xóa nhu cầu một “dispatcher khổng lồ”.

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.

Best practices ngắn gọn, áp dụng được ngay

best practices, guidelines
  • Bắt đầu từ chỗ đau: chỉ đa hình hóa nơi có if-else dài hoặc duplication rõ ràng.
  • Đặt tên interface theo năng lực (Capability) thay vì chi tiết: PaymentStrategy, RateCalculator, Renderer.
  • Interface hẹp, mục đích rõ; tách khi nó phình quá 3–5 phương thức không liên quan.
  • Tận dụng default method (Java) hoặc mixin để chia sẻ mã chung mà không lặp.
  • Dùng decorator để thêm cross-cutting concerns (log, metrics, retry) mà không chạm code lõi.
  • Null Object thay vì null checks, đặc biệt với logger, cache, optional sink.
  • Registry/factory tập trung, ánh xạ key cấu hình sang implementation; tránh if-else đăng ký rải rác.
  • Đo và theo dõi: dùng cloc và kiểm lại sau mỗi refactor để đảm bảo ta thực sự giảm LoC.
  • Viết test theo hợp đồng (contract test) cho mỗi implementation; số test ít nhưng bao phủ tốt.

Mini case-study: rút 32% LOC ở module hóa đơn

case study, invoice module

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:

  • Mỗi khi thêm loại “bên thu”, sửa 4–5 chỗ, test cập nhật nhiều.
  • Nhiều if-else kiểm tra loại khách hàng, khu vực, ưu đãi.

Refactor:

  • Trích xuất interface BillablePartyPolicy với phương thức: computeTax, computeDiscount, validate.
  • Dùng Template Method trong InvoiceCalculator để gom normalize dữ liệu, audit trail.
  • Thêm decorator Retry cho chính sách gọi API thuế vụ.
  • Dùng Null Object cho AuditSink (nếu tắt audit).

Kết quả sau 2 tuần:

  • InvoiceService còn ~220 dòng (chủ yếu orchestration). 5 policy lớp ~50–80 dòng/lớp.
  • Helper gộp lại thành AbstractCalculator ~150 dòng, bỏ 2 helper cũ.
  • Tổng LoC vùng invoice giảm từ ~1250 xuống ~850 dòng: -32%.
  • Số test còn 24, nhưng rõ ràng theo từng policy; thời gian chạy test giảm ~18%.
  • Thêm loại “bên thu” mới chỉ cần viết 1 lớp policy và đăng ký.

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.

Đánh giá bài viết

Thêm bình luận & đánh giá

Đánh giá của người dùng

Dựa trên 0 đánh giá
5 Star
0
4 Star
0
3 Star
0
2 Star
0
1 Star
0
Thêm bình luận & đánh giá
Chúng tôi sẽ không bao giờ chia sẻ email của bạn với bất kỳ ai khác.