bash - staycrafting.dev

~$ cat chu-d-trong-solid-ong-ban-da-ve-que-ca-quan-ban-tra-nong.md

Chữ D trong SOLID: ông bán đá về quê, cả quán bán trà nóng

Chữ D trong SOLID (Dependency Inversion): code nghiệp vụ không nên dính chặt vào một dịch vụ bên ngoài cụ thể. Bài cuối series SOLID quán trà đá.

Quán trà đá lấy đá của ông Ba đầu chợ, quen đến mức cái thùng đá được đóng vừa khít khay đá nhà ông Ba, sổ sách ghi "tiền đá ông Ba", nhân viên được dặn "hết đá thì gọi ông Ba, số 09xx". Hôm ông Ba về quê ăn giỗ một tuần, cả quán bán trà nóng giữa tháng Sáu.

Muốn lấy đá nhà máy đầu phố thì không xong: khay của nhà máy không vừa thùng, sổ sách phải sửa, nhân viên không biết gọi ai. Ông Ba không có lỗi gì. Lỗi là cả cái quán được xây quanh ông Ba.

Lời dẫn

Đây là bài cuối của series SOLID, mỗi bài một chữ:

Chữ Tên đầy đủ Nói kiểu quán trà đá
S Single Responsibility Principle Mỗi người nghe một sếp
O Open/Closed Principle Thêm món thì gài thêm thẻ, đừng sơn lại biển
L Liskov Substitution Principle Người thay ca phải bán đúng như người cũ
I Interface Segregation Principle Tuyển trông xe thì đừng đòi biết pha latte
D Dependency Inversion Principle Cần đá viên đạt chuẩn, đừng cần đúng ông Ba

Bốn chữ trước nói chuyện chia code cho đúng, cắm thêm cho êm, thay thế cho chuẩn, hứa hẹn cho vừa. Chữ D trả lời câu hỏi còn lại: ai phụ thuộc vào ai. Đây là chữ quyết định cái lõi của chương trình có bị trói vào một ông bán đá nào đó hay không.

Bài này đọc riêng vẫn hiểu. Đọc cả series thì sẽ thấy D là chữ mà anh em đã làm từ bài S mà chưa biết tên.

Trong bài có bài refactor mẫu đi từng bước, và bài tập nhanh ở cuối để anh em tự kiểm tra.

1. D là gì?

Dependency Inversion Principle (DIP), nguyên tắc đảo ngược phụ thuộc. Bác Bob (Robert C. Martin) phát biểu thành hai ý, trong bài viết năm 1996 và sau đó là sách Agile Software Development (2002):

A. High level modules should not depend upon low level modules. Both should depend upon abstractions.

B. Abstractions should not depend upon details. Details should depend upon abstractions.

A. Module cấp cao không nên phụ thuộc vào module cấp thấp. Cả hai nên phụ thuộc vào abstraction.

B. Abstraction không nên phụ thuộc vào chi tiết. Chi tiết nên phụ thuộc vào abstraction.

— Robert C. Martin, The Dependency Inversion Principle, C++ Report, 1996

Nghe như văn bản hành chính. Dịch cabin:

  • Module cấp cao là phần quan trọng, phần nghiệp vụ: "mỗi đêm sao lưu database", "mỗi sáng gửi bản tin bài mới". Ở quán: "bán trà đá cho khách".
  • Module cấp thấp là chi tiết vặt: mysqldump, Google Cloud Storage, Brevo, Stripe. Ở quán: ông Ba.
  • Abstraction là cái mô tả nhu cầu, không nói tên ai: "một thứ biết xuất database ra file", "một chỗ cất file". Ở quán: "đá viên sạch, giao trước 6 giờ sáng, vừa khay chuẩn".

Câu ngắn nhất: phần quan trọng không nên biết tên của mấy thứ vặt.

Đảo ngược cái gì?

Bình thường, chiều phụ thuộc đi từ trên xuống:

Bán trà đá  ──phụ thuộc──▶  Ông Ba

Quán thiết kế theo ông Ba. Ông Ba đổi khay, quán đổi theo. Ông Ba nghỉ, quán nghỉ.

Đảo ngược là:

Bán trà đá  ──phụ thuộc──▶  "Đá viên đạt chuẩn"  ◀──đáp ứng──  Ông Ba
                                                 ◀──đáp ứng──  Nhà máy đá

Quán đặt ra tiêu chuẩn. Ai bán đá thì phải theo tiêu chuẩn của quán. Mũi tên từ ông Ba giờ chỉ ngược lên. Đó là chữ "đảo ngược". Điểm mấu chốt: tiêu chuẩn do quán đặt, không phải do ông Ba đặt.

Hiểu nhầm phổ biến

"Dependency Inversion với Dependency Injection là một." Hai cái tên na ná, hay bị lẫn. Dependency Injection (DI) là một kỹ thuật: thay vì tự new bên trong, class nhận thứ nó cần từ bên ngoài, thường qua constructor. Dependency Inversion (DIP) là một nguyên tắc về chiều phụ thuộc. DI là cách phổ biến nhất để đạt DIP, nhưng DI xong chưa chắc đã DIP: nếu cái được truyền vào vẫn mang nguyên hình dạng của ông Ba (gọi method tên goi_ong_ba, nhận khay cỡ nhà ông Ba), thì quán vẫn bị trói, chỉ là trói bằng dây dài hơn.

"Phải dùng DI container, framework, file cấu hình XML." Không cần. Trong Ruby, truyền qua tham số initialize là đủ. Anh em đã làm từ bài S rồi:

MonthlyBillReport.new(
  reader:    GcpBillingCsvReader.new("billing-2026-09.csv"),
  formatter: MarkdownBillFormatter.new,
  sender:    EmailBillSender.new(to: "hello@staycrafting.dev")
)

MonthlyBillReport không biết GCP, không biết Brevo. Nó chỉ biết cần một reader, một formatter, một sender. Đó là D.

"Cái gì cũng phải bọc abstraction." Không. Mục 5 sẽ nói kỹ.

Kết luận phần này: cái lõi đặt ra yêu cầu, chi tiết chạy theo yêu cầu, không phải ngược lại.

2. Chuyện đời thường: ổ cắm, sạc, và ông Ba

Ví dụ ổ cắm điện ở bài O cũng là ví dụ của D, chỉ là nhìn từ góc khác. Cái nồi chiên không phụ thuộc vào nhà máy điện nào. Nhà máy thủy điện, nhiệt điện, điện mặt trời đều phải cung cấp đúng chuẩn 220V, 50Hz. Cái nồi chỉ cần biết chuẩn, không cần biết điện từ nhà máy nào. Nhà máy nào muốn bán điện thì phải chạy theo chuẩn.

Ngược lại, thời điện thoại mỗi hãng một kiểu chân sạc là thời điện thoại phụ thuộc vào cục sạc của đúng hãng đó. Mất sạc là mất liên lạc. Đến khi cả ngành thống nhất USB-C, điện thoại chỉ cần "một cục sạc USB-C", hãng nào cũng được.

Quán trà đá của mình sau vụ ông Ba về quê cũng rút kinh nghiệm:

Trước Sau
Thùng đá đóng vừa khay ông Ba Thùng đá theo khay cỡ chuẩn, nhà ai giao cũng vừa
Sổ ghi "tiền đá ông Ba" Sổ ghi "tiền đá", bên cạnh ghi nhà cung cấp
"Hết đá gọi ông Ba" "Hết đá gọi số trên bảng", bảng ghi hai số

Ông Ba vẫn là nhà cung cấp chính, vẫn giao đá mỗi sáng. Chỉ là quán không còn chết theo ông Ba.

Mẹo nhỏ: Đọc file nghiệp vụ quan trọng nhất của dự án. Nếu thấy tên một hãng, một SDK, một dịch vụ cụ thể (Stripe, Aws::, Google::Cloud, Brevo, Net::HTTP) nằm giữa logic nghiệp vụ, chỗ đó đang ghi "tiền đá ông Ba".

3. Vi phạm D thì đã sao?

  • Không test được nếu không có thứ thật. Muốn test logic "đặt tên file backup theo ngày", phải có MySQL thật và tài khoản cloud thật. Kết quả thường gặp: không ai viết test.
  • Đổi nhà cung cấp là mổ lõi. Muốn chuyển từ Google Cloud Storage sang Cloudflare R2, từ Brevo sang dịch vụ khác, phải sửa thẳng vào code nghiệp vụ. Mà code nghiệp vụ là phần đắt nhất để làm hỏng.
  • Không chạy được ở máy local. Chạy thử ở máy mình là đẩy file thật lên cloud thật, gửi mail thật cho người thật. Xin chúc mừng, anh em vừa gửi bản tin test "asdfgh" cho toàn bộ người đăng ký.
  • Chi tiết rò vào lõi. Lỗi của SDK, tên tham số của API bên ngoài, giới hạn của nhà cung cấp lan dần vào code nghiệp vụ. Đến lúc nào đó, không còn phân biệt được đâu là luật của mình, đâu là luật của ông Ba.

Dấu hiệu nhận biết

  • Class nghiệp vụ tự new các client bên ngoài ngay trong method: Google::Cloud::Storage.new, Stripe::Charge.create, Net::HTTP.post.
  • Class nghiệp vụ gọi system(...), đọc ENV[...], đọc file cấu hình trực tiếp.
  • Class nghiệp vụ gọi Time.now, Date.today ở nhiều chỗ, và test phải "đóng băng thời gian" mới chạy được.
  • Test phải mock thẳng class của thư viện bên ngoài (allow(Stripe::Charge).to receive...).

4. Bài refactor: job backup database

Code bằng Ruby như các bài trước. Anh em ngôn ngữ khác cứ đọc như mã giả.

Bối cảnh: ai theo series Blog trà đá thì biết blog này có một job backup database mỗi đêm, chạy mysqldump rồi đẩy file lên một bucket Google Cloud Storage. Dưới đây là một phiên bản minh họa viết kiểu "dính chặt", không phải code thật của blog, nhưng anh em sẽ thấy nó quen.

4.1. Code ban đầu: cả quán xây quanh ông Ba

require "google/cloud/storage"

class DatabaseBackup
  def run
    file = "/tmp/backup-#{Date.today}.sql"
    db = Rails.configuration.database_configuration["production"]

    system("mysqldump -h #{db['host']} -u #{db['username']} " \
           "-p#{db['password']} #{db['database']} > #{file}") or raise "mysqldump lỗi"

    bucket = Google::Cloud::Storage.new.bucket("staycrafting-backup")
    bucket.create_file(file, File.basename(file))
  ensure
    File.delete(file) if file && File.exist?(file)
  end
end

Mười mấy dòng, chạy ngon mỗi đêm. Nhưng nhìn kỹ, DatabaseBackup biết quá nhiều thứ:

Nó biết Đó là chi tiết của
Lệnh mysqldump và cú pháp tham số MySQL
Database tên gì, nằm trong cấu hình Rails nào Rails, môi trường production
Google Cloud Storage, tên bucket Google
File tạm để ở /tmp Hệ điều hành
Hôm nay là ngày nào (Date.today) Đồng hồ hệ thống

Còn phần thật sự là của nó, phần nghiệp vụ, thì chỉ có bốn việc: đặt tên file theo ngày, xuất database ra file đó, cất file đi, dọn file tạm. Bốn việc đó bị chôn giữa đống chi tiết.

Hậu quả:

  • Test: muốn kiểm tra file có được đặt tên đúng ngày không, phải có MySQL và quyền ghi vào bucket thật.
  • Chạy local: chạy thử là đẩy file thật lên bucket production.
  • Đổi nhà cung cấp: chuyển sang Cloudflare R2 hay sang PostgreSQL là mổ thẳng vào run.
  • Bảo mật: mật khẩu nằm ngay trên dòng lệnh, ai liệt kê tiến trình trên máy cũng đọc được. Chi tiết vặt mà để lọt vào lõi thì lỗi vặt cũng lọt theo.

4.2. Bước 1: cái lõi cần gì?

Quên MySQL, quên Google đi. Hỏi: để sao lưu, DatabaseBackup cần những gì?

  1. Một thứ biết xuất database ra một file cho trước.
  2. Một chỗ biết cất một file với một cái tên.
  3. Biết hôm nay là ngày nào (để đặt tên).

Viết thành "tiêu chuẩn" bằng ngôn ngữ của nghiệp vụ, không dính tên nhà cung cấp:

Vai trò Lời hứa
dumper dump_to(path): xuất toàn bộ database ra file ở path, lỗi thì ném lỗi
store save(path, name:): cất file ở path với tên name
clock today: trả về ngày hôm nay

Để ý tên method: dump_to, save. Không phải run_mysqldump, không phải create_file_in_bucket. Tiêu chuẩn do quán đặt, bằng tiếng của quán.

4.3. Bước 2: cái lõi chỉ làm việc với tiêu chuẩn

require "tmpdir"

class DatabaseBackup
  def initialize(dumper:, store:, clock: Date)
    @dumper = dumper
    @store = store
    @clock = clock
  end

  def run
    name = "backup-#{@clock.today}.sql"
    Dir.mktmpdir do |dir|
      path = File.join(dir, name)
      @dumper.dump_to(path)
      @store.save(path, name: name)
    end
    name
  end
end

Đọc lên như đọc quy trình: đặt tên, xuất ra, cất đi. Dir.mktmpdir lo luôn chuyện dọn file tạm. Không có chữ MySQL, không có chữ Google nào. clock: Date là mặc định cho đời thật, test thì truyền đồng hồ giả.

4.4. Bước 3: chi tiết chạy theo tiêu chuẩn

Mỗi nhà cung cấp một class nhỏ, đáp ứng tiêu chuẩn ở bước 1:

class MysqlDumper
  def initialize(config)
    @config = config
  end

  def dump_to(path)
    ok = system(
      { "MYSQL_PWD" => @config["password"] },
      "mysqldump", "-h", @config["host"], "-u", @config["username"], @config["database"],
      out: path
    )
    raise "mysqldump lỗi" unless ok
  end
end

class GcsBackupStore
  def initialize(bucket_name)
    @bucket = Google::Cloud::Storage.new.bucket(bucket_name)
  end

  def save(path, name:)
    @bucket.create_file(path, name)
  end
end

class LocalFolderStore
  def initialize(dir)
    @dir = dir
    FileUtils.mkdir_p(dir)
  end

  def save(path, name:)
    FileUtils.cp(path, File.join(@dir, name))
  end
end

Tiện tay sửa luôn chuyện mật khẩu: truyền qua biến môi trường MYSQL_PWD thay vì lên dòng lệnh, và gọi system với từng tham số riêng để không phải lo chuyện ký tự đặc biệt trong mật khẩu. Chi tiết của MySQL thì sửa ở chỗ của MySQL, cái lõi không cần biết.

4.5. Bước 4: một chỗ duy nhất biết tên ông Ba

Phải có một chỗ biết hết các nhà cung cấp thật để lắp ráp. Chỗ đó nên nằm ở rìa ngoài cùng, ví dụ rake task:

# lib/tasks/backup.rake
task backup: :environment do
  db = Rails.configuration.database_configuration[Rails.env]

  store =
    if Rails.env.production?
      GcsBackupStore.new(ENV.fetch("BACKUP_BUCKET"))
    else
      LocalFolderStore.new("tmp/backups")
    end

  DatabaseBackup.new(dumper: MysqlDumper.new(db), store: store).run
end

Đây là cái bảng "hết đá gọi số nào" của quán. Đổi nhà cung cấp là sửa đúng chỗ này. Chạy ở máy local thì file nằm trong tmp/backups, không bay lên cloud.

Sơ đồ phụ thuộc giờ thành:

rake task (chỗ lắp ráp)
   │  biết tất cả, chỉ để lắp
   ▼
DatabaseBackup ──cần──▶ dumper / store / clock   (tiêu chuẩn)
                              ▲      ▲
                   MysqlDumper  GcsBackupStore, LocalFolderStore

Lõi trỏ vào tiêu chuẩn. Chi tiết trỏ ngược lên tiêu chuẩn. Đảo ngược xong.

4.6. Test: không cần MySQL, không cần cloud

require "minitest/autorun"

class DatabaseBackupTest < Minitest::Test
  class FakeDumper
    def dump_to(path)
      File.write(path, "-- fake dump")
    end
  end

  class MemoryStore
    attr_reader :saved

    def initialize
      @saved = {}
    end

    def save(path, name:)
      @saved[name] = File.read(path)
    end
  end

  FixedClock = Struct.new(:today)

  def test_dat_ten_theo_ngay_va_cat_dung_noi_dung
    store = MemoryStore.new
    backup = DatabaseBackup.new(
      dumper: FakeDumper.new,
      store: store,
      clock: FixedClock.new(Date.new(2026, 10, 4))
    )

    assert_equal "backup-2026-10-04.sql", backup.run
    assert_equal({ "backup-2026-10-04.sql" => "-- fake dump" }, store.saved)
  end
end

Test chạy trong nháy mắt, trên máy nào cũng chạy, không cần mạng. MysqlDumper và GcsBackupStore thì test riêng (hoặc kiểm tra bằng một lần chạy thật trên môi trường staging), vì chúng mỏng và chỉ làm đúng một việc nói chuyện với bên ngoài.

4.7. So sánh trước và sau

Trước Sau
DatabaseBackup biết MySQL, Google, Rails config, /tmp, đồng hồ Chỉ biết tiêu chuẩn: dumper, store, clock
Test logic sao lưu Cần MySQL và cloud thật Object giả, chạy trong vài mili giây
Chạy ở máy local Đẩy file lên bucket production Lưu vào tmp/backups
Chuyển sang R2 hoặc PostgreSQL Mổ DatabaseBackup Viết class mới, sửa chỗ lắp ráp
Mật khẩu Nằm trên dòng lệnh Truyền qua biến môi trường
Số class 1 1 lõi + mỗi nhà cung cấp 1 class mỏng

5. Đừng đảo ngược quá tay

Học xong D, rất dễ rơi vào cảnh bọc mọi thứ bằng abstraction: StringUtilsInterface, DateProvider, ArrayFactory. Kìm lại.

Không cần đảo ngược với thứ ổn định. Thư viện chuẩn của Ruby (Date, BigDecimal, JSON, File cho việc đơn giản) mấy chục năm không đổi. Phụ thuộc vào nó như phụ thuộc vào chuyện nước sôi ở 100 độ. Không ai bọc nước sôi bằng interface.

Khoan, vậy sao mục 4 lại tách clock ra khỏi Date? Vì cái ổn định là class Date: cách cộng ngày, cách so sánh, cách in ra chuỗi. Còn "hôm nay là ngày nào" thì mỗi lần chạy một khác, và test không điều khiển được. PriceCalculator dùng BigDecimal thì cứ dùng thẳng. DatabaseBackup hỏi "hôm nay" thì nên hỏi qua clock.

Đảo ngược ở biên giới. Chỗ đáng đặt tiêu chuẩn là chỗ chương trình chạm ra thế giới bên ngoài:

  • Thứ chậm: mạng, database ngoài, file lớn.
  • Thứ tốn tiền hoặc có tác dụng phụ thật: gửi mail, trừ tiền, đẩy file lên cloud.
  • Thứ hay đổi nhà cung cấp: email, lưu trữ, thanh toán, SMS.
  • Thứ khó kiểm soát trong test: thời gian, số ngẫu nhiên.

Đừng chống lại framework. Trong Rails, controller gọi thẳng model ActiveRecord là chuyện bình thường. Bọc toàn bộ ActiveRecord sau một lớp "repository" cho app nhỏ là tự tạo thêm việc, mà lợi ích chẳng thấy đâu. Rails đã tự đảo ngược phụ thuộc ở những chỗ quan trọng: Active Storage như bài O có nhắc, rồi Active Job, Action Mailer cũng vậy, đều cho cắm adapter qua config. Code của anh em gọi post.cover.attach(...), không gọi Google.

Tiêu chuẩn chỉ đáng đặt khi đã có ông Ba thứ hai, hoặc sắp có, hoặc test đang đau. Quán chỉ cần đặt tiêu chuẩn khay đá, không cần đặt tiêu chuẩn cho cái thìa khuấy.

Nói riêng với anh em Rails

  • Service object nhận dependency qua initialize, kèm giá trị mặc định là kiểu D vừa phải nhất cho Rails: def initialize(sender: BrevoSender.new, clock: Time) (BrevoSender ở đây là class mỏng tự viết, giả định). Đời thật không phải truyền gì, test thì truyền đồ giả. Cái giá phải nói thẳng: service vẫn biết tên ông Ba, ở đúng một dòng mặc định. Đổi lại, phần thân của nó chỉ gọi @sender, và chỗ gọi service không phải lắp ráp gì. Muốn lõi sạch hẳn thì bỏ giá trị mặc định, lắp ráp ở rìa ngoài như mục 4.5.
  • Thời gian: Rails có travel_to để đóng băng thời gian trong test, dùng được. Nhưng nếu một class nghiệp vụ phụ thuộc nặng vào thời gian (lịch, hạn, khuyến mãi theo giờ như bài O), truyền clock vào sẽ rõ ràng hơn.
  • Gem SDK bên ngoài (Stripe, AWS, Google, Brevo): luôn bọc trong một class mỏng của mình, đặt tên theo nhu cầu của mình. Lúc gem lên phiên bản mới đổi tên method, anh em chỉ sửa đúng một file.

6. Bài tập nhanh

Đến lượt anh em. Một bài khởi động nhẹ, không cần mở editor, chỉ cần một ly trà đá: đọc từng tình huống và đoán xem chỗ nào vi phạm, chỗ nào không đáng sửa. Bài tập sửa code dài hơn sẽ gom vào một bài tổng ôn riêng của series.

Cố tự làm trước rồi hãy xem lời giải. Xem lời giải trước khi làm thì giống chép đáp án đề thi thử: cảm giác rất giỏi, cho đến hôm thi thật.

Trường hợp nào vi phạm D? Giải thích bằng câu hỏi "phần quan trọng có đang biết tên một chi tiết vặt không, và chi tiết đó có đáng để tách không?".

  1. OrderService#checkout tính tổng tiền, áp khuyến mãi, rồi gọi thẳng Stripe::Charge.create(amount: total, ...) ngay trong method.
  2. PriceCalculator dùng BigDecimal để tính tiền cho khỏi sai số.
  3. SignupService.new(mailer: BrevoMailer.new). Bên trong: @mailer.brevo_template_id = 12 rồi @mailer.send_brevo_template(to: email).
  4. PostsController#index gọi Post.published.order(published_at: :desc). App blog cá nhân, một database, không có kế hoạch đổi.
  5. CouponValidator#valid? kiểm tra hạn dùng bằng Date.today rải rác ở bốn chỗ. Test hiện phải dùng travel_to ở mọi ca, và hay quên.

Túm cái váy lại

Chữ D

Ghi nhớ
Định nghĩa Cấp cao không phụ thuộc cấp thấp, cả hai phụ thuộc abstraction
Dịch cabin Phần quan trọng không nên biết tên của mấy thứ vặt
Câu hỏi thần chú "Ông Ba về quê thì quán có phải đóng cửa không?"
Đảo ngược Tiêu chuẩn do cái lõi đặt, chi tiết chạy theo tiêu chuẩn
Dấu hiệu Tên SDK, system, ENV, Date.today nằm giữa logic nghiệp vụ
Cách làm Hỏi lõi cần gì, đặt tiêu chuẩn bằng tiếng của lõi, mỗi nhà cung cấp một class mỏng, lắp ráp ở rìa ngoài
Hiểu nhầm DI là DIP. DI là kỹ thuật, DIP là chiều phụ thuộc. Inject rồi vẫn có thể bị trói
Đừng Bọc mọi thứ. Chỉ đảo ngược ở biên giới: chậm, tốn tiền, hay đổi, khó test

Cả series, gói trong một bảng

Chữ Câu hỏi thần chú Quán trà đá
S Ai sẽ bắt mình sửa class này? Bà chủ kiêm hết, ốm một hôm là cả ngõ tê liệt
O Thêm một loại mới thì phải sửa mấy file cũ? Thêm trà chanh thì gài thẻ, đừng sơn lại biển
L Người gọi có phải hỏi "đứa nào đây?" không? Người trông quán hộ phải bán trà đá khi khách gọi trà đá
I Đoạn code này thật sự gọi mấy method? Tuyển trông xe thì đừng đòi biết pha latte
D Ông Ba về quê thì quán có phải đóng cửa không? Cần đá viên đạt chuẩn, đừng cần đúng ông Ba

Năm chữ, nhưng chung một mục đích: sửa code mà không sợ. S chia đúng chỗ. O cho cắm thêm mà không đụng cũ. L đảm bảo cắm vào thì chạy đúng. I giữ cho mỗi chỗ cắm vừa vặn. D đảm bảo cái lõi không bị trói vào bất kỳ ai.

Và như mọi bài đều nhắc: SOLID là để code dễ sửa hơn, không phải để code trông kiến trúc hơn. Áp khi đau, áp ở chỗ đau. Code chạy ngon, không ai phải sửa, thì cứ để nó yên mà uống trà.

Sau series này, anh em đã sở hữu:

  • Năm câu hỏi thần chú, mỗi câu soi một kiểu bệnh.
  • Năm bài refactor mẫu, từ báo cáo hóa đơn đến job backup.
  • Và hy vọng là một cái nhìn khác về quán trà đá đầu ngõ: nó vận hành trơn tru hơn rất nhiều hệ thống phần mềm mình từng thấy.

Chúc anh em code dễ sửa, deploy không giật mình, nhà cung cấp có về quê cũng không sao, và trà luôn đủ đá.

À mà, nếu anh em đang nghĩ "dự án em gọi thẳng SDK khắp nơi mà vẫn chạy ngon", thì mình tin. Quán cũng lấy đá ông Ba mười năm nay rồi. Chỉ là chưa đến tháng Sáu ông Ba về quê thôi.

Đăng nhập để xem đáp án.

~$ cd ..