Ở thời điểm hiện tại - 12/1/2023 - thì mình đã sắp xếp lại các bài viết của Sub-Series Procedural Programming
và lược bỏ bớt những bài viết về các công cụ liên quan tới Lập Trình Hướng Đối Tượng OOP
để dành cho Sub-Series mới này. Mục đích là để khoanh vùng định nghĩa của các mô hình lập trình được giới thiệu trong Series tổng, cũng giống như việc để dành các yếu tố Functional
của Elm
cho Sub-Series riêng về Funtional Programming
.
Về việc lựa chọn ngôn ngữ để tìm hiểu về mô hình lập trình Hướng Đối Tượng OOP - Object-Oriented Programming
thì mình đã chọn Java
để học và chia sẻ kiến thức qua các bài viết trong Sub-Series [Object-Oriented + Java]
tiếp theo. Còn Sub-Series [Object-Oriented + Ada]
này chỉ là một phần bổ sung để hoàn thiện cho việc giới thiệu ngôn ngữ Ada
.
OOP in Ada
Câu chuyện OOP
của Ada
cũng bắt đầu cùng khoảng thời gian mà Java
xuất hiện năm 1995. Ở thời điểm đó thì các kỹ sư thiết kế phần cứng đã triển khai thành công các kiến trúc vi xử lý lúc bấy giờ để hỗ trợ việc mô phỏng các Đối Tượng object
trong bộ nhớ đệm. Điều này đã tạo điều kiện cho việc áp dụng tư duy lập trình Hướng Đối Tượng Object-Oriented
đã được thảo luận rất nhiều tại MIT
từ những năm 1950
, và sau đó đã được Sir Alan Kay
giới thiệu lại vào năm 1966 để rồi trở thành một trong số thuật ngữ lập trình được nhắc đến nhiều nhất.
Mặc dù Ada
và Java
có cú pháp sử dụng khác nhau rất nhiều, tuy nhiên các yếu tố thuộc về OOP
nói chung thì không bị ràng buộc bởi bất kỳ ngôn ngữ nào bởi tất cả đều xuất phát từ các trang tài liệu trong phòng Lab
của MIT
, và sau thời điểm được Sir Alan Kay
giới thiệu lại thì OOP
cũng lại càng có thêm được nhiều sự đồng thuận của cộng đồng về những khái niệm trọng tâm thiết yếu.
Và ở đây, tại Sub-Series này thì chúng ta sẽ chỉ đề cập tới những khía cạnh đặc trưng thuộc về OOP
và những công cụ mà Ada
cung cấp để triển khai những tính năng này trong code. Bao gồm:
-
Interaction
- Tính Tương Tác là thể hiện cơ bản nhất củaOOP
trong khuynh hướng mô phỏng các đối tượng logic như các thực thể sống, với khả năng biểu thị sự tương tác hay phương thức hành động qua cácsub-program
với cú phápsubject.do_something_to(another)
; ĐượcAda 95
hỗ trợ với khái niệm Phép Thực Thi Nguyên BảnPrimitive Operation
, hoặc gọi ngắn gọn làPrimitive
. -
Inheritance
- Sự Kế Thừa là thể hiện thiết yếu tiếp theo giúp giảm thiểu việc phải viết lặp lại các yếu tố trong định nghĩa của các đối tượng logic có liên quan. Ví dụ các đối tượng dữ liệu mô phỏng ngườiPerson
hiển nhiên sẽ có những yếu tố được sử dụng chung với các đối tượng dữ liệu mô phỏng lập trình viênCoder
theo mô tuýpCoder
được mở rộng từ định nghĩa củaPerson
; ĐượcAda 95
hỗ trợ với khái niệmTagged Record
. -
Polymorphism
- Tính Đa Hình, mà chính xác hơn thì làSub-typing Polymorphism
, cùng vớiInheritance
ở trên giúp giảm thiểu thao tác viết lặp lại các định nghĩasub-program
để tiếp nhận vào các đối tượng logic trong cùng cây phả hệfamily tree
hay hệ thống kế thừainheritance hierachy
. Ví dụ mộtsub-program
tác động lên một trường dữ liệu trongPerson
hiển nhiên sẽ có thể hoạt động tốt vớiCoder
mà không cần phải viết định nghĩa lại với các mỗi định kiểu tham số đầu vào làPerson
vàCoder
; ĐượcAda 95
hỗ trợ với khái niệmType'Class
, hay còn được gọi làClasswide
. Các kiểuPolymorphism
còn lại thì không thuộc vềOOP
nói riêng và chúng ta đã nói đến trong Sub-SeriesProcedural
trước đó. -
Abstraction
- Tính Trừu Tượng được thể hiện trong giai đoạn thiết kế tổng quan phần mềm, khi mà các định nghĩa được viết ở dạng chỉ khai báo các tên định danh của kiểu dữ liệu và cácsub-program
nhưng không có code mô tả chi tiết; ĐượcAda 95
hỗ trợ với khái niệmInterface
. -
Encapsulation
- Tính Năng Đóng Gói được sử dụng để điều hướng việc tương tác với giao diện lập trình mà mộtobject
cung cấp ra bên ngoài. Thay vì code tham chiếu từ bên ngoài có thể trực tiếp chỉnh sửa một trường dữ liệu của mộtobject
thì sẽ phải gửi yêu cầu cập nhật trường dữ liệu đó qua một phương thức có dạng nhưobject.set(property, value)
. Như vậy dữ liệu có thể được kiểm tra và sàng lọc bớt những trường hợp không phù hợp với logic quản lý mà chúng ta muốn xây dựng trong code. Tính năng này rất quan trọng đối với các ngôn ngữOOP
phổ biến nhưng lại không mấy quan trọng đối với các ngôn ngữ có hỗ trợDesign by Contract
nhưAda
; Tuy nhiên vẫn được hỗ trợ rất đầy đủ với khái niệmPackage Privacy
vàPrivate Definition
.
Và trong bài viết mở đầu này chúng ta sẽ nói về Interaction
với sự hỗ trợ của khái niệm Primitive Operation
.
Environment Setup
Sau khi thực hiện xong cái mini project
về Tic-Tac-Toe
cũng tiêu tốn kha khá thời gian thì mình đã suýt quên mất cách sử dụng Alire
để khởi tạo project
mới và một số tùy chỉnh cần thiết cho các tệp quản lý project
. Vì vậy nên ở đây mình sử dụng một phần của bài viết này để ghi chú lại cách sử dụng căn bản và lúc nào đó nhỡ có quên thì cũng có thể tìm lại ở các bài viết mở đầu project tic-tac-toe
trong Sub-Series Procedural
trước đó hoặc tại đây.
cd Documents
alr init --bin learn_ada Success: learn_ada initialized successfully.
Sau khi thư mục learn_ada
được Alire
khởi tạo xong thì chúng ta cần chỉnh sửa lại tệp quản lý của gprbuild
đầu tiên để đổi tên tệp khởi chạy project
về main.adb
cho quen thuộc. Thêm vào đó là ở phần chỉ định các switch
sử dụng kèm lệnh gprbuild
chúng ta chỉ cần "-gnat2020"
, "-gnatX"
và "-gnata"
để kích hoạt những tính năng mới nhất của ngôn ngữ. Còn cái danh sách tham số trong tệp cấu hình bổ sung của Alire
thì hơi thừa đối với người mới sử dụng Ada
.
with "config/learn_ada_config.gpr"; project Learn_Ada is for Source_Dirs use ("src/", "src/**", "config/"); -- for ... for Main use ("main.adb"); package Compiler is for Default_Switches ("Ada") use ("-gnat2020", "-gnatX", "-gnata"); end Compiler; -- package ... end Learn_Ada;
Lưu ý thêm là ở phần chỉ định các thư mục chứa các tệp mã nguồn thì mình có thêm pattern
là "src/**"
để trình biên dịch sẽ duyệt thêm tất cả những thư mục con bên trong thư mục src
theo dạng đệ quy. Trong trường hợp không có thói quen sắp xếp các module
ở dạng các thư mục xếp lớp nested
thì không cần thêm bước này.
... executables = ["main"]
Cuối cùng là trong tệp quản lý project
của Alire
phần executables
cần trỏ tới đúng tên tệp thực thi sau khi biên dịch xong sẽ tương ứng với tên tệp khởi đầu project
là main
.
Primitive & OOP Syntax
Khái niệm Primitive Operation
hay được gọi ngắn gọn là Primitive
do Ada
đặt ra - được sử dụng để chỉ những sub-program
được định nghĩa trong cùng package
với kiểu dữ liệu của tham số đầu tiên mà sub-program
đó tiếp nhận.
Ví dụ, chúng ta có package Person
đamg chứa định nghĩa record Entity
; Nếu như chúng ta định nghĩa thêm procedure Greet (Self : in Entity)
ngay trong package Person
thì lúc này procedure Greet
đang có tham số đầu tiên chính là kiểu Entity
được định nghĩa cùng cấp, và như vậy procedure Greet
sẽ được gọi là Primitive
của Entity
.
package Person is type Entity; type Class is access all Entity; -- type definition & sub-program declarations type Entity is record Name : String (1 .. 12); end record; procedure Greet (Self: in Entity, Other: in Entity); private -- nothing here end Person;
Và khi sử dụng các Primitive
như procedure Greet
ở đây thì chúng ta sẽ có thể phát động với cú pháp gọi phương thức phổ biến trong OOP
có dạng me.greet(someone);
.
with Ada.Text_IO; use Ada.Text_IO; package body Person is procedure Greet ( Self : in Entity ; Other : in Entity ) is -- local begin Put_Line ("Hi, " & Other.Name); Put_Line ("I'm " & Self.Name); end Greet; end Person;
Như vậy là chúng ta đã có thể bắt đầu mô phỏng các object
có khả năng tương tác với nhau trong code sử dụng tại main
.
with Ada.Text_IO; use Ada.Text_IO;
with Person; use Person; procedure Main is Me, Lucy : Person.Class;
begin Me := new Person.Entity' (Name => "Semi Dev_ "); Lucy := new Person.Entity' (Name => "Lucy "); Me.Greet (Lucy.all);
end Main;
Oh.. thế nhưng tại sao khi khởi tạo các object
chúng ta nên sử dụng kiểu con trỏ access
thay vì kiểu record
nguyên bản? Riêng điểm này thì mình đã phải rà lại và cập nhật gọn gàng nhất cho bài viết về con trỏ access
trong Sub-Series Procedural
tại đây: Recursive Record & Access Pointer. Khi làm việc với các record
, chúng ta sẽ sử dụng pattern
này và thao tác dereference
bằng khóa .all
để tạo một giao diện lập trình chung cho mọi trường hợp.
alr run Hi, Lucy
I'm Semi Dev_