Hi, I am

Ngô Tôn

I am a programmer.

Home / Programming / Go / Lập trình hướng đối tượng trong Golang – OOP in Go

Lập trình hướng đối tượng trong Golang – OOP in Go

1. Khái niệm kỹ thuật được đề cập

  • abstraction

  • polymorphism

  • inheritance

  • composition

  • class

  • object

  • type struct

  • type embedding

2. Giới thiệu

Nếu bạn mới làm quen với lập trình, có thể bạn chưa từng tiếp xúc với “lập trình hướng đối tượng”. Nếu bạn là một lập trình viên kỳ cựu, rất có thể bạn đã phát triển các chương trình bằng ngôn ngữ hướng đối tượng: bạn biết đối tượng là gì, đóng gói có nghĩa là gì…v.v.

Mục đích của phần này là xem xét tất cả các đặc điểm của ngôn ngữ hướng đối tượng. Bằng cách đó, chúng ta sẽ cố gắng hiểu tại sao Go khác với các ngôn ngữ truyền thống đó.

Hầu hết học viên của tôi đều sợ các thuật ngữ và khái niệm liên quan đến lập trình hướng đối tượng. Đừng ấn tượng với những thuật ngữ phức tạp đó.

3. Ngôn ngữ hướng đối tượng là gì

Khái niệm này không phải là mới.

Định nghĩa của loại ngôn ngữ này là gì? Ngôn ngữ hướng đối tượng là “một ngôn ngữ lập trình cho phép người dùng thể hiện một chương trình dưới dạng các đối tượng và thông điệp giữa các đối tượng đó” .

Định nghĩa nhấn mạnh rằng đây là một loại ngôn ngữ lập trình cung cấp cho người dùng các đối tượng và cách truyền tải thông điệp giữa các đối tượng.

Các ngôn ngữ hướng đối tượng có một số đặc điểm chung:

  • Lớp và đối tượng là các khối xây dựng chương trình.

  • Nó cung cấp một mức độ trừu tượng nhất định

  • Nó cung cấp cơ chế đóng gói

  • Đa hình là có thể

  • Các lớp có thể kế thừa lẫn nhau

Hãy cùng xem xét từng đặc điểm đó để xác định xem Go có thể được coi là Ngôn ngữ lập trình hướng đối tượng hay không.

4. Lớp, đối tượng và thể hiện

Phần này sẽ đưa ra một số định nghĩa cơ bản về class, object, instance. Đó là những định nghĩa chung áp dụng cho hầu hết các ngôn ngữ lập trình hướng đối tượng

 Class

  • Đây là một thực thể lập trình do người dùng định nghĩa

  • Nó định nghĩa một tập hợp các thuộc tính (mỗi thuộc tính có một kiểu)

  • Thuộc tính cũng được gọi là thuộc tính, trường, thành viên dữ liệu

  • Nó định nghĩa các phương thức (hành vi và hoạt động) và cách triển khai chúng.

  • Các phương thức và thuộc tính có “khả năng hiển thị” cụ thể. Các thuộc tính và phương thức chỉ có thể được gọi trong các điều kiện cụ thể.

  • Thông thường, các lớp có một “hàm tạo”, đây là một phương thức được thiết kế để tạo ra một đối tượng.

    • Các hàm tạo được sử dụng để khởi tạo “trạng thái” của đối tượng, tức là khởi tạo giá trị của các thuộc tính thành các giá trị cụ thể.

Sau đây là ví dụ về một lớp trong C++:

Object

  • Các đối tượng được tạo ra trong thời gian chạy chương trình. Chúng là “thực thể thời gian chạy”.

  • Gọi hàm tạo lớp sẽ tạo ra một đối tượng.

  • Một đối tượng cũng được gọi là “một thể hiện”

  • Quá trình tạo ra một đối tượng được gọi là “tạo phiên bản”.

5. Class in Go?

Go có “ types ” nhưng không có class. Một type chỉ định: “một tập hợp các giá trị”

“các hoạt động và phương thức cụ thể cho các giá trị đó” Các kiểu cấu trúc cho phép bạn định nghĩa một tập hợp các trường có tên và kiểu. Hãy lấy ví dụ về đối tượng teacher:

Chúng tôi định nghĩa kiểu Giáo viên có ba trường: id kiểu int, firstname và lastname kiểu string.

Chúng ta cũng có thể đính kèm các phương thức vào loại này:

Chúng tôi đã định nghĩa một kiểu struct với một cấu trúc dữ liệu và các phương thức kèm theo. Hãy tạo một thể hiện t của đối tượng đó:

Sau đó chúng ta có thể sử dụng các phương thức được xác định trên đối tượng với trường hợp cụ thể đó:

Bạn cũng có thể tạo một kiểu dựa trên một kiểu khác:

Và kiểu này cũng có thể có các phương thức:

Trong trường hợp này, tập hợp các giá trị StatusCodeđược giới hạn ở số nguyên

  • Cấu trúc kiểu Go gần giống với lớp

  • Cấu trúc kiểu có các trường

  • Bạn có thể định nghĩa các phương thức được đính kèm vào các kiểu

  • So với các ngôn ngữ khác, không có “hàm tạo” nào được tích hợp sẵn.

  • Các trường có kiểu struct được tự động khởi tạo bởi Go (thành giá trị bằng không của kiểu trường).

  • Thay vì sử dụng hàm tạo, một số thư viện định nghĩa một Newhàm:

Sau đây là một Newchức năng của mô-đun github.com/gin-gonic/gin:

Sau đây là một Newhàm được định nghĩa trong gói chuẩn math/rand:

Và một sản phẩm khác trong docgói:

  • Bạn có thể lưu ý rằng, nói chung, Newhàm trả về một con trỏ

  • Cũng phổ biến khi có một Newhàm trả về một con trỏ VÀ mộterror

6. Trừu tượng

Một khái niệm quan trọng khác của lập trình hướng đối tượng là tính trừu tượng.

Khi chúng ta xây dựng mã, chúng ta gọi các phương thức hoặc hàm để thực hiện một hoạt động cụ thể:u.store()

Ở đây chúng ta đang bảo chương trình của mình gọi phương thức store. Nó không yêu cầu bạn phải biết về cách phương thức được triển khai (cách nó được mã hóa bên trong).

  • Thuật ngữ trừu tượng bắt nguồn từ tiếng Latin abstractio. Thuật ngữ này truyền tải ý tưởng về sự tách biệt, về việc lấy đi một cái gì đó.

  • Việc gọi hàm và phương thức đang làm mất đi phần triển khai cụ thể và các chi tiết phức tạp.

  • Ưu điểm chính của việc ẩn chi tiết triển khai là bạn có thể thay đổi mà không ảnh hưởng đến lệnh gọi hàm (nếu bạn không thay đổi chữ ký hàm).

7. Đóng gói

Sự đóng gói bắt nguồn từ thuật ngữ tiếng Latin “capsula” có nghĩa là “một chiếc hộp nhỏ”. Trong tiếng Pháp, thuật ngữ “capsule” dùng để chỉ thứ gì đó được đóng hộp. Trong sinh học, thuật ngữ này được sử dụng để chỉ màng tế bào.

Capsule là một vật chứa mà bạn có thể đóng hộp các vật thể. Đóng gói một thứ gì đó tương đương với việc bao bọc nó bên trong một capsule.

Nếu chúng ta quay lại với khoa học máy tính, đóng gói là một kỹ thuật thiết kế được sử dụng để nhóm các hoạt động và dữ liệu vào các viên nang mã hóa riêng biệt. Quyền truy cập vào các hoạt động và dữ liệu đó được quy định và các “viên nang” đó xác định các giao diện bên ngoài để tương tác với chúng.

Ngôn ngữ lập trình hướng đối tượng cung cấp cho nhà phát triển khả năng đóng gói các đối tượng. Có phải Go cũng vậy không? Chúng ta có thể đóng gói mọi thứ không? Chúng ta có thể định nghĩa các giao diện bên ngoài nghiêm ngặt trên các gói không?

Câu trả lời là có! Khái niệm gói cho phép bạn xác định một giao diện bên ngoài nghiêm ngặt.

 Khả năng hiển thị

Hãy lấy một ví dụ. Chúng ta định nghĩapackageA

Chỉ packageAđịnh nghĩa một hàm public DoSomething. Thế giới bên ngoài có thể sử dụng hàm này, nhưng nó sẽ không biên dịch nếu bạn cố gắng truy cập vào một hàm private ( onePrivateFunctionanotherPrivateFunction…). Không có từ khóa public hoặc private trong ngôn ngữ; khả năng hiển thị được xác định bằng trường hợp của chữ cái đầu tiên của các phần tử được xác định (phương thức, kiểu, hằng số …)

Ưu điểm của việc đóng gói

  • Nó ẩn việc triển khai gói cho người dùng.

  • Nó cho phép bạn thay đổi việc triển khai mà không phải lo lắng về việc phá vỡ thứ gì đó trong mã của người dùng. Bạn chỉ có thể phá vỡ thứ gì đó nếu bạn thay đổi một trong các phần tử được xuất (công khai) của gói.

  • Việc bảo trì và tái cấu trúc dễ thực hiện hơn. Những thay đổi nội bộ hiếm khi ảnh hưởng đến khách hàng.

8. Đa hình

Đa hình là khả năng của một cái gì đó có nhiều hình dạng. Các từ này xuất phát từ thuật ngữ poly (có nghĩa là nhiều) và “morphism” thiết kế các hình dạng, hình dạng của một cái gì đó.

Làm thế nào để áp dụng điều đó vào ngôn ngữ lập trình? Để hiểu về đa hình, bạn phải tập trung vào các hoạt động, tức là các phương thức.

Một hệ thống được gọi là đa hình nếu “cùng một hoạt động có thể hoạt động khác nhau trong các lớp khác nhau”.

Hãy tinh chỉnh ví dụ về giáo viên của chúng ta. Một giáo viên trường học không giống với một giáo viên đại học; do đó chúng ta có thể định nghĩa hai “lớp” khác nhau, hai kiểu struct khác nhau. Kiểu SchoolTeacherstruct :

Và UniversityTeacherkiểu struct:

Loại giáo viên thứ hai có nhiều lĩnh vực hơn cho chuyên môn và khoa của mình.

Tiếp theo, chúng ta sẽ định nghĩa cùng một hoạt động cho hai loại giáo viên:

Và đối với loại còn lại, cách thực hiện có đôi chút khác biệt:

Giáo viên đại học luôn nêu rõ chuyên ngành và khoa của mình khi chào lớp. Trong khi giáo viên trường học thì không.

Ở đây chúng ta có cùng một hoạt động được định nghĩa trên hai kiểu khác nhau; chúng ta có thể định nghĩa một giao diện Teacher:

Tự động các loại SchoolTeachervà UniversityTeachersẽ triển khai giao diện (bạn không cần phải chỉ định nó trong mã nguồn). Hãy tạo một lát cắt bao gồm hai đối tượng:

John là một giáo viên đại học chuyên ngành sinh học; Marc dạy trẻ em. Công việc của họ không giống nhau, nhưng họ chia sẻ một hoạt động chung: hoạt động chào hỏi lớp học. Do đó, họ triển khai loại giao diện Teacher. Chúng ta có thể nhóm hai đối tượng đó trong một lát cắt bao gồm các đối tượng Teacher:

Sau đó, chúng ta có thể lặp lại lát cắt và yêu cầu chúng chào lớp của chúng theo phong cách cụ thể của chúng:

Trong danh sách mã cuối cùng này, chúng ta đã khám phá ra sức mạnh của đa hình:

  • Go có thể tìm ra phương thức nào để gọi dựa trên loại t.

  • Cùng một loại giao diện thiết kế các triển khai khác nhauTeacher

9. Kế thừa

Một đứa trẻ có thể thừa hưởng các đặc tính di truyền của tổ tiên. Nhưng chúng ta không thể quy trẻ em thành tổ tiên của chúng. Khi chúng lớn lên, chúng sẽ xây dựng các đặc tính riêng của mình.

Khái niệm kế thừa tương tự cũng áp dụng cho các lớp trong lập trình hướng đối tượng. Khi bạn tạo một ứng dụng phức tạp, thường thì các đối tượng được xác định sẽ chia sẻ một số thuộc tính và hoạt động chung.

Ví dụ, mỗi giáo viên đều có tên, họ, ngày sinh, địa chỉ, v.v. Giáo viên trường học và giáo viên đại học đều có cả hai thuộc tính đó. Tại sao không tạo một siêu đối tượng sẽ chứa các thuộc tính chung đó và chia sẻ chúng với các lớp con?

Đó là ý tưởng về sự kế thừa. Các đối tượng sẽ định nghĩa các thuộc tính và hoạt động riêng biệt của chúng và kế thừa tổ tiên của chúng. Điều này sẽ làm giảm sự lặp lại của mã. Các thuộc tính được chia sẻ chỉ được định nghĩa trong siêu đối tượng, chỉ một lần.

Có thể với Go không? Câu trả lời là không. Chúng ta sẽ thấy một khái niệm thiết kế quan trọng: nhúng kiểu. Mục tiêu chính của phần này là xem liệu chúng ta có thể thấy nhúng kiểu như một dạng kế thừa hay không.

Nhúng loại đơn

Bạn có thể nhúng một kiểu vào một kiểu khác. Hãy lấy ví dụ trước. Chúng ta có một kiểu struct human với hai trường:

Điều này đại diện cho một con người. Chúng ta thêm cho anh ta khả năng đi bộ:

Một giáo viên trường học và một giáo viên đại học đều là con người; chúng ta có thể nhúng loại này Humanvào hai loại đó:

Để thực hiện việc này, chúng ta chỉ cần thêm tên của loại dưới dạng một trường mới (không có tên).

Bằng cách đó, chúng ta có thể xác định được hai giáo viên của mình:

Chúng ta có thể gọi phương thức Walk on john and marc:

Kết quả sẽ là:

Nhúng nhiều loại

Bạn không bị giới hạn ở một kiểu nhúng vào một cấu trúc kiểu. Bạn có thể nhúng nhiều kiểu. Hãy giới thiệu kiểu Researcher:

Researchercó một lĩnh vực nghiên cứu và một số lượng nhất định các nghiên cứu sinh tiến sĩ mà anh ta phải theo dõi. Các giáo viên đại học cũng là các nhà nghiên cứu. Chúng ta có thể nhúng loại Researchervào loại UniversityTeacher:

Xung đột tên

Nhưng có một hạn chế quan trọng đối với kiểu nhúng: xung đột tên. Nếu bạn muốn nhúng hai kiểu đến từ các gói khác nhau nhưng có cùng tên, chương trình của bạn sẽ không biên dịch được. Hãy lấy một ví dụ. Bạn có hai gói packageAvà packageBmỗi gói khai báo một kiểu có tên là MyType:

Bây giờ trong gói chính của bạn, bạn định nghĩa một gói MyOtherTypenhúng cả hai kiểu:

Có sự trùng lặp của tên trường. Hai trường đầu tiên là trường nhúng; do đó, tên của chúng đều MyTypethuộc kiểu struct MyOtherType. Không thể có hai trường có cùng tên. Chương trình của bạn sẽ không biên dịch:

Truy cập trực tiếp vào các thuộc tính và hoạt động

Trong các ngôn ngữ hướng đối tượng khác, lớp B kế thừa từ lớp A có thể truy cập trực tiếp tất cả các thuộc tính và hoạt động được định nghĩa trong lớp A.

Nó có nghĩa là gì? Hãy lấy ví dụ về PHP. Chúng ta định nghĩa một lớp Acó phương thức sayHi(public nghĩa là thế giới bên ngoài có thể truy cập vào nó, giống như tên phương thức bắt đầu bằng ký tự viết hoa):

Sau đó, trong cùng một tập lệnh, chúng ta định nghĩa lớp Bkế thừa từ lớp A. Từ khóa trong PHP là extends:

Lớp này Blà lớp kế thừa của A. Do đó, chúng ta có thể truy cập các phương thức của trực tiếp Abằng một đối tượng có kiểu B:

Tại đây chúng ta có thể truy cập trực tiếp vào các thuộc tính và hoạt động được xác định trên A từ B.

Chúng ta có thể truy cập trực tiếp các thuộc tính của kiểu nhúng bằng cách nhúng kiểu. Chúng ta có thể viết:

Chúng ta cũng có thể viết:

Thành phần trên kế thừa

Nhúng kiểu và kế thừa có cùng mục tiêu: tránh lặp lại mã và tối đa hóa việc tái sử dụng mã. Nhưng thành phần và kế thừa là hai thiết kế rất khác nhau. Phương pháp tiếp cận của Go là thành phần hơn là kế thừa.

Tôi sẽ viết về sự phân biệt được đưa ra bởi các tác giả của cuốn sách rất nổi tiếng Design Patterns: Elements of Reusable Object-Oriented Software :

  • Kế thừa là chiến lược “tái sử dụng hộp trắng” : Các lớp thừa kế có quyền truy cập vào các thuộc tính nội bộ của lớp tổ tiên; các thuộc tính và hoạt động nội bộ có thể được lớp thừa kế nhìn thấy.

  • Composition là chiến lược “tái sử dụng hộp đen” : Các lớp được hợp thành cùng nhau, kiểu Anhúng một kiểu Bkhông có quyền truy cập vào tất cả các thuộc tính và hoạt động bên trong. Các trường và phương thức công khai có thể truy cập được; còn các trường và phương thức riêng tư thì không.

Hãy lấy một ví dụ. Chúng ta có một cartđịnh nghĩa kiểu struct Cart:

Cấu trúc kiểu này có hai trường có tên là IDvà locked. Trường IDđược xuất. Trường có tên lockedkhông thể truy cập được từ gói khác.

Trong chương trình chính, chúng ta tạo một cấu trúc kiểu khác có tên là User:

Kiểu cart.Cartđược nhúng vào User. Nếu chúng ta tạo một biến có kiểu Userthì chúng ta chỉ có thể truy cập các trường được xuất từ cart.Cart​​:

Nếu chúng ta gọi

chương trình của chúng tôi sẽ không biên dịch được với thông báo lỗi sau:

Ưu điểm của thành phần so với kế thừa

  • Trong mô hình kế thừa, lớp heir có thể truy cập vào các thành phần bên trong của lớp ancestor. Các thành phần bên trong của lớp ancestor có thể được nhìn thấy bởi heir; quy tắc đóng gói bị phá vỡ phần nào. Trong khi với composition, bạn không thể truy cập vào các phần ẩn của lớp ancestor. Kỹ thuật này đảm bảo rằng người dùng của structs của bạn sẽ chỉ sử dụng các phương thức và trường đã xuất.

  • Composition thường đơn giản hơn là inheritance. Nếu bạn là một lập trình viên có kinh nghiệm, bạn có thể đã từng đối mặt với thiết kế hướng đối tượng, trong đó inheritance được sử dụng quá mức. Trong những trường hợp như vậy, dự án khó hiểu hơn và những người mới phải dành hàng giờ với các lập trình viên cao cấp để hiểu biểu đồ các lớp.

About ngoton

Ngô Tôn is a programmer with passion for tailored software solutions. Comes with 7+ years of IT experience, to execute beautiful front-end experiences with secure and robust back-end solutions.

Leave a Reply

avatar
  Subscribe  
Notify of