Luồng điều khiển cấu trúc với lập trình hướng đối tượng (Phần 2)

Apr 09 2022
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 thay vì cách tiếp cận Hình dạng, Hình tròn, Hình vuông rất điển hình.

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 Shapehình .CircleSquare

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

  • ifcác câu lệnh
  • if-else-ifcác câu lệnh
  • switch-casecác câu lệnh
Danh sách mã bắt đầu của chúng tôi mô tả miền sự cố của chúng tôi. Dòng 17–19 là nơi chúng ta sẽ tập trung.

Đ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 đã xáo trộn các điều kiện if-else để làm cho mã liệt kê gọn gàng hơn một chút, nhưng đây là mã rất điển hình cho logic như vậy.

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:

Một lớp cơ sở trừu tượng không thể được khởi tạo trực tiếp và xác định một hợp đồng và khuôn mẫu cho cách tất cả các chiến lược vận chuyển phải hoạt động.

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 extendstừ khóa. Lưu ý những gì sẽ xảy ra khi chúng tôi gia hạn ShippingStrategy:

Intellisense của chúng tôi phát hiện ra rằng chúng tôi có lỗi trong quá trình triển khai của mình vì việc triển khai của chúng tôi không đầy đủ theo hợp đồng.

Lớp trừu tượng định nghĩa một hợp đồng - bất kỳ abstractthà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.

Lưu ý cách lớp cơ sở cung cấp hợp đồng cho chúng ta.

Giờ đây, chúng tôi có thể chuyển logic của mình vào UspsShippingStrategyvà 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ở:

Các phương thức lớp cơ sở có thể được ghi đè tùy chọn để cung cấp logic tùy chỉnh trong các lớp cụ thể.

Một điều thú vị cần lưu ý là tham chiếu đến this.weightthis.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:

Logic của chiến lược vận chuyển UPS (không nên nhầm lẫn với USPS).

Lưu ý rằng chúng tôi đã không ghi đè lên bộ nhận surchargevì 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ả:

Nó trông có vẻ phức tạp, nhưng chúng tôi chỉ ánh xạ các cấu trúc của chúng tôi với các phương thức vận chuyển của chúng tôi.

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à surchargetà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 iOSPhonehoặ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:

Chúng tôi sẽ truy cập lại mã trên dòng 11, nơi phụ phí được áp dụng.

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:

Chỉ cần áp dụng phụ phí ở dòng 15 khi chúng tôi tính giá.

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 calculatephương thức:

Lưu ý những thay đổi trên dòng 16 bằng cách áp dụng công cụ sửa đổi được bảo vệ và bổ sung một phương thức mới trên dòng 18

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 calculatephương thức thành protectedvà 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:

Chúng tôi không thể truy cập phương thức này nữa vì chúng tôi đã đánh dấu nó là được bảo vệ.

Đâ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, protectedpublic.

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 protectedvà tách thuật toán của chúng tôi khỏi API bên ngoài.

Và đây là Orderlớp học cuối cùng của chúng tôi:

Lưu ý sự thay đổi trên dòng 17.

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.MasterCardProcessorDiscoverProcessor
  • Thù lao cho nhân viên - HourlyCompensationStrategy,SalariedCompensationStrategyContractorCompensationStrategy
  • Quản lý giá của nhà cung cấp - AmazonPricingStrategy,, v.v.WalmartPricingStrategyTargetPricingStrategy

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

  1. 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
  2. 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)

© Copyright 2021 - 2023 | vngogo.com | All Rights Reserved