Luồng điều khiển cấu trúc với lập trình hướng đối tượng (Phần 2)
Tiền đề của hai bài viết này là một chút tưởng tượng lại cách chúng ta nghĩ về các mô hình lập trình hướng đối tượng và những lợi ích mà chúng ta có thể đạt được thông qua việc áp dụng chúng.
Chúng tôi sẽ có một cái nhìn thực tế và thực dụng hơn về OOP hơn là cách tiếp cận học thuật điển Shape
hình .Circle
Square
Trong bài trước , chúng ta đã nói về một số lý do đơn giản để áp dụng cách tiếp cận hướng đối tượng, ngay cả trong các ngôn ngữ hướng chức năng như JavaScript và TypeScript. Một lợi ích đơn giản và rất có thể đạt được của việc đóng gói hướng đối tượng là khả năng phát hiện và giúp chúng ta tránh các hành vi phân kỳ trở nên phổ biến khi cơ sở mã tuân theo mẫu Tập lệnh giao dịch .
Nhưng đối với tôi, lợi ích chính của tư duy hướng đối tượng là nó cung cấp cho bạn khả năng thay đổi luồng điều khiển thủ tục thành luồng điều khiển cấu trúc thông qua các mẫu thiết kế hành vi và thường xuyên, điều này làm cho mã phức tạp dễ dàng hơn trong việc lập luận, mở rộng và duy trì. .
Tôi đã chọn sử dụng TypeScript trong các ví dụ cụ thể bởi vì nó nằm giữa ranh giới giữa các mô hình chức năng và đối tượng và các nhà phát triển thường bỏ qua các khả năng hướng đối tượng với TypeScript, nhưng các kỹ thuật và mẫu thiết kế áp dụng cho bất kỳ ngôn ngữ nào hỗ trợ các nguyên tắc hướng đối tượng .
Luồng cấu trúc so với thủ tục
Tất cả các nhà phát triển đều quen thuộc với quy trình kiểm soát thủ tục. Đây là những cấu trúc cốt lõi mà tất cả chúng ta đều được dạy đầu tiên khi bắt đầu viết mã:
if
các câu lệnhif-else-if
các câu lệnhswitch-case
các câu lệnh

Điều chúng tôi quan tâm là calculateShippingCost()
phương pháp trên dòng 17–19. Mỗi phương thức vận chuyển của chúng tôi có một bộ quy tắc khác nhau dựa trên:
- Nhà cung cấp vận chuyển
- Trọng lượng của sản phẩm theo từng cấp cụ thể cho từng phương thức vận chuyển
- Và có thể là một khoản phụ phí nếu có chất lỏng trong gói

Tôi chắc chắn rằng mã này trông sẽ quen thuộc bởi vì mọi cơ sở mã đều có một số khối điều kiện 100–200 dòng khổng lồ như thế này (và bằng cách nào đó, có một người có thể đọc nó mà không cần phải nhíu mi!).
Loại mã này có nhiều vấn đề. Đối với người mới bắt đầu, mã rất khó đọc. Chúng tôi chỉ giải quyết một số biến thể khác nhau và nó đã khá phức tạp và hơi cẩu thả. Thứ hai, chúng ta có thể thấy rằng mã này sẽ là một thách thức để duy trì. Nếu chúng tôi cung cấp nhiều tùy chọn giao hàng hơn trong tương lai hoặc chúng tôi có các quy tắc mới cho các phương thức vận chuyển hiện tại, chúng tôi sẽ phải thêm nhiều nhánh hơn vào tuyên bố điều kiện này (ví dụ: nếu chúng tôi muốn thêm phí vận chuyển quốc tế).
Đây là nơi chúng ta có thể chuyển đổi logic thủ tục này sang logic cấu trúc bằng cách sử dụng các nguyên tắc thiết kế hướng đối tượng. Trong trường hợp này, chúng tôi sẽ sử dụng Mô hình Chiến lược kết hợp với một biến thể của Mô hình Nhà máy để đóng gói các quy tắc của chúng tôi.
Chúng tôi sẽ tạo một lớp cơ sở trừu tượng mô hình hóa các hành vi và thuộc tính chung của tất cả các chiến lược vận chuyển; hãy nghĩ về nó như một mẫu xác định cách chúng tôi có thể sử dụng bất kỳ chiến lược vận chuyển nào:

Và bây giờ chúng ta có thể triển khai các lớp cụ thể đại diện cho từng phương thức. Trong TypeScript, điều này được thực hiện bằng cách sử dụng extends
từ khóa. Lưu ý những gì sẽ xảy ra khi chúng tôi gia hạn ShippingStrategy
:

Lớp trừu tượng định nghĩa một hợp đồng - bất kỳ abstract
thành viên nào cũng phải được thực hiện - và do đó, việc triển khai của chúng ta không hoàn chỉnh cho đến khi chúng ta triển khai một calculate()
phương thức.
Điều này mạnh mẽ hơn những gì tưởng tượng. Bằng cách xác định hợp đồng, nó cung cấp gợi ý cho những người khác làm việc trong cơ sở mã về cách tạo chiến lược vận chuyển mới trong tương lai và hợp đồng đó chứa hành vi của calculate()
phần còn lại của logic đơn hàng.

Giờ đây, chúng tôi có thể chuyển logic của mình vào UspsShippingStrategy
và tách biệt hoàn toàn khỏi các chiến lược vận chuyển khác của chúng tôi! Điều này có nhiều lợi ích bao gồm dễ dàng kiểm tra chiến lược cũng như cách ly tốt hơn mã khỏi kiểm tra hồi quy.
Vì USPS yêu cầu phụ phí vận chuyển chất lỏng, chúng tôi cũng có thể chuyển logic này vào chiến lược bằng cách ghi đè hành vi của lớp cơ sở:

Một điều thú vị cần lưu ý là tham chiếu đến this.weight
và this.hasLiquid
. Chúng được lớp cơ sở tự động cung cấp cho mã của chúng tôi ShippingStrategy
.
Đối với mỗi phương thức vận chuyển, sau đó chúng tôi có thể thực hiện một chiến lược khác nhau:

Lưu ý rằng chúng tôi đã không ghi đè lên bộ nhận surcharge
vì UPS không tính phụ phí vận chuyển chất lỏng. Trong trường hợp này, nó sẽ chỉ đơn giản là lấy việc triển khai từ lớp cơ sở trả về hệ số nhân 1x.
Bây giờ chúng tôi đã có các chiến lược của mình, chúng tôi cần một cơ chế ánh xạ chuỗi phương thức vận chuyển thành một chiến lược. Để làm như vậy, chúng ta thường chuyển sang cái được gọi là Factory Pattern , nhưng chúng ta không cần phải quá cầu kỳ. Trong trường hợp này, chúng ta có thể tạo một bản đồ đơn giản và tạo một nhà máy giả:

Nhà máy trả về lớp trừu tượng ShippingStrategy
chứ không phải lớp cụ thể. Đó là bởi vì mọi chiến lược vận chuyển cụ thể đều thực hiện hợp đồng được xác định bởi lớp cơ sở trừu tượng và do đó, người gọi của chúng ta không cần biết chính xác nó đang làm việc với lớp nào vì hợp đồng đảm bảo rằng các tương tác của chúng ta với lớp trừu tượng là hợp lệ với bất kỳ của các lớp cụ thể.
Nói cách khác, chúng tôi có một đảm bảo rằng bất kỳ chiến lược vận chuyển nào cũng sẽ cung cấp cho chúng tôi một calculate()
phương thức và surcharge
tài sản. Bạn có thể nghĩ về nó giống như yêu cầu bạn bè của bạn mượn của họ Phone
để làm cho một call()
. Nó có thể là một iOSPhone
hoặc AndoidPhone
; cả hai đều thực hiện hợp đồng của Phone.call()
.
Điều này cho phép chúng ta làm là chuyển đổi logic thủ tục cao của chúng ta calculateShippingCost()
thành logic cấu trúc:

Có một số lợi ích chính hiện nay:
- Logic của
calculateShippingCost()
phương pháp rất rõ ràng và dễ hiểu vì chúng ta đã cô lập hoặc đóng gói logic của mỗi chiến lược trong một lớp. - Thêm một chiến lược vận chuyển mới rất dễ dàng và được cách ly tốt; chúng tôi không còn phải lo lắng về việc phá vỡ các chiến lược vận chuyển khác nếu chúng tôi mắc lỗi trong logic điều kiện.
- Khi thêm chiến lược vận chuyển mới - giả sử Uber Local - chúng tôi không phải kiểm tra hồi quy
calculateShippingCost()
phương pháp này vì chúng tôi hoàn toàn không chạm vào mã đó! - Nếu chúng ta cần thực hiện một thay đổi chung cho tất cả các chiến lược vận chuyển, chúng ta có thể thực hiện nó ở lớp cơ sở hoặc chúng ta có thể thực hiện nó trong lớp
calculateShippingCost()
.
Tập trung hóa Logic chung
Bởi vì chúng tôi luôn muốn áp dụng phụ phí (cho dù là 1x hay 1.25x), nên chúng tôi muốn thay đổi chiến lược USPS của chúng tôi như sau:

Nhưng làm như vậy có nghĩa là bất kỳ ai thực hiện logic đều phải nhớ áp dụng hệ số nhân . Việc có lớp cơ sở trừu tượng cung cấp cho chúng ta một cơ chế khác để làm như vậy giúp quản lý dễ dàng hơn bằng cách gói lời gọi vào calculate
phương thức:

Một lần nữa, chúng tôi sử dụng cấu trúc của lớp để giúp xác định logic của chúng tôi.
Ở dòng 16, chúng tôi đã thay đổi calculate
phương thức thành protected
và thêm một phương thức mới getShippingPrice()
thực sự gọi phương thức. Lợi ích chính của phương pháp này là nó cho phép chúng ta có cơ hội tập trung logic cho những thứ như thêm phép đo từ xa và ghi nhật ký như chúng ta đã làm ở dòng 20.
Nếu chúng ta nhìn vào trang web cuộc gọi của mình bên dưới, chúng ta có thể thấy rằng calculate()
phương thức này hiện không thể truy cập được nữa:

Đây là một lợi ích khác của lập trình hướng đối tượng và cách sử dụng hợp lý các công cụ sửa đổi quyền truy cập như private
, protected
và public
.
Mặc dù điều này có vẻ tầm thường nhưng điều này cho phép chúng tôi làm là thông báo cho người gọi của chúng tôi - bạn biết đấy, đồng đội và cộng tác viên của chúng tôi - những thao tác nào được phép trên đối tượng bằng cách chỉ định logic nội bộ của phép tính protected
và tách thuật toán của chúng tôi khỏi API bên ngoài.
Và đây là Order
lớp học cuối cùng của chúng tôi:

Khả năng áp dụng
Bạn có thể thấy khả năng áp dụng chung của mẫu này trong việc giải quyết nhiều lớp vấn đề với độ phức tạp của mã thường khiến mã khó bảo trì, cấu trúc lại và kiểm tra.
Đặc biệt, bất kỳ lúc nào có các ranh giới rời rạc của hành vi dựa trên một yếu tố phân biệt, mẫu Chiến lược và Nhà máy có thể được sử dụng để tổ chức logic một cách cấu trúc thay vì sử dụng logic thủ tục để làm sạch mã.
Những ví dụ bao gồm:
- Xử lý thanh toán -
VisaProcessor
,, v.v.MasterCardProcessor
DiscoverProcessor
- Thù lao cho nhân viên -
HourlyCompensationStrategy
,SalariedCompensationStrategy
ContractorCompensationStrategy
- Quản lý giá của nhà cung cấp -
AmazonPricingStrategy
,, v.v.WalmartPricingStrategy
TargetPricingStrategy
Trong khi lập trình chức năng dường như đang thịnh hành vào thời điểm này, thực tế là lập trình hướng đối tượng thường có thể cung cấp các cách cấu trúc rõ ràng hơn để tổ chức hành vi và logic phức tạp, giúp mã dễ hiểu hơn, dễ bảo trì hơn và dễ mở rộng hơn.
Các mẫu thiết kế hướng đối tượng dường như không được ưa chuộng vì đường cong học tập và kinh nghiệm cần thiết để áp dụng chúng (và khá thẳng thắn, cách trình bày quá hàn lâm trong hầu hết các trường hợp khiến chúng khó tiếp cận). Nhiều nhà phát triển trẻ có thể kết thúc bằng việc nhìn vào các sơ đồ UML để cố gắng giải thích các khái niệm này bởi vì bản thân UML đã trở nên cứng rắn khi các nhóm áp dụng “nhanh nhẹn” và tránh xa tài liệu và thiết kế. Nhưng các mẫu thiết kế này thường được thử nghiệm qua các dự án lớn, phức tạp và có thể giúp các nhóm nhỏ thực hiện những cải tiến đáng kể trong cơ sở mã.
Nếu bạn muốn khám phá sâu hơn, tôi thực sự giới thiệu hai cuốn sách nhỏ về chủ đề:
- Mẫu thiết kế: Các yếu tố của phần mềm hướng đối tượng có thể tái sử dụng
- Các mẫu kiến trúc ứng dụng doanh nghiệp
Hy vọng rằng, sau hành trình ngắn gọn và thực tế này, bạn sẽ thấy rằng các nguyên tắc hướng đối tượng là một công cụ mạnh mẽ - ngay cả khi làm việc với các ngôn ngữ chức năng - và thấy những khái niệm này bớt… trừu tượng hơn một chút.
(Danh sách mã đầy đủ ở bên dưới)