Định nghĩa- Trong mẫu thiết kế này, một interface được xác định để tạo ra các đốitượng, những loại cụ thể của đối tượng sẽ được tạo ra lại để cho các lớpcon quyết định.- Điều này cho phé
KIẾN THỨC NỀN TẢNG
Lịch sử phát triển của phát triển phần mềm
Từ những năm 1950, Phát triển phần mềm là một hoạt động khá phức tạp Việc lập trình chỉ được thực hiện ở một vài nơi trên thế giới Ngôn ngữ phổ biến tại thời điểm đó là Assembly nên việc viết code trở nên phức tạp, không có hỗ trợ cấu trúc và khả năng tái sử dụng
Cho đến những năm 1960, các cấu trúc điều khiển (if, else, for, while,…) và các quy trình con ra đời, phương pháp Lập trình hướng cấu trúc (Procedure Oriented Programming – POP) dần được phổ biến POP là một kĩ thuật truyền thống, trong đó chương trình được chia thành các chương trình con và liên kết bằng biến cục bộ và con trỏ, giúp đơn giản hóa công việc mã lệnh Việc này cải thiện khả năng tổ chức của mã lệnh, và tăng khả năng tái sử dụng Tuy nhiên, sự phụ thuộc vào biến toàn cục (global variable) gây khó khăn trong việc quản lý dữ liệu
Phát triển phần mềm hướng đối tượng
Lý thuyết về OOP bắt đầu từ những năm 1960, và được thực hiện lần đầu tiên bằng ngôn ngữ Simula và tiếp tục phát triển đến những năm 80 với ngôn ngữ Smalltalk. Đến tận những năm 1980, lập trình hướng đối tượng mới được sử dụng rộng rãi nhờ sự phổ biến của các ngôn ngữ lập trình hướng đối tượng C++ hay Eiffel OOP càng trở nên đột phá khi có sự ra đời của ngôn ngữ JAVA vào những năm 90 Đến năm
2002, Microsoft đã giới thiệu ngôn ngữ lập trình hướng đối tượng C#, khiến OOP trở thành phương pháp chủ đạo và được dùng trong nhiều lĩnh vực khác nhau.
Phần mềm hướng đối tượng tập trung vào việc định nghĩa các đối tượng, trong đó kiến trúc của phần mềm dựa trên sự tương tác giữa các đối tượng với nhau để hoàn thành một công việc Sự tương tác này có dạng các thông điệp được gửi qua lại giữa các đối tượng Để trả lời một thông điệp, một đối tượng có thể thực hiện một hành động hoặc một phương thức.
OOP được cấu thành từ 2 phần chính:ClassvàObject.
Trong đó Class sẽ bao gồmAttributevàMethods,đóng vai trò là thành phần cơ bản định nghĩa nên đối tượng Class được định nghĩa như một bản thiết kế để xây dựng đặc tính của một đối tượng cụ thể như đặc điểm và hành vi.
Attribute: định nghĩa thông tin, đặc điểm của đối tượng.
Methods: là phương thức, hành vi thường có của đối tượng.
Trong khi đó Object là 1 đối tượng cụ thể của 1 Class, được xây dựng dựa trên 1 Class và có các trạng thái và hành vi riêng.
Ví dụ: LớpComputer bao gồm các thuộc tính như Brand, Color và có các phương thức như Start() và Shutdown() Đối tượng myComputer được tạo từ lớp Computer sẽ có các đặc điểm riêng như BrandL hay Color=WHITE và cũng sở hữu các phương thức Start() và Shutdown().
1.2.2 Bốn tính chất của hướng đối tượng
Encapsulation (Đóng gói): Đối tượng chỉ cần công khai các phương thức public để tương tác với các lớp khác, giúp ẩn các chi tiết bên trong đối tượng nhằm tăng tính bảo mật.
Inheritance (Kế thừa): Cho phép một lớp con có thể kế thừa và sử dụng thuộc tính và phương thức lớp cha Cho phép thêm thuộc tính hoặc mở rộng và sửa đổi phương thức (Polymorphism)
Abstraction (Trừu tượng): Tạo ra các khái niệm trừu tượng để chỉ định các phương thức và thuộc tính chung mà các lớp đối tượng cần tuân thủ.
Polymorphism (Đa hình): Cho phép phương thức kế thừa trong lớp con ghi đè lên phương thức lớp cha Từ đó có thể thực hiện nhiều hành động theo nhiều cách khác nhau tùy vào đối tượng đang thực hiện nó.
SOLID là từ viết tắt bởi 5 nguyên tắc thiết kế phần mềm hướng đối tượng được soạn ra bởi Robert C Martin và Michael Feathers.
● Single responsibility principle: Việc một lớp sở hữu quá nhiều chức năng sẽ trở nên cồng kềnh và khó đọc hơn Do đó, nguyên tắc thứ nhất yêu cầu mỗi một class chỉ mang 1 trách nhiệm duy nhất Điều này còn giúp việc sửa chữa trở nên dễ dàng hơn.
● Open/Closed principle: Nguyên tắc này yêu cầu khi muốn thêm chức năng cho chương trình, ta không nên sửa đổi Class cũ Việc đó sẽ phát sinh thêm mâu thuẫn với các Class khác, từ đó ta phải soát lại toàn bộ Thay vào đó, nguyên tắc yêu cầu tạo 1 class mới kế thừa từ class đã có Tuy sẽ làm tăng số lượng lớp trong chương trình nhưng bù lại ta có thể tách những phần dễ sửa đổi ra riêng biệt, đảm bảo không ảnh hưởng đến các phần còn lại
● Liskov substitution principle: Nguyên tắc nói rằng các phương thức lớp cha có thể bị thay thế bởi các phương thức của lớp con mà không làm thay đổi tính đúng đắn của chương trình.
● Interface segregation principle: Nguyên tắc cho rằng các interface cần được phần chia thành nhiều Interface nhỏ với nhiều mục đích khác nhau, tránh việc implement thừa method
● Dependency inversion principle: Nguyên tắc này đề cập tới phụ thuộc của module và các lớp trong hệ thống Các module cấp cao không thể phụ thuộc vào các module cấp thấp, thay vào đó cả 2 nên phụ thuộc vào các nguyên tắc trừu tượng ít biến động như Interface Như vậy chương trình sẽ trở nên linh động và có thể thay đổi bất cứ lúc nào.
Các Design Patterns phổ biến
- Mẫu thiết kế Abstract Factory cung cấp một interface để tạo ra các đối tượng liên quan hoặc phụ thuộc mà không cần xác định lớp cụ thể.
- Mẫu thiết kế này đặc biệt hữu ích khi hệ thống tạo ra, kết hợp và biểu diễn các đối tượng. b Mục đích sử dụng
- Mẫu thiết kế Abstract Factory giải quyết vấn đề của việc tạo ra và kết nối nhiều loại đối tượng khác nhau trong hệ thống đồng thời đảm bảo chúng tương thích với nhau.
- Bằng cách sử dụng mẫu thiết kế Abstract Factory, việc tạo ra và kết nối các đối tượng này trở nên linh hoạt và dễ quản lý hơn. c Cài đặt
Participants (Các lớp/Đối tượng):
- AbstractFactory: Định nghĩa interface để tạo ra một họ các sản phẩm.
- ConcreteFactory: Kế thừa từ AbstractFactory để tạo ra một họ các sản phẩm cụ thể.
- AbstractProduct: Định nghĩa interface cho một loại sản phẩm.
- ConcreteProduct: Triển khai interface AbstractProduct để xác định một sản phẩm cụ thể.
- Client: Sử dụng các interface được định nghĩa bởi AbstractFactory và AbstractProduct để làm việc với các sản phẩm.
[Hình 1] Sơ đồ cấu trúc Abstract Factory d Mẫu tương đồng
- Factory Method: Abstract Factory thường sử dụng Factory Methods trong việc triển khai của nó.
- Builder: Abstract Factory có thể kết hợp với mẫu thiết kế Builder để xây dựng các đối tượng phức tạp từng bước một Abstract Factory tạo ra builder, và builder có trách nhiệm xây dựng sản phẩm.
- Prototype: Abstract Factory có thể được sử dụng cùng với mẫu thiết kếPrototype để tạo ra các đối tượng mới bằng cách sao chép các đối tượng hiện có, cho phép tạo ra các đối tượng với các biến thể khác nhau.
- Trong mẫu thiết kế này, một interface được xác định để tạo ra các đối tượng, những loại cụ thể của đối tượng sẽ được tạo ra lại để cho các lớp con quyết định.
- Điều này cho phép các lớp con cung cấp hàm triển khai riêng của mình để tạo ra các đối tượng với các loại khác nhau được dẫn xuất từ cùng một interface Điều này mang lại tính linh hoạt và thúc đẩy khả năng tái sử dụng mã nguồn. b Mục đích sử dụng
- Mẫu thiết kế Factory Method định nghĩa một interface để tạo ra một đối tượng, nhưng nó để lại việc lựa chọn loại đối tượng cho các lớp con, tạo ra một đối tượng của một trong nhiều lớp có thể có.
- Nó cho phép một lớp trì hoãn việc khởi tạo đối tượng cho các lớp con của nó, cho phép một lớp chuyển trách nhiệm tạo đối tượng cho các lớp con của nó. c Cài đặt
Participants (Các lớp/Đối tượng):
- Creator: Khai báo Factory Method, trả về một đối tượng kiểu Product.
- ConcreteCreator: Triển khai Factory Method để tạo và trả về một phiên bản cụ thể của Product.
- Product: Định nghĩa interface của các đối tượng được tạo ra bởi Factory Method.
- ConcreteProduct: Triển khai interface Product để tạo ra các phiên bản sản phẩm cụ thể.
[Hình 2] Sơ đồ cấu trúc Factory d Mẫu tương đồng
- Abstract Factory: Factory Method thường được sử dụng kết hợp với Abstract Factory Trong khi Factory Method tạo ra một sản phẩm duy nhất, Abstract Factory tạo ra nhóm các sản phẩm liên quan.
- Singleton: Mẫu Singleton có thể được kết hợp với Factory Method để đảm bảo hệ thống chỉ có một đối tượng duy nhất của một tạo ra cụ thể, kiểm soát quyền truy cập vào việc khởi tạo các lớp tạo ra.
- Template Method: Factory Method là một sự đặc biệt hóa của mẫu Template Method Trong Factory Method, phương pháp mẫu là Factory Method chính và các bước là quá trình tạo ra sản phẩm.
- Mẫu Builder tách rời quá trình xây dựng một đối tượng phức tạp khỏi việc biểu diễn nó, cho phép quá trình xây dựng cùng một đối tượng có thể tạo ra các biểu diễn khác nhau.
- Bằng cách sử dụng một lớp builder, mã khách có thể chỉ định các bước xây dựng độc lập với biểu diễn cụ thể của đối tượng được xây dựng.
- Điều này tạo ra sự linh hoạt và khả năng tái sử dụng trong việc tạo đối tượng. b Mục đích sử dụng
- Mẫu thiết kế Builder tách quá trình xây dựng một đối tượng phức tạp khỏi biểu diễn của nó, cho phép cùng một quy trình xây dựng tạo ra các biểu diễn khác nhau.
- Mã khách chỉ định loại và nội dung của đối tượng cần tạo, và mẫu Builder cung cấp một loạt các bước để xây dựng nó. c Cài đặt
Participants (Các lớp/Đối tượng):
- Director: Quản lý quá trình xây dựng bằng cách sử dụng interface Builder.
- Builder: Khai báo các bước xây dựng cho sản phẩm.
- ConcreteBuilder: Thực hiện interface Builder để xây dựng và lắp ráp các phần của sản phẩm.
- Product: Đại diện cho đối tượng phức tạp đang được xây dựng.
- Client: Yêu cầu Director xây dựng sản phẩm.
[Hình 3] Sơ đồ cấu trúc Builder d Mẫu tương đồng
- Abstract Factory: Trong khi Builder tập trung vào việc xây dựng một đối tượng phức tạp, Abstract Factory liên quan đến việc tạo ra nhóm các đối tượng liên quan hoặc phụ thuộc vào nhau.
CÁC PHƯƠNG PHÁP ĐO LƯỜNG ĐỘ PHỨC TẠP
Depth of Inherit Tree - DIT
Độ sâu của cây kế thừa (DIT) đề cập đến độ dài tối đa của đường dẫn kế thừa trong hệ thống phân cấp hướng đối tượng Nó đo số cấp trong cây kế thừa từ một lớp cụ thể đến lớp gốc.
Trong lập trình hướng đối tượng, tính kế thừa cho phép các lớp kế thừa các thuộc tính và hành vi từ lớp cha, tạo ra mối quan hệ phân cấp Mỗi lớp dẫn xuất kế thừa các đặc điểm từ lớp cha của nó, và lớp cha đó là mang các đặc điểm từ lớp cha của riêng nó Quá trình này tiếp tục cho đến khi đạt đến lớp gốc DIT cung cấp thước đo định lượng về độ phức tạp Giá trị DIT cao hơn biểu thị hệ thống phân cấp sâu hơn với nhiều cấp độ kế thừa hơn, điều này có thể dẫn đến code phức tạp hơn và tăng khả năng ghép nối giữa các lớp, làm cho mã nguồn khó hiểu, khó bảo trì và sửa đổi hơn Bằng cách giới hạn độ sâu của sự kế thừa, chúng ta có thể giảm độ phức tạp của các tương tác giữa các lớp và cải thiện khả năng đọc và sử dụng lại mã.
Ví dụ với [Hình 25] DIT(A)=0 and DIT(G)=1 and DIT(H)=2
Number of Children - NOC
Số liệu NOC tập trung vào một lớp cụ thể và đếm số lượng lớp con trực tiếp kế thừa từ lớp đó, từ đó có cái nhìn sâu sắc về độ phức tạp và khả năng mở rộng của một lớp Một lớp có chỉ số NOC cáo cho thấy có thể cho thấy rằng nó đang cố gắng đáp ứng nhiều tính năng trong 1 lớp, dẫn đến nhiều lớp con.
NOC còn có thể đánh giá khả năng sử dụng lại và bảo trì, một lớp có nhiều lớp con nghĩa là chức năng cụ thể đã được phân bổ giữa các lớp con, khiến mã có thể khó để tái sử dụng.
Ví dụ với [Hình 25] NOC(A)=2 and NOC(F)=3 and NOC(B)=0
Coupling Between Object - CBO
Số liệu CBO đếm số lượng các lớp khác mà một lớp cụ thể được liên kết Nó xem xét cả sự phụ thuộc trực tiếp và gián tiếp Giá trị CBO cao cho thấy rằng một lớp có nhiều sự phụ thuộc vào các lớp khác Mặt khác, giá trị CBO thấp cho thấy khả năng kết nối lỏng lẻo và mức độ độc lập cao hơn.
Giá trị CBO cao có thể dẫn đến một số vấn đề trong phát triển phần mềm.
Những thay đổi trong một lớp có thể yêu cầu sửa đổi ở nhiều lớp khác khiến cho khó bảo trì Ngoài ra, khả năng kết nối cao có thể cản trở việc sử dụng lại mã và khiến hệ thống khó hiểu hơn.
Ví dụ với [Hình 25] CBO(A)=4 and CBO(B)=1 and CBO(F)=0, CBO(H)=0
ĐÁNH GIÁ ĐỘ PHỨC TẠP
- Giá trị trung bình của các độ đo được tính theo công thức:
- average_dit = trung bình cộng dit của tất cả các lớp trong dự án
Project name Class or Interface name
DIT NOC CBO Class or
Interface prototype-sample Prototype 0 1 5 interface prototype-sample ConcretePrototype 1 0 2 class prototype-sample Client 0 0 2 class prototype-sample Main 0 0 2 class
[Bảng 1] Bảng thống kê các giá trị của dự án sử dụng prototype
Giá trị trung bình của các trọng số của dự án prototype sẽ được tính như:
Pattern Average DIT Average NOC Average CBO
[Bảng 2] Bảng thống kê các giá trị trung bình của mẫu Prototype
- Giá trị trung bình của các độ đo cho từng mẫu được mô tả trong bảng dưới đây:
Pattern Average DIT Average NOC Average CBO
[Bảng 3] Bảng thống kê các giá trị trung bình của 23 mẫu thiết kế
GIẢI PHÁP CHUNG
Tổng quan
Để xác định một dự án phần mềm được viết bằng java trong việc tuân thủ mẫu thiết kế được thực hiện trong 4 bước sau:
- Phân tích cú pháp của Java (parsing Java code) để tìm ra các thông tin có trong mỗi lớp java như:
- tên lớp cha (do java chỉ cho kế thừa từ 1 lớp cha)
- danh sách tên các interface
- danh sách tên các lớp được sử dụng (dependencies)
2 Tính toán các thông số:
- Từ kết quả trả ra của quá trình phân tích để tiến hành tính toán các thông số cần thiết cho quá trình học máy như:
- DIT - Depth of Inherit Tree
3 Huấn luyện mô hình (train-test model):
- Tạo bộ dữ liệu bao gồm 23 mẫu thiết kế
- Sử dụng thuật toán học máy MLPClassifier - Multi-layer Perceptron classifier và bộ dữ liệu đã chuẩn bị trước để tiến hành huấn luyện mô hình.
- Lưu mô hình đã được huấn luyện để dự đoán.
- Load mô hình đã có sẵn và tiến hành dự đoán.
2 Tính toán các thông số
Chi tiết
4.2.1 Phân tích cú pháp (Parse Code)
- Phân tích cú pháp một ngôn ngữ lập trình cần xác định rõ:
- vd: class, abstract, boolean, int, public, extends, implement
- vd: [từ khóa][tên biến][phép gán]
- vd: int a = 5; “a” là tên biến
- Việc định nghĩa các việc định nghĩa các từ khóa và cú pháp sẽ thay đổi tùy thuộc vào ngôn ngữ lập trình được chọn và phiên bản (version) của ngôn ngữ.
- Tiến hành phân tích cú pháp và tạo cây phân tích (parsed tree) sample code:
``` if (Id.chars[Id.Count] ==‘\n')line++
[Hình 26] Cây phân tích - Parsed tree
- Cả dòng code if trên sẽ được chứa trong đối tượng thuộc lớp Stat (statement) trong đó bao gồm các thuộc tính theo thứ tự:
3 đối tượng thuộc lớp Exp (Expression - các phép so sánh)
5 đối tượng thuộc lớp Stat
- Một đối tượng thuộc lớp Stat sẽ ứng với một đoạn code có nghĩa (được định nghĩa hợp lệ trong Parser)
- Trong vd trên có 2 đối tượng thuộc lớp Stat ứng với 2 đoạn code:
- if (Id.chars[Id.Count] ==‘\n')line++
- Có thể thấy cả đoạn code “line++" thỏa mãn các điều kiện trong parser và hoàn toàn “có nghĩa" nếu đứng một mình.
- Các đối tượng thuộc lớp Exp:
- Luôn tuân thủ quy tắc so sánh: Exp[từ khóa so sánh (vd: “==”, ) ]Exp
- Có thể chứa danh sách các tên biến hoặc giá trị cần so sánh
- Bằng cách kiểm tra các node có trong cây để lấy ra các thông tin có trong mỗi lớp như sau:
- Tên lớp cha (do java chỉ cho kế thừa từ 1 lớp cha)
- Danh sách tên các interface
- Danh sách tên các lớp được sử dụng (dependencies)
Diagram của dự án có áp dụng mẫu prototype:
[Hình 27] Sơ đồ cấu trúc Prototype file parsed_projects.json:
4.2.2 Tính toán các thông số (Extract Information)
- Tiếp sau đó các thông tin đã lấy được ở mỗi lớp sẽ được phân tích và tính toán để lấy được các thông số dùng để dự đoán như sau:
- DIT - Depth of Inherit Tree
- DIT được tính toán theo quy tắc:
- nếu không có kế thừa (extends) hay implement:
- nếu chỉ có kế thừa mà không có implement:
- dit = dit(của lớp cha) + 1
- nếu chỉ có implement mà không có kế thừa:
- nếu có cả kế thừa và implement:
- dit = dit(của lớp cha) + dit(của interface) + 1
- NOC được tính toán theo quy tắc:
- nếu không có kế thừa (extends) hay implement:
- nếu chỉ có kế thừa mà không có implement:
- nếu chỉ có implement mà không có kế thừa:
- nếu có cả kế thừa và implement:
- CBO được tính toán theo quy tắc:
- cbo = tổng số lượng lớp phụ thuộc
- cbo của mỗi lớp phụ thuộc += 1 file transformed_projects.csv:
Project name Class or Interface name
DIT NOC CBO Class or
Interface prototype-sample Prototype 0 1 5 interface prototype-sample ConcretePrototype 1 0 2 class prototype-sample Client 0 0 2 class prototype-sample Main 0 0 2 class
[Bảng 4] Bảng thống kê các giá trị của dự án sử dụng prototype
4.2.3 Huấn luyện mô hình (train-test model)
- Tiến hành gán nhãn (labeling) cho mỗi lớp trong bộ dữ liệu
- Các nhãn bao gồm: 23 mẫu thiết kế + 1 nhãn chung (dành cho các lớp không có sử dụng nhãn)
- Thuật toán máy học MLP - Multi-layer Perceptron, là một dạng “vanilla" của mạng lưới thần kinh (neural network) bao gồm:
[Hình 28] Sơ đồ mạng neural với 2 lớp ẩn
- Hệ thống nhiều lớp ẩn (hidden layer) được kết nối chằng chịt (fully connected) với nhau
- Cơ chế lan truyền xuôi (feedforward) để cho ra kết quả (output) từ thông tin đầu vào (input)
- Cơ chế lan truyền ngược (backpropagation) dùng để chỉnh sửa trọng số (weight) ở mỗi đợt (epoch) nhằm giảm độ lỗi (reduce loss).
- Nguyên tắc học của MLP chia dữ liệu đầu vào (input) thành nhiều đợt (epoch), tại mỗi đợt:
1 dùng cơ chế lan truyền xuôi (feedforward) để tính toán được kết quả (output)
2 so sánh với nhãn thực tế (label)
3 sau đó tiến hành giảm độ lỗi (reduce loss) bằng lan truyền ngược (backpropagation)
- Sử dụng thuật toán phân loại học máy MLPClassifier - Multi-layer
Perceptron classifier, và bộ dữ liệu đã chuẩn bị trước để tiến hành huấn luyện mô hình.
- Lưu mô hình đã được huấn luyện dưới dạng file.pkl để dự đoán.
- Cần chuyển các dự án java thành data phù hợp với input của model có sẵn
1 Phân tích cú pháp - có được fileparsed_projects.json
2 Tính toán các thông số - có được filetransformed_projects.csv
3 Load mô hình có dạng file.pkl
4 Tiến hành dự đoán - có được 2 file result.csvvà result.json file result.csv:
DIT NOC CBO Class or
Prototype 0 1 5 interface Prototype prototype- sample
ConcretePrototype 1 0 2 class Prototype prototype- sample
Client 0 0 2 class Common prototype- sample
[Bảng 5] Bảng thống kê các giá trị và mẫu dự đoán của dự án sử dụng prototype file result.json:
CÀI ĐẶT
Docker Image
- Tạo docker container: dockerrun rm -it 21522246/se121:v1
- Tạo docker container với volume options: dockerrun rm -it -v /IO:/home/app_user/IO 21522246/se121:v1
- Thư mục /IO cần chứa:
+ Thư mục input (./IO/input) cần chứa các projects java:
+ Thư mục output (./IO/output)
- Thư mục /IO/output sau khi chạy sẽ bao gồm 4 files:
+ parsed_projects.json(./IO/output/parsed_projects.json)
+ transformed_projects.csv (./IO/output/transformed_projects.csv) + result.csv (./IO/output/result.csv)
+ result.json(./IO/output/result.json)
Source Code
- Pull gitrepository: gitclone https://gitlab.com/school-21522246/hk5/se121/antlr.git
- Install python package: pipinstall -r requirements.txt
- Thêm các project vào thư mục IO/input:
- Thư mục input (./IO/input) sẽ chứa các projects java:
- Thư mục /IO/output sau khi chạy sẽ bao gồm 4 files:
+ parsed_projects.json(./IO/output/parsed_projects.json)
+ transformed_projects.csv (./IO/output/transformed_projects.csv) + result.csv (./IO/output/result.csv)
+ result.json(./IO/output/result.json)
Danh sách màn hình
STT Tên Kiểu Chức Năng
1 title h1 Hiển thị “Upload Project"
2 uit_logo image Hiển thị “UIT logo”
3 btn_upload button Hiển thị cửa sổ chọn thư mục.
[Bảng 6] Bảng thống mô tả của màn hình upload
STT Biến Cố Xử Lý
1 on_click Lấy đường dẫn dự án và truyền tham số vào mô hình máy học.
[Bảng 7] Bảng danh sách các biến cố và xử lý của màn hình upload
[Hình 29] Màn hình kết quả
STT Tên Kiểu Chức Năng
1 title h1 Hiển thị “Upload Project"
2 uit_logo image Hiển thị “UIT logo”
3 table_result table Hiển thị danh sách các dòng từ file result.json.[Bảng 8] Bảng thống mô tả của màn hình kết quả