Let’s Go: Lập trình hướng đối tượng trong Golang

116

Go là tập hợp kì lạ của các ý tưởng cũ và mới. Nó có cách tiếp cận tươi mới khi sẵn sàng loại bỏ những khái niệm về cách thức lập trình đã tồn tại trước đó. Nhiều người thậm chí còn không biết liệu Go có phải là ngôn ngữ hướng đối tượng hay không.

Vậy nên hãy để tôi làm rõ cho bạn ngay bây giờ nhé!

Trong bài hướng dẫn này, bạn sẽ biết được những chi tiết phức tạp về thiết kế hướng đối tượng trong Go, làm cách nào để thể hiện các cột lập trình đối tượng như Encapsulation (nguyên tắc hợp tế), inheritance (tính kế thừa) và polymorphism (tính đa hình) trong Go và Go khác gì so với các ngôn ngữ khác.

Triết lý thiết kế của Go

Nguồn gốc của Go dựa trên C và mở rộng theo hệ gia đình Algol. Ken Thompson đã nửa đùa nửa thật rằng anh cùng Rob Pike, Robert Granger đã tập trung lại với nhau và quyết định sẽ ghét C++. Dù chỉ là đùa giỡn đi nữa, Go vẫn rất khác so với C++. Hơn nữa, Go mang đến sự đơn giản tối đa. Điều này sẽ được Rob Pike giải thích trong Less is exponentially more.

Go không có classes, không có đối tượng, không có những trường hợp ngoại lệ và không có templates. Nó sở hữu garbage collection (quá trình thu gom rác) và built-in concurrency. Thiết sót đáng kể nhất về hướng đối tượng là không có hệ thống phân cấp type trong Go. Điều này khiến Go đối lập với hầu hết các ngôn ngữ hướng đối tượng như C++, Java, C#, Scala và thậm chí đối lập với các ngôn ngữ năng động như Python và Ruby.

Go không có classes, nhưng lại có types. Cụ thể, Go có structs. Structs là các types định dạng người dùng. Các types Struct (với methods) có cùng mục đích với các classes trong các ngôn ngữ khác.

Một struct sẽ định dạng trạng thái. Đây là Creature struct. Nó có 1 trường tên và 1 boolean flag gọi là Real, giúp chúng ta xác định nó có phải là creature thật hay creature ảo. Structs chỉ giữ 1 trạng thái và đứng yên.

Methods là các hàm hoạt động trên 1 số types nhất định. Chúng có 1 clause receiver ủy nhiệm loại type mà chúng đang hoạt động. Đây là 1 method Dump() hoạt động trên Creature structs và print trạng thái của chúng:

Đây là 1 syntax bất thường, nhưng nó rất minh bạch và rõ ràng (khác với “this” ngầm định hoặc “self” rắc rối của Python).

Bạn có thể nhúng types ẩn danh vào nhau. Nếu bạn đã nhúng 1 struct vô danh, sau đó struct đã được nhúng đưa trực tiếp trạng thái của nó (và methods) cho struct đang được nhúng. Ví dụ, FlyingCreature có 1 struct vô danhCreature được nhúng bên trong nó, có nghĩa 1 FlyingCreature là 1 Creature.

Bây giờ, nếu bạn có 1 instance của FlyingCreature, bạn có thể tiếp cận Name của nó và Real attributes 1 cách trực tiếp:

Interfaces là dấu hiệu cho thấy khả năng hỗ trợ hướng đối tượng của Golang. Interfaces là các types xác thực bộ methods. Tương tự như các interfaces trong các ngôn ngữ khác, chúng không có implementation.

Các đối tượng thực hiện tất cả các interface methods sẽ tự động thực hiện interface. Không có từ khóa inheritance (tính kế thừa) hay subclassing hay “implements”. Trong đoạn code dưới, type Foo thực thi Fooer interface (theo quy ước, các tên interface Go kết thúc bằng “er”).

Cách để Go đo lường pillars của lập trình hướng đối tượng là: Encapsulation (nguyên tắc hợp tế), inheritance (tính kế thừa) và polymorphism (tính đa hình). Đây là những tính năng của các ngôn ngữ lập trình class-based – những ngôn ngữ lập trình hướng đối tượng nổi tiếng nhất.

Về cốt lõi, các đối tượng là các constructs ngôn ngữ có trạng thái và hành vi vận hành trên trạng thái và phơi nó ra cho các phần khác của chương trình 1 cách chọn lọc.

Go hợp thể mọi thứ ở cấp độ package. Những cái tên bắt đầu với chữ cái thường sẽ chỉ được nhìn thấy trong package đó. Bạn có thể giấu mọi thứ trong package cá nhân và chỉ expose các types, interfaces và các hàm factory chuyên biệt.

Ví dụ, để giấu type Foo trên và expose interface, bạn có thể đổi tên thành chữ thường foo và cung cấp 1 hàm NewFoo() trả về interface Fooer công khai:

Sau đó, code từ package khác có thể sử dụng NewFoo() và tiếp cận với 1 interface Fooe được thực thi bởi type foo nội bộ:

Inheritance (tính kế thừa) hoặc subclassing luôn là 1 vấn đề gây tranh cãi. Có rất nhiều vấn đề xảy ra khi thực thi inheritance (trái ngược với interface inheritance). Nhiều inheritance được ngôn ngữ C++ và Python thực hiện, và các ngôn ngữ khác gặp phải diamond problem, nhưng mỗi inheritance đơn lại không gây khó khăn với vấn đề base-class.

Các ngôn ngữ hiện đại và cách nghĩ hướng đối tượng hiện nay thiên về composition hơn là inheritance. Go rất quan tâm đến vấn đề này và không sở hữu bất kì thứ bậc type nào. Go cho phép bạn chia sẻ các chi tiết thực thi qua composition. Nhưng trong những tình huống bất thường, Golang (nhiều khả năng xuất phát từ các vấn đề khi sử dụng) cho phép composition ẩn danh bằng cách embedding.

Thực tế là, composition bằng cách nhúng 1 type ẩn danh tương đương với implementation inheritance. Một struct được nhúng cũng mong manh, yếu ớt như 1 base class. Bạn có thể nhúng 1 interface – loại interface tương đương với việc kế thừa từ 1 interface trong các ngôn ngữ như Java hoặc C++. Nó có thể dẫn đến lỗi runtime mà bạn không phát hiện được trong thời gian compile nếu type đang nhúng không thực thi tất cả các interface methods.

Ở đây, SuperFoo nhúng interface Fooer nhưng không thực thi methods của nó. Compiler Go sẽ cho phép bạn tạo 1 SuperFoo mới và gọi các methods Fooer, nhưng rõ ràng sẽ thất bại trong lúc runtime.

Khi chạy chương trình này, bạn sẽ nhận được 1 kết quả tồi tệ:

Polymorphism (tính đa hình) là bản chất của lập trình hướng đối tượng: khả năng xử lý đồng bộ các đối tượng thuộc nhiều type khác nhau miễn là chúng dính với cùng interface. Go interfaces cung cấp tính năng này 1 cách trực tiếp và trực quan.

Dưới đây là 1 ví dụ chi tiết với nhiều creatures (và 1 cánh cửa!) thực thi Dumper interface, tạo và lưu trữ trong 1 slice và sau đó, method Dump() được gọi cho lẫn nhau. Bạn sẽ chú ý thấy các styles khác nhau để cá thể hóa các đối tượng.

Go thực sự là 1 ngôn ngữ hướng đối tượng. Nó kích hoạt modeling dựa trên đối tượng và quảng bá khả năng sử dụng interfaces tốt nhất, thay vì các thứ bậc type cứng nhắc. Go đem đến những lựa chọn cú pháp bất thường, nhưng về tổng thể, khi làm việc với types, methods và interfaces thì lại đơn giản, nhẹ nhàng và tự nhiên.

Embedding (nhúng) không hoàn toàn nguyên bản, rõ ràng, nhưng quan niệm thực hành luôn xuất hiện ở công sở, vì vậy nhúng được cung cấp thay vì chỉ composition (tổ hợp) theo tên.