Đối tượng, và Lớp

Lớp là gì?

Là chi tiết kỹ thuật của một tập các object tương tự nhau.

Đối tượng là gì?

Là một tập các hàm đoạt động dựa trên các mảnh dữ liệu được bao gói.

Nói cách khác, một đối tượng là một tập các hàm ám vị về dữ liệu.

Bạch thầy, thầy nói ám vị về dữ liệu nghĩa là sao ạ?

Các hàm của một đối tượng ám thị về sự tồn tại của một dữ liệu nào đó; nhưng dữ liệu đó không truy cập trực tiếp hay nhìn thấy được từ bên ngoài đối tượng.

Ám vị: ám chỉ về sự tồn tại

Không phải là dữ liệu đó nó nằm trong đối tượng ạ?

Có thể có; nhưng không bắt buộc phải như thế. Nhìn từ phía người dùng, một đối tượng không có gì khác hơn là một tập các hàm. Dữ liệu mà các hàm đó cần để hoạt động dĩ nhiên phải tồn tại, nhưng dữ liệu đó được đặt ở đâu thì người dùng không biết được.

Vầng.

Tốt. Thế, giờ, cấu trúc dữ liệu là gì?

Cấu trúc dữ liệu là một tập cố kết của các mảnh dữ liệu.

Hay nói cách khác, cấu trúc dữ liệu là một tập các mảnh dữ liệu được vận hành bởi các hàm ám vị.

Ý thầy là những hàm của cấu trúc dữ liệu không được liệt kê trong chi tiết kỹ thuật của cấu trúc dữ liệu, nhưng sự tồn tại của cấu trúc dữ liệu ám thị rằng chúng hẳn phải tồn tại?

Chuẩn. Giờ con thấy phát hiện ra điều gì về hai định nghĩa vừa rồi?

Chúng đối lập nhau.

Thực ra. Chúng hoàn thiện cái còn lại. Chúng vừa với nhau như tay vừa với găng tay.

  • Một đối tượng là một tập các hàm hoạt động dựa trên các dữ liệu ám vị.
  • Một cấu trúc dữ liệu là một tập các dữ liệu hoạt động dựa trên các hàm ám vị.

Woào, tức là đối tượng không phải là cấu trúc dữ liệu.

Chuẩn. Đối tượng đối lập với cấu trúc dữ liệu.

Vậy các Data Transfer Object không phải là đối tượng?

Đúng vậy. DTO là các cấu trúc dữ liệu.

Và các bảng dữ liệu cũng không phải là đối tượng nốt?

Lại đúng. Cơ sở dữ liệu chứa các cấu trúc dữ liệu, không phải các đối tượng.

Khoan thầy ơi. Em tưởng là ORM ánh xạ các bảng dữ liệu tới đối tượng chứ ạ?

Tất nhiên là không. Không có ánh xạ nào giữa bảng dữ liệu và đối tượng cả. Bảng dữ liệu là cấu trúc dữ liệu. không phải đối tượng.

Thế thì ORM thực ra là làm gì?

Chúng chuyển vận dữ liệu giữa các cấu trúc dữ liệu.

Thế chúng làm gì với các đối tượng?

Không gì hết. Không có thứ gọi là gì là Object Relational Mapper cả; bởi vì chẳng có ánh xạ nào giữa bảng dữ liệu và đối tượng hết.

Ủa con tưởng các ORM nặn các đối tượng nghiệp vụ cho mình mà?

Nâu, các ORM trích xuất dữ liệu mà các đối tượng nghiệp vụ dùng đến. Dữ liệu đó được chứa trong một cấu trúc dữ liệu được dựng nên bởi ORM.

Và rồi các đối tượng nghiệp vụ sẽ chứa đựng các cấu trúc dữ liệu đó phải không ạ?

Có thể có, có thể không. Dù gì đó cũng không phải là việc của ORM.

Có vẻ chỉ là chuyện ngữ nghĩa nhỏ nhặt thôi nghỉ.

Không hề. Khác biệt chỗ này ảnh hưởng rất nhiều đấy.

Chẳng hạn thế nào ạ?

Chẳng hạn như thiết kế của cơ sở dữ liệu với thiết kế của các đối tượng nghiệp vụ. Các đối tượng nghiệp vụ định ra cấu trúc của các hành vi trong nghiệp vụ. Còn thiết kế cơ sở dữ liệu định ra cấu trúc của dữ liệu nghiệp vụ. Hai cấu trúc đó được ràng buộc bởi những lực rất khác nhau. Cấu trúc của dữ liệu nghiệp vụ không nhất thiết phải là cấu trúc tốt nhất cho hành vi nghiệp vụ.

Em rối rồi.

Nghĩ thế này. Cơ sở dữ liệu không được tối ưu cho chỉ một ứng dụng; nó phải phục vụ toàn thể hệ thống. Thế nên cấu trúc của dữ liệu là sự dung hòa nhu cầu của nhiều ứng dụng khác nhau.

Chỗ này hiểu ạ.

Ừ còn mô hình đối tượng thì khác, chúng chỉ phải mô tả cấu trúc các hành vi của bản thân ứng dụng đó.

Do cơ sở dữ liệu phải thỏa hiệp với nhiều ứng dụng khác nhau, nên nó sẽ không được phù hợp tối ưu cho bất kỳ một ứng dụng riêng rẽ nào.

Chuẩn. Đối tượng và Cấu trúc Dữ liệu ràng buộc bởi những luật lực hoàn tòa khác nhau. Không mấy khi chúng song ứng ngăn nắp. Người ta gọi đây là trở kháng Đối tượng/Quan hệ.

Có hồi con nghe thấy rồi. Nhưng cứ tưởng rằng trở kháng đó đã được giải quyết bởi các ORM.

Và giờ con biết một chuyện hoàn toàn khác. Chẳng có trở kháng nào cả bởi vì đối tượng và cấu trúc dữ liệu là những bổ sung của nhau, chứ không phải là hình chiếu của cái còn lại.

Là nào ạ?

Chúng là những thực thể đối lập, không phải tương tự.

Đối lập?

Chuẩn, theo một cách rất hay. Thế này nhé, đối tượng và cơ sở dữ liệu ám thị những cấu trúc điều khiển hoàn toàn khác nhau.

Khoan, sao cơ ạ?

Hãy xem xét một tập các lớp đối tượng mà đều tuân theo một interface chung. Chẳng hạn tất cả các lớp đại diện cho các hình hình học hai chiều đều có những hàm để tính toán dienTichchuVi.

Sao lúc nào cũng là ví dụ đó thế?

Thử bốc ra hai lớp: SquareCircle. Rõ rành là dienTichchuVi của hoi lớp đó hoạt động dựa vào những dữ liệu ám vị hoàn toàn khác nhau. Và nói rõ là cách để gọi tới chúng chính gọi là đa hình động.

Khoan hẵng. Cái gì ấy ạ?

Có hai hàm dienTich khác nhau; một cho Square, cái còn lại cho Circle. Khi gọi hàm dienTich trên những đối tượng khác nhau, chính đối tượng đó sẽ biết hàm nào cần gọi. Đó gọi là đa hình động.

Vâng đồng ý. Đối tượng biết nơi triển khai những phương thức của nó. Đồng ý.

Rồi giờ chuyển các đối tượng đó thành các cấu trúc dữ liệu. Dùng Biệt Thể đi.

Cái gì biệt ấy ạ?

Biệt Thể. Trong trường hợp của mình thì chỉ là hai cấu trúc dữ liệu khác nhau thôi. Một cái cho Square và cái kia cho Circle. Các mảnh dữ liệu có cấu trúc Circle sẽ có tâm, bán kính, và thêm một mã để định vị rằng nó là một Circle.

Mã đó là một enum, nhỉ?

Đúng thể, cấu trúc dữ liệu Square có điểm trên bên trái, và chiều rộng. Nó cũng có mã biệt thể lúc nãy.

Vâng, hai cấu trúc dữ liệu có chung một mã kiểu.

Đúng. Rồi giờ đến hàm dienTich. Sẽ có một cái switch ở đây đúng không?

Tránh đâu được ạ, nó sẽ có hai case khác nhau. Một cho Square và còn lại cho Circle. Hàm chuVi cũng sẽ cần đến một câu lệnh switch tương tự.

Lại đúng. Giờ con nghĩ xem cấu trúc về hai tình huống. Trong tình huống của đối tượng, hai triển khai của hàm area là biệt lập với cái còn lại và đi theo kiểu đối tượng. Hàm area của Square đi theo lớp Square và hàm area của Circle đi theo lớp Circle.

Vâng. Còn trong tình huống cấu trúc dữ liệu, hai triển khai của hàm area nằm trong cùng một hàm, chúng không “đi theo” kiểu đối tượng.

Khá hơn rồi đấy. Nếu con muốn thêm kiểu đối tượng Triangle vào tình huống đối tượng, mã nào sẽ bị sửa?

Không có mã nào phải sửa cả. Con chỉ cần tạo một lớp Triangle mới. Ah mà có thể sẽ cần sửa tí ti chỗ factory.

Đúng vậy. Sửa ít, khi thêm một kiểu đối tượng mới, rất ít mã phải sửa. Giờ thử đặt tình huống cần bổ sung hàm mới, diemTam chẳng hạn.

Sẽ phải bổ sung nó vào cả ba kiểu đối tượng, cả Circle cả Square lẫn Triangle.

Tốt, vậy là bổ sung hàm mới thì vất vả, sẽ phải thay đổi từng lớp một.

Nhưng cấu trúc dữ liệu thì không thế. Để bổ sung Triangle con sẽ phải sửa từng hàm một để thêm case về Triangle vào câu lệnh switch.

Đúng, với cấu trúc dữ liệu, thêm kiểu dữ liệu thì vất vả, sẽ phải sửa từng hàm một.

Nhưng khi tạo thêm hàm diemTam thì không phải sửa gì cả.

Yup. Thêm hàm thì lại dễ.

Woào. Hoàn toàn đối lập nhỉ.

Tất nhiên rồi. Điểm lại này:

  • Thêm hàm mới vào một tập các lớp là việc vất vả, sẽ phải sửa tất cả các lớp.
  • Thêm hàm mới vào một tập các cấu trúc dữ liệu thì đơn giản, cứ thế thêm vào là xong chuyện.
  • Thêm một lớp mới vào một tập các lớp thì lại đơn giản, chỉ cần thêm vào và cũng xong chuyện.
  • Nhưng thêm một kiểu dữ liệu mới vào một tập các kiểu dữ liệu thì lại khó, sẽ phải sửa tất cả các hàm.

Vâng, đối lập, theo một cách rất hay. Vậy là nếu ta biết rằng rồi sẽ phải bổ sung hàm mới về sau, ta sẽ dùng cấu trúc dữ liệu. Còn nếu dự là sẽ phải bổ sung lớp mới về sau thì ta nên dùng lớp.

Đánh giá ổn đấy. Nhưng chúng còn một khía cạnh đối lập khác nữa. Cái này liên quan đến các phụ thuộc.

Các phụ thuộc ấy ạ?

Ừ, cụ thể là hướng phụ thuộc của mã nguồn.

Vâng, thầy nói tiếp đi ạ.

Trong trường hợp của cấu trúc dữ liệu nhé. Mỗi hàm có một câu lệnh switch để chọn ra triển khai thích hợp dựa theo mã kiểu nằm trong Biệt Thể.

Đúng, rồi sao nữa ạ?

Giả sử gọi hàm area nhé. Bên gọi sẽ phụ thuộc vào hàm dienTich, và hàm dienTich thì phụ thuộc vào tất cả các triển khai.

Thầy nói phụ thuộc ở đây nghĩa là sao ạ?

Tưởng tượng mỗi triển khai của dienTich được viết thành một hàm. Chẳng hạn dienTichHinhTrondienTichHinhVuongdienTichHinhTamGiac.

Vâng, để câu lệnh switch chỉ việc gọi mấy hàm đó.

Tưởng tượng tiếp là các hàm đó mỗi cái được đặt ở một tập tin.

Vậy là tập tin chứa câu lệnh switch sẽ phải import, hay use hay include gì đó.

Đúng, import tất. Cái đó gọi là phụ thuộc. Một tập tin mã nguồn phụ thuộc vào một tập tin mã nguồn khác để hoạt động. Con thấy hướng phụ thuộc ở đây như thế nào?

Tập tin chứa switch phụ thuộc vào những tập tin chứa các triển khai.

Thế chỗ mã nguồn mà gọi hàm dienTich thì sao?

Khối mã đó phụ thuộc vào câu lệnh switch, thứ bị phụ thuộc vào tất cả các triển khai.

Chính xác. Tất cả các mối phụ thuộc đều xuất phát từ bên gọi đến bên triển khai. Nếu con làm thay đổi gì đó ở mã triển khai thì…?

Vâng. Thay đổi đó sẽ khiến mã nguồn của câu lệnh switch buộc phải biên dịch lại, và tất cả các bên gọi đến switch đó cũng sẻ phải biên dịch lại theo.

Đúng thế. Ít nhất thì nó đúng với mấy ngôn ngữ mà phụ thuộc vào ngày chỉnh sửa của tập tin để định vị những module cần biên dịch lại.

Mấy ngôn ngữ đó chắc là định kiểu tĩnh hết thầy nhỉ?

Thật ra có mấy cái không đâu.

Biên dịch lại nhiều thật.

Ừ, và theo đó là rất nhiều deploy lại.

Vâng, nhưng trong trường hợp của các lớp thì không như thế ạ?

Đúng, bởi vì bên gọi dienTich phụ thuộc vào interface, và bên triển khai cũng phụ thuộc vào interface.

Con hiểu ý thầy. Tập tin mã của Square sẽ import hay use hay include gì đó với tập tin của interface Shape.

Đúng. Hướng của sự triển khai mã nguồn ngược với hướng của lời gọi. Chúng chỉ thẳng đến chỗ gọi luôn. Ít nhất với các ngôn ngữ định kiểu tĩnh thì là như thế. Với những ngôn ngữ định kiểu động thì thậm chí bên gọi hàm dienTich còn chẳng phụ thuộc vào cái gì hết. Các mối liên kết sẽ được tạo ra vào thời điểm thực thi.

Vâng OK ạ. Vậy nếu ta thay đổi một trong số các triển khai thì…

Mỗi tập tin đó cần biên dịch và deploy lại thôi.

Là nhờ hướng của phụ thuộc của mã nguồn ngược với hướng của lời gọi.

Đúng. Ta thường gọi đó là Đảo ngược Phụ thuộc.

Để con tóm tắt lại nhé. Lớp và Cấu trúc Dữ liệu là đối lập nhau, ít nhất ở ba khía cạnh:

  • Lớp thì hiển hiện các hàm và ám vị các dữ liệu. Cấu trúc dữ liệu thì hiển hiện các dữ liệu và ám vị các hàm.
  • Sẽ dễ bổ sung lớp mới nhưng khó thêm hàm cho chúng. Ngược lại sẽ dễ thêm hàm cho các cấu trúc dữ liệu nhưng lại khó bổ sung cấu trúc mới.
  • Các cấu trúc dữ liệu bắt buộc bên gọi phải biên dịch và deploy lại. Các lớp thì cô lập bên gọi ra khỏi những thao tác đó.

Ngon rồi đấy. Đó là những vấn đề mà mọi kỹ sư và nhà thiết kế phần mềm tốt cần phải nằm lòng.


Nguồn: https://blog.cleancoder.com/uncle-bob/2019/06/16/ObjectsAndDataStructures.html

_

Leave a Comment

Your email address will not be published. Required fields are marked *