Tìm hiểu thiết kế hướng đối tượng trong Rails Phần 3
Bài đăng này đã không được cập nhật trong 8 năm
Tìm hiểu thiết kế hướng đối tượng trong Ruby on Rails (Phần III)
I. Giới thiệu
Trong phần trước đã giới thiệu qua về thiết kế lớp với tiếu chí một chức năng duy nhất. Trong bài viết này, chúng ta sẽ tìm hiều quy tắc tiếp theo trong thiết kế, đó là:
** Quản lý sự phụ thuộc**
II. Nguyên tắc 3: quản lý sự phụ thuộc
Các ngôn ngữ lập trình hương đối tượng hướng đến tính hiệu quả. Các đối tượng phản ánh một vấn đề thực tế và các tương tác giữa các đối tượng đấy cung cấp các giải pháp. Những tương tác đó là không tránh khỏi. Một đối tượng đơn không thể biết mọi thứ, vì vậy nó sẽ phải liên hệ với đối tượng khác.
Nếu bạn có thể nhìn kỹ một ứng dụng phức tạp và theo dõi các thông điệp mà chúng gửi đi, lộ trình đó là rất đáng kể. Sẽ có rất nhiều thứ thực hiện. Tuy nhiên nếu bạn nhìn lại và quan sát toàn cảnh, một khuôn mẫu sẽ trở nên rõ ràng hơn. Mỗi thông điệp được khởi đầu bởi một đối tượng để gọi ra vài phản ứng. Tất cả phản ứng được gói trong các đối tượng. Do đó, với một vài phản ứng mong muốn hơn, một đối tượng vừa biết về nó, thừa kế nó, vừa biết đối tượng khác cũng biết về nó.
ở chương trước đã đề cập vấn đề thứ nhất, các phản ứng của một lớp nên thực hiện riêng lẻ. Thứ hai, kế thừa các hành vi, sẽ được mô tả trong chương 6. Trong chương này, sẽ đề cập tới các hành vi mà được thực hiện trong các đối tượng khác.
Bởi vì các đối tượng được thiết kế với một chức năng duy nhất, điều đó yêu cầu chúng phải liên kết để thực hiện các công việc phức tạp hơn. Sự kết hợp này rất mạnh mẽ. Để liên kết, một đối tượng phải biết thông tin về đối tượng khác. Điều đó nghĩa là tạo ra một sự phụ thuộc. Nếu không quản lý cẩn thận, những sự phụ thuộc này sẽ làm phá vỡ ứng dụng của bạn.
Hiểu về sự phụ thuộc
Một đối tượng phụ thuộc đối tượng khác nếu, khi một đối tượng thay đổi, đối tượng kia bắt buộc phải thay đổi.
Ví dụ đây là một bản thay đổi của lớp Gear, khi Gear được khởi tạo với 4 đối tượng. Lớp gear_inches
sử dụng 2 trong số chúng, rim và tire, để tạo một thể hiện của lớp Wheel.
class Gear
attr_reader :chainring, :cog, :rim, :tire
def initialize(chainring, cog, rim, tire)
@chainring = chainring
@cog = cog
@rim = rim
@tire = tire
End
def gear_inches
ratio * Wheel.new(rim, tire).diameter
end
def ratio
chainring / cog.to_f
end
end
class Wheel
attr_reader :rim, :tire
def initialize(rim, tire)
@rim = rim
@tire = tire
end
def diameter
rim + (tire * 2)
end
end
Gear.new(52, 11, 26, 1.5).gear_inches
TÌm hiểu đoạn code trên và tạo ra một danh sách các tình huống mà Gear sẽ phải bắt buộc thay đổi bởi vì sự thay đổi của Wheel. Đoạn code trên dường như không có gì nguy hiểm nhưng nó phức tạp. Gear có ít nhất 4 sự phụ thuộc với Wheel. Hầu hết những sự phụ thuộc là không cần thiết, nó ảnh hưởng đến phong cách code. Gear không cần chúng để thực hiện chức năng của mình. Sự tồn tại của chúng làm cho Gear trở nên khó để thay đổi hơn.
Nhận ra những sự phụ thuộc
Một đối tượng có một sự phụ thuộc khi nó biết:
- Tên của lớp khác. Gear trông chờ một lớp tên là Wheel tồn tại.
- Tên của một tin nhắn mà gửi đến người khác hơn là chính mình. Gear trông chờ một thể hiện Wheel phản hồi diameter.
- Các tham số mà một tin nhắn yêu cầu. Gear biết hàm Wheel.new yêu cầu một rim và một tire
- Thứ tự của các tham số. Gear biết tham số đầu tiên của Wheel.new là rim, thứ hai là tire. Mỗi sự phụ thuộc đó tọa một khả năng là Gear sẽ bắt buộc phải thay đổi bởi vì Wheel thay đổi. Một vài thay đổi đổi giữa 2 lớp là không tránh khỏi, chúng sẽ tạo nên các liên kết, nhưng hầu hết các phụ thuộc trên là không cần thiết. Những phụ thuộc không cần thiết khiến cho code it hợp lý hơn. Bởi vì chúng làm tăng khả năng mà Gear bắt buộc phải thay đổi.
Thử thách cho thiết kế của bạn là quản lý các sự phụ thuộc để mỗi lớp có ít nhất khả năng, một lớp nên chỉ biết đủ về công việc của nó và không nhiều hơn một.
Kết hợp các đối tượng (CBO)
Những sự phụ thuộc đó kết hợp Gear với Wheel. Thêm nữa, bạn có thể nói là mỗi cặp liên kết tạo ra một sự phụ thuộc. Gear càng biết nhiều về Wheel, thì chúng càng liên kết chặt chẽ hơn. Liên kết càng chặt giữa 2 đối tượng, chúng càng giống như một thực thể.
Nếu bạn tạo ra một sự thay đổi với Wheel, bạn có thể thấy cần phải thay đổi cả Gear. Nếu bạn muốn dùng lại Gear, Wheel cũng cần phải đi kèm. Khi bạn kiểm tra Gear, bạn cũng cần phải kiểm tra cả Wheel nữa.
Khi 2 hay nhiều đối tượng có liên kết chặt chẽ, chúng sẽ thực hiện như một đơn vị, và không thể tái sử dụng một đối tượng riêng lẻ. Sự thay đổi với một đói tượng sẽ theo sau là tất cả. Những sự phụ thuộc không được quản lý sẽ dẫn đến toàn bộ ứng dụng trở nên hỗn loạn. Tới một ngày bạn sẽ thấy dễ dàng hơn là viết lại mọi thứ hơn là thay đổi vài chỗ.
Những sự thay đổi khác
Phần còn lại của chương này sẽ đề cập đến 4 kiểu của phụ thuộc được liệt kê ở trên và đưa ra vài kỹ thuật phòng tránh những vấn đề phát sinh. Tuy nhiên, trước khi tiến vào tìm hiểu, ta sẽ nó sơ qua một vài sự phụ thuộc phổ biến liên quan đến các vấn đề sẽ được tìm hiểu trong các chương tiếp theo.
Một kiểu phụ thuộc xảy ra khi một đối tượng biết đối tượng khác mà nó biết đối tượng khác nữa biết một vài thứ, nghĩa là một vài thông điệp móc nối với nhau để đến các hành vi nằm ở một đối tượng ở xa. Thông điệp moc nối với nhau tạo ra một sự phụ thuộc giữa đối tượng ban đầu với một đối tượng và thông điệp dọc con đường đi tới mục tiêu. Các liên kết bổ sung đấy sẽ làm tăng khả năng một đối tượng bắt buộc phải thay đổi bởi vì một sự thay đổi của một vài đối tượng trung gian.
Trường hợp này sẽ vi phạm luật Demeter
Những sự phụ thuộc khác có thể kể đến là quá trình test. Ngoài phạm vi quyển sách này, test sẽ tiến hành đầu tiên. Chúng hình thành nên thiết kế. Tuy nhiên, chúng lại liên quan đến code và do đó phụ thuộc vào code. Với những lập trình viên mới sẽ viết các đoạn test quá liên kết với code. Những liên kết đó sẽ dẫn tới ảnh hưởng vô cùng lớn, các test sẽ phá vỡ mọi nỗ lực khi code được refactor, thậm chí cả hành vi gốc của code không thể thay đổi. Sự liên kết này là những phụ thuộc dẫn đến sự thay đổi code ảnh hưởng đến test, bắt buộc chúng phải thay đổi theo.
Viết các đoạn code ít liên quan
Mọi phụ thuộc như là một miếng keo dán làm cho các lớp của bạn dán mọi thứ mà nó chạm vào. Một vài miếng là cần thiết, nhứng khi quá nhiều thì ứng dụng của bạn sẽ thành một khối cứng chắc. Giảm bớt phụ thuộc nghĩa là nhận ra và loại bỏ những thứ không cần thiết.
Ví dụ bên dưới mô phỏng các kỹ thuật code giảm bớt các sự phụ thuộc bằng cách tách code
Inject dependency
Trong phiên bản bên dưới của lớp Gear, hàm gear_inches
chứa một tham chiếu với lớp Wheel:
def gear_inches
ratio * Wheel.new(rim, tire).diameter
end
Có một vấn đề là nếu tên của lớp Wheel thay đổi, hàm gear_inches cũng phải thay đổi.
Nhưng sự thay đổi này là rất nhỏ. Nếu một Gear cần liên hệ với một Wheel, nó phải tạo ra một thể hiện mới của lớp Wheel. Nếu Gear tự nó đã biết tên của lớp Wheel, code trong Gear phải được thay thế nếu đổi tên của Wheel.
Thực tế, đối diện với vấn đề thay đổi tên rất đơn giản. Bạn có thể có công cụ cho phép tìm kiếm và thay thế trong toàn bộ project. ĐIều đó không quá khó khăn. Tuy nhiên, với ratio * Wheel.new(rim, tire).diameter
lại phát sinh một vấn đề lớn hơn.
Khi Gear để cố định tham chiếu đến Wheel trong hàm gear_inches
, nghĩa là nó sẽ chỉ thực hiện tính toán với đối tượng của Wheel. Gear từ chỗi mọi liên kết với các kiểu lớp khác, thậm chí đói tượng có một diameter và sử dụng gear.
Nếu ứng dụng mở rộng chứa cả các đối tượng như disk hay cylinder và bạn cần biết gear inch của gear mà chúng sử dụng, bạn không thể làm được. Mặc dù thực tế là disk và cylinder vốn có một diameter ban có thể không bao giờ tính được gear inch bởi vì Gear được gắn cho Wheel.
Đoạn code trên thực hiện đính kèm với một lại cố định. Không quan trong là lớp của đối tượng, mà là thông điệp bạn định gửi đi. Gear cần tiếp cận một đối tượng mà có thể gửi lại diameter. Gear không quan tâm và nên biết về lớp của đối tượng khác. Điều đó không cần thiết cho Gear biết về sự tồn tại của lớp Wheel để tính gear_inches
.
Lưu giữ những sự phụ thuộc không cần thiết Gear sẽ làm giảm bớt khả năng tái sửu dụng và làm tăng sự bắt buộc phải thay đổi không cần thiết. Gear trở nên ít hữu dụng hơn khi biết quá nhiều về các đối tượng khác, nếu biết ít hơn thì nó có thể làm nhiều thứ hơn.
Thay vì được gắn với Wheel, phiên bản kế của Gear được khởi tạo với một đối tượng có thể thực hiện diameter:
class Gear
attr_reader :chainring, :cog, :wheel
def initialize(chainring, cog, wheel)
@chainring = chainring
@cog = cog
@wheel = wheel
end
def gear_inches
ratio * wheel.diameter
end
end
# Gear expects a ‘Duck’ that knows ‘diameter’
Gear.new(52, 11, Wheel.new(26, 1.5)).gear_inches
Sự thay đổi này là nhỏ nhưng có ảnh hương lớn. Chuyển quá trình khởi tạo Wheel ra ngoài Gear. Gear bây giờ có thể kết hợp với một vài đối tượng để thực hiện diameter. Không cần phải thêm dòng code, tách liên kết được thực hiện nhờ sắp xếp lại code.
Cô lập các phụ thuộc:
Tốt nhất là chia nhỏ tất cả các phụ thuộc không cần thiết nhưng thật không may trong khi đấy luôn là khả năng đấy kỹ thuật nhưng thực tế không khả thi. Khi làm việc với một ứng dụng sẵn có, bạn có thể thấy một vài giới hạn số lượng mà bạn có thể thực sự thay đổi. Nếu ngăn chặn việc đạt đến sự hoàn hảo, mục tiêu của bạn nên chuyên sang cải thiện thổng thể bằng việc loại bỏ code tốt hơn là tìm chúng.
Do đó, nếu bạn không thể loại bỏ những sự phụ thuộc không cần thiết, bạn nên cô lập chúng trong lớp của bạn. Trong chương 2, thiết kế các lớp với một chức năng duy nhất, bạn đã cô lập các trách nhiệm để chúng có thể dễ dàng nhận ra và loại bỏ khi cần thiết, bạn nên loại bỏ những sự phụ thuộc không cần thiết để chúng dễ dàng đánh dấu và loại bỏ.
Cô lập quá trình tạo ra đối tượng
Nếu bạn bị giới hạn không thể thay đổi code để tach Wheel từ một Gear, bạn nên cô lập quá trình tạo ra một đối tượng của Wheel trong lớp Gear.
Đầu tiên, quá trình tạo ra đối tượng của Wheel được chuyển từ hàm gear_inches
sang hàm khởi tạo của Gear. Làm giảm bớt cho hàm gear_inches
và đẩy sự phụ thuộc và hàm initialize của Gear. Khi đó sẽ luôn tạo ra một đối tượng của Wheel khi Gear được tạo ra.
def initialize(chainring, cog, rim, tire)
@chainring = chainring
@cog = cog
@wheel = Wheel.new(rim, tire)
end
Ta cũng có thể thực hiện tạo ra đối tượng của Wheel trong hàm wheel và sử dụng toán tử ||= của Ruby. Trong trường hợp này, quá trình tạo ra một đối tượng của Wheel được thực hiện khi hàm gear_inches
gọi hàm wheel
def wheel
@wheel ||= Wheel.new(rim, tire)
end
Cả 2 ví dụ trên, Gear vẫn biết quá nhiều, nó vẫn cần rim và tire như các tham số khởi tạo và nó vẫn tạo ra đối tượng mới của Wheel. Gear vẫn gắn với Wheel, nó không thể tính toán gear inches với các đối tượng khác.
Tuy nhiên, một cải tiến đã được thực hiện. Với phong cách code sẽ giảm bớt sự phụ thuộc của gear_inches
trong khi vẫn thể hiện sự phụ thuộc của Gear vào Wheel. Chúng tiết lộ thay vì giấu đi, giảm bớt bức tường để tái sử dụng và làm cho code dễ dàng được refactor khi cho phép. Sự thay đổi này làm cho code trở nên nhanh nhạy hơn, nó có thể dễ dàng đáp ứng với các khả năng trong tương lai.
Cách mà bạn quản lý sự phụ thuộc của tên các lớp bên ngoài có ảnh hưởng lớn đến ứng dụng của bạn. Nếu bạn chú ý những phụ thuộc và phát triển thói quen loại bỏ chúng, các lớp của bạn sẽ ít các liên kết. Nếu bạn bỏ qua vấn đề này và cho phép các tham chiếu bừa bãi, ứng dụng của bạn sẽ như là một tấm dệt không lồ hơn là một tập các đối tượng độc lập. Một ứng dụng mà các lớp của nó được hình thành tứ các tham chiếu không rõ ràng thì sẽ không linh động, trông khi một ứng dụng mà các lớp phụ thuộc một cách hợp lý, và bị cô lập có thể dễ dàng đáp ứng với các yêu cầu mới.
Cô lập các thông điệp dễ gây sai sót
Bây giờ bạn cô lập các tham chiếu đến các lớp bên ngoài, giờ là lúc bạn tập trung dến các thông điệp bên ngoài, đó là các thông điếp gửi cho ai đó chứ không phải là chính bạn. Ví dụ, hàm gear_inches
gửi ratio và wheel đến chính nó , nhưng gửi diameter đến wheel:
def gear_inches
ratio * wheel.diamter
end
Đây là một hàm đơn giản và nó chỉ chứa tham chiếu của Gear với wheel.diameter. Trong trường hợp này, code trên là tốt, nhưng với tính huống phức tạp hơn. Tưởng tượng là tính toán gear_inches
yêu cầu nhiều công thức hơn và hàm có thể trông như:
def gear_inches
#... a few lines of scary math
foo = some_intermediate_result * wheel.diameter
#... more lines of scary math
end
Bây giờ wheel.diamter trở thành một hàm phức tạp. Hàm phức tạp này phụ thuộc vào Gear liên hệ với wheel và với wheel liên hệ với diameter. Nhúng tất cả các phụ thuộc bên ngoài vào hàm gear_inches
là không cần thiết và làm tăng nguy cơ gây sai sót.
Bất cứ khi nào bạn thay đổi một vài thứ, bạn lại đứng trước khả năng tạm dừng, gear_inches
giờ quá phức tạp và điều đó khiến cho nó vừa cần thay đổi vừa có nhiều khả năng bị lỗi khi thực thi. Bạn có thể làm giảm bớt nguy cơ bằng cách tạo một thay đổi với gear_inches
bằng cách xóa sự phụ thuộc bên ngoài và đóng gói nó trong một hàm:
def gear_inches
#... a few lines of scary math
foo = some_intermediate_result * diameter
#... more lines of scary math
end
def diameter
wheel.diameter
end
Hàm diameter mới chính là hàm mà bạn nên viết lại nếu có quá nhiều tham chiếu đến wheel.diameter qua Gear và bạn muốn thực hiện DRY.
Đoạn code ban đầu, gear_inches
biết wheel có một diameter. Thông tin này là một sự phụ thuộc nguy hiểm tạo ra liên kết đến một đối tượng bên ngoài. Sau khi với thay đổi này, gear_inches
trở nên trừu tượng hơn, Gear bây giờ cô lập với wheel.diamter trong một hàm riêng biệt và gear_inches
phụt thuộc vào một thông điệp được gửi từ chính nó.
Nếu Wheel thay đổi tên hay sự thực thi của diameter, những ảnh hưởng đến Gear sẽ được thực hiện chỉ với một hàm đơn giản.
Kỹ thuật này trở nên cần thiết khi một lớp chứa các tham chiếu. Cô lập tham chiếu sẽ khiến đảm bảo chống lại ảnh hưởng của sự thay đổi.
III. Summary
Trên đây là nguyên tắc quản lý sự phụ thuộc trong thiết kế hàm.
Hẹn gặp lại trong phần tới ta sẽ tìm hiểu về cách sử dụng linh hoạt giao diện trong quá trình thiết kế phần mềm
Tài liệu tham khảo:
All rights reserved