Ở trong bài OOP – chủ đề huyền thoại trong các cuộc phỏng vấn tuyển dụng tôi có nhắc đến OOP designing, trong bài này tôi sẽ nói kỹ hơn về nó.
Hầu hết chúng ta khi nhắc đến OOP đều chỉ nhớ là nó có 3 (hoặc 4) thuộc tính: đóng gói, thừa kế, đa hình và trừu tượng. Thực chất đó chỉ là những công cụ và nguyên liệu, cái quan trọng nhất vẫn là từ những công cụ và nguyên liệu này, bạn sẽ xây nên ngôi nhà như thế nào. Đó cũng là sự khác nhau giữa ông thợ xây và kiến trúc sư, giữa coder và designer. Muốn lên một trình cao hơn, ta cần biết cách sử dụng chúng một cách đúng đắn, và người xưa đã đúc kết ra một số quy tắc. Ta hãy cùng xem qua những nguyên tắc (nếu là một bậc thầy marketing thì sẽ thêm từ “bí mật” ) nhé:
Danh từ tạo nên lớp và thuộc tính, động từ tạo nên phương thức: Có lẽ nhiều người sẽ ngạc nhiên về điều này, nhưng thực ra, viết mô tả các chức năng hệ thống luôn phải là công việc đầu tiên khi thiết kế.
Hãy xem câu sau: “Người dùng nhập họ tên và email để đăng ký, hệ thống sẽ kiểm tra xem Email đã có trong hệ thống hay chưa, nếu có rồi sẽ hiển thị thông báo lỗi, nếu chưa có sẽ lưu vào cơ sở dữ liệu và gửi email chúc mừng đến người dùng vừa đăng ký.”
Trong đoạn văn trên, ta dễ dàng nhận ra một số danh từ: Người dùng, họ tên, email, hệ thống, cơ sở dữ liệu… và một số động từ: đăng ký, kiểm tra (email), hiển thị (thông báo lỗi), lưu (vào CSDL), gửi (email chúc mừng)…
Các danh từ có giá trị đơn (họ tên, email…) sẽ là thuộc tính (property), các danh từ chứa các thuộc tính sẽ là class.Đây là một quy tắc chung, giúp bạn không sai hướng khi phải làm việc với vô số thông tin đầu vào, và bạn có thể bám sát được đề bài. Sau bước này bạn sẽ có một danh sách thô, bạn sẽ phải tinh chỉnh, xem xét cái nào thừa, cái nào thiếu, nhóm chung các lớp cùng chức năng vào chung module… Ở bước này đòi hỏi kỹ năng và kinh nghiệm cửa người thiết kế khá nhiều, và không có cách nào khác là bạn phải thực hành, và tham khảo các dự án nguồn mở.
Mỗi lớp chỉ có 1 chức năng: Nếu trong ứng dụng cửa hàng trực tuyến của bạn có một lớp như CheckOut dùng để tạo đơn hàng, trong đó có các chức năng: AddProductToCart, RemoveProductFromCart, CreateOrder, SendCheckOutEmails… thì xin chúc mừng, bạn đã làm sai bét. Nguyên nhân là vì bạn đang xây dựng một class vừa để quản lý giỏ hàng, vừa thực hiện việc tạo đơn hàng, vừa gửi email cho khách hàng nữa. Tôi hiểu ý của bạn: vì giỏ hàng là một biến lưu ngay bên trong class này nên khi tạo đơn hàng bạn có thể lấy ra và sử dụng luôn một cách dễ dàng? Vậy nếu tôi muốn giỏ hàng được lưu lại vào DB khi khách hàng đăng nhập thì sao? Phải chăng tôi sẽ thêm một phương thức SaveCart? Rồi nếu hệ thống của tôi to ra, tôi muốn tạo một microservice để quản lý giỏ hàng? Rõ ràng tôi sẽ phải thêm code để làm việc với service ngay trong lớp CheckOut, vốn chẳng có liên hệ gì với cái microservice kia.
Trong trường hợp này, hãy tạo ra lớp Cart để chứa nội dung giỏ hàng, lớp CartService để cung cấp các chức năng như lưu lại giỏ, đọc nội dung giỏ hàng, lớp CheckOut sẽ có CreateOrder với tham số là Cart (và các thông tin cần thiết khác)… như vậy sau này khi cần thay đổi gì sẽ rất đơn giản. Nên nhớ, trong một hệ thống lớn có thể có hàng ngàn class, hàng trăm table, một table có thể chứa hàng triệu dòng, sửa một lỗi không đơn giản là cập nhật lại code rồi copy lên server đâu.
Một lớp không được phép thay đổi chức năng khi mở rộng (thừa kế): tức là nếu cùng một tham số, cùng lời gọi hàm… tất cả các điều kiện giống nhau thì phải ra cùng kết quả, kể cả trong lớp thừa kế cũng vậy – hay nói cách khác, lớp thừa kế chỉ mở rộng chứ không thay đổi lớp cơ sở.
Tận dụng tối đa tính trừu tượng (abstraction): 4 đặc tính trong OOP luôn bổ trợ cho nhau, bạn không có thừa kế thì không thể có đa hình (polymorphism), để đảm bảo dữ liệu được toàn vẹn ta cần có đóng gói… Trừu tượng ở đây nói đến việc ta chỉ quan tâm đến chính xác những gì ta cần mà không cần biết đằng sau nó là gì, nó làm thế nào. Ví dụ: chỉ cần biết ILogger có các method Debug, Info là ta dùng được mà không cần biết làm sao nó ghi dữ liệu, nó ghi dữ liệu vào đâu… đó là nhiệm vụ của các lớp khác. Hay nếu biết ICache cung cấp các phương thức: Get, Save để lấy hoặc lưu dữ liệu từ bộ nhớ cache, ta chỉ cần gọi mà không cần quan tâm nó cache vào đâu: memory, memcached, redis, mongodb… Việc đi vào chi tiết sẽ do lớp MemoryCache, RedisCache, MemcachedCache… làm, mỗi lớp sẽ tự thực hiện các method Get, Save theo cách của nó, việc chọn loại cache nào sẽ có method GetCache của lớp CacheManager làm. Thậm chí CacheManager cũng có thể phân tích thành ICacheManager, sau này ta có thể có XmlCacheManager và JsonCacheManager. Tính trừu tượng cao cũng vừa giúp sau này mở rộng dễ dàng, vừa giúp giảm sự phụ thuộc giữa các lớp với nhau, từ đó cũng giảm các side effect của lỗi hoặc thay đổi sau này.
Một trong những ưu điểm cần nhắc đến cho tính trừu tượng là bạn có thể viết code có-thể-test-được. Tưởng tượng nếu lớp CategoryService phải dựa vào lớp RedisCache, và lớp RedisCache lại phải kết nối vào 1 máy chủ Redis để hoạt động, vậy mỗi lần muốn test CategoryService, bạn phải setup một máy chủ Redis, mà bạn cũng không chắc máy chủ Redis đó mỗi lần chạy test có đang ở một trạng thái giống nhau hay không nữa. Trong khi đó, nếu CategoryService chỉ phụ thuộc vào ICache, khi chạy test ta chỉ cần truyền vào 1 MemoryCache là được.
Sử dụng Inversion of Control (IoC): IoC là một design pattern rất phổ biến, ý tưởng là các component khi chạy sẽ chỉ cần được cung cấp các tham số, thuộc tính cần thiết là chạy, chúng không bao giờ quan tâm tới việc các tham số đó được tạo ra thế nào, giống như ICache ở trên, khi một object CategoryService được tạo ra, chúng sẽ được cung cấp 1 cache (ICache) để nó chỉ việc sử dụng mà không cần biết cache đó lưu vào đâu, IoC container sẽ làm phần còn lại (bạn có thể tìm hiểu thêm về dependency injection, Autofac…).
Thiết kế là một quá trình phức tạp và đòi hỏi nhiều kinh nghiệm, ta luôn phải cân nhắc giữa nhiều yếu tố: hiệu năng, toàn vẹn dữ liệu, bảo mật, mô hình hoạt động, kỹ năng của các lập trình viên, chi phí… Kết quả cũng rất khác nhau giữa những người thiết kế khác nhau, và sự khác nhau đó đôi khi chỉ có thể thấy được khi ứng dụng trở nên cồng kềnh, hoặc lượng dữ liệu, lượng truy cập tăng lên, hoặc thậm chí trong trường hợp xảy ra sự cố.
VẬY TÔI CẦN HỌC GÌ
Trước tiên bạn cần học code cho thật tốt, hiểu về nền tảng mình đang nhắm tới (.NET, PHP, Java…), hãy học code không dựa trên bất kỳ framework nào – bạn hãy học cách người ta tạo nên các framework thay vì học cách sử dụng nó.
Tìm hiểu về các design pattern. Có rất nhiều design pattern đang được dùng, một số rất đơn giản và phổ biến: Skeleton, Singleton, Factory… một số phức tạp hơn: IoC, Unit of Work, Repository… Cố gắng nắm vững chúng và sử dụng trong các ứng dụng của mình. Bạn có thể tìm đến web site của ngài Martin Fowler, hay truy cập vào đây để xem các ebook và tài liệu thiết kế từ Microsoft: https://dotnet.microsoft.com/…/dotnet/architecture-guides.
Tìm hiểu về các dạng kiến trúc hệ thống: SOA, 3-tier, microservice sẽ giúp bạn có cái nhìn rộng hơn.
Tham khảo các dự án mã nguồn mở. Vô số và vô số ứng dụng mã nguồn mở được dùng bởi hàng triệu người, một số đã hoạt động hàng chục năm qua và trở nên rất phổ biến. Điều đó chứng tỏ chúng được thiết kế rất tốt, tìm hiểu xem người ta thiết kế CSDL thế nào, tổ chức các module ra sao, viết code sao cho đẹp và cả cách người ta viết tài liệu nữa.Tóm lại, hãy bắt tay vào làm, nghĩ ra đề bài mà làm, đừng chờ đến khi xin việc người ta đòi hỏi kinh nghiệm lại đổ thừa chưa đi làm sao có kinh nghiệm.Mắc cỡ nhất là cuộc đời của mình lại đi đổ thừa người khác.