-
My private distributed storage.
Intro
Last semester, I studied System Administration subject and worked on a project about Ethereum applications. Specifically, my topic involved using Ethereum Private Blockchain as a distributed storage system. I found this quite challenging initially because I knew nothing about distributed storage or how to build DApps on Ethereum. I asked my teacher for help, and he suggested some keywords like Geth and IPFS. I then researched these topics extensively, experimented, and ultimately earned an A+ score for my project. I believe it’s worth sharing my experience with everyone who is interested. So, let’s dive into it!
This post does not explain in detail what Geth and IPFS are. Instead, I will outline the structure of my project and the way I implemented it.
My repository is here. It includes the set up tutorial, so I will not mention it in this post. I suggest reading the tutorial simultaneously with reviewing the code.
Geth and Ethereum Private Blockchain
Geth is an Ethereum client written in Go. Running Geth transforms a computer into an Ethereum node. Since I only have one computer, I decided to create multiple nodes using Docker, as I wanted to learn it. I set up three Ethereum nodes using Geth. In order to make my network private, I created a bootstrap node. The bootstrap node is a normal node that is designated to be the entry point that other nodes use to join the network.
So, I have 4 nodes in my private network. To start my private blockchain with these nodes, I needed to configure my chain a little bit. First, I created a local chain with a unique chainId, which Ethereum supports. I also configured some features for my private chain such as the gas limit and consensus protocol,…. Finally, I used Docker to run all the nodes with the set up above to create my private blockchain ^^. You can read my setup in Dockerfile and docker-compose.yml.
Actually, I primarily followed this tutorial from Geth , but I ran all the nodes in Docker.
To test my private blockchain, I created accounts for each node and made some transactions:
eth.sendTransaction({to: "0xb166b7aaed24a12b3dd5dfa668ba3b9d10b1950d", from: eth.accounts[0], value: 25000})
Here is the demo:
IPFS
IPFS is a distributed file system in a peer-to-peer (P2P) network architecture. The machines participating in the system can store and upload data without going through a server. When a file is added to IPFS, it is split into smaller blocks. Each block is hashed using a cryptographic hash function, producing a unique identifier (hash) for each block. This hash is returned to the user, who can then retrieve their file using this hash.
I created two nodes, each running IPFS via Docker to participate in my distributed system. They share the same SWARM key to join the same network and communicate with each other.
To run IPFS on the two Docker nodes, I added this image to them and copied the SWARM key:
FROM ipfs/go-ipfs:latest
COPY swarm.key /swarm.key
Here is how I set up and use my private IPFS:
Combine
Now it’s time to combine my private blockchain and IPFS. Users can use my project to upload pictures into distributed storage.
Here is the structure for my project:
There are 4 components:
Frontend: Where users interact with my system.
Backend: Handle request from user via the frontend.
IPFS: My distributed storage, consisting of 2 nodes
Private Ethereum Network: My private network, consisting of 3 nodes.
How it works
First, let’s look at the uploading process:
The user uploads a file with the corresponding name to our system via the frontend. The file and name are then sent to the backend. The backend uploads the file to the private IPFS and receives a hash. Next, this hash is stored in the private Ethereum network by invoking smartcontract via a transaction, which also indicates whether the file was stored successfully.
Now, let’s dive into the downloading process:
When a user requests his files via the frontend, the request is sent to the backend. The backend retrieves the hash file from the private Ethereum network by invoking the smart contract. Then, the frontend receives the hash and uses it to get the file from the private IPFS. Finally, the file is sent back to the user.
SmartContract
Above is the basic workflow of my project. To store and retrieve the file hash from the network, I wrote a smart contract
with two functions: one for storing the hash and one for retrieving it. I used Hardhat to deploy the contract on Ethereum.
Here is the contract and the setup of my project:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Store {
struct File {
string filename;
string cid;
}
mapping(address => File[]) public userFiles;
function storeFile(string memory _filename, string memory _cid) public {
File[] storage files = userFiles[msg.sender];
files.push(File(_filename, _cid));
}
function getUserFiles() public view returns (File[] memory) {
return userFiles[msg.sender];
}
}
# init hardhat
npx hardhat init
# select: Create an empty hardhat.config.js
# compile smart contract
npx hardhat compile
# deploy smart contract
npx hardhat ignition deploy ./ignition/modules/Store.js --network localhost
I recommend following the tutorial of HardHat here. I found HardHat quite user-friendly for beginners like me, and it took only two days to deploy everything. Since I coded the backend in Javascript, I needed to import ethers library to interact with the Ethereum blockchain.
Finally, let’s see how my project performs:
End
After this post, I hope you understand the architecture of my project, and this post will help you in some ways. You can find the full demo video and slide in Vietnamese here:
Slide
Video
If you have any questions or need clarification, feel free to comment on this post. Additionally, I encourage you to contribute to the project by creating issues or making pull requests on the repository.
-
Mình đã tạo một game bằng C++ và SDL2.0
Xin chào mọi người. Ở kì học thứ 2, mình được học môn Lập trình nâng cao, là môn thứ 2 để hướng dẫn học sinh về lập trình sau môn “Nhập môn lập trình” thuộc kì học 1. Tại môn này, mình đã được hướng dẫn làm một project game 2D bằng ngôn ngữ C++ và sử dụng SDL 2.0 để hỗ trợ. Đây là project đầu tiên của mình, và hôm nay mình sẽ viết về cách mà mình tạo ra nó.
1. Cài đặt môi trường và sử dụng SDL2.0
Về môi trường cài đặt, mình đã được giảng viên dành 1 buổi trên lớp để hướng dẫn cụ thể các bước về tải thư viện SDL2.0, chỉnh sửa các câu lệnh và chạy thử. Mình code game này ở trên CodeBlock , và có dựa theo hướng dẫn sau để cài đặt môi trường: Youtube và Lazyfoo.
Về việc sử dụng SDL2.0, mình tham khảo trên website Lazyfoo, ở đây có những hướng dẫn rất chi tiết theo từng bước, rất phù hợp cho việc tìm hiểu và học tập.
Bài viết này, mình sẽ không đi sâu về mặt kĩ thuật cài đặt và xử lý (vì trong tài liệu trên website Lazyfoo ở trên đã nói rất kĩ từng phần) mà chủ yếu là hành trình mình tạo ra game này.
2. Ý tưởng về game này
Mình là một người rất đam mê tìm hiểu về các bí ẩn và đặc biệt là các truyền thuyết về truy tìm kho báu nên ý tưởng đầu tiên nảy ra trong đầu mình sẽ là làm game về đề tài này. Tuy nhiên, để cụ thể hoá ý tưởng thì là điều không hề dễ dàng. Lý do là vì trong những tài liệu hay bộ phim về truy tìm kho báu, nhân vật chính thường phải trải qua rất nhiều thử thách và cạm bẫy thì mới có thể tiến đến kho báu. Kết quả là sau một khoảng thời gian suy nghĩ, mình quyết định tạo ra các mini-game trong game này, với mỗi mini-game tượng trưng cho mỗi thử thách cần vượt qua với độ khó tăng dần, cùng với đó là việc xây dựng nội dung từng thử thách sao cho có tính logic và cốt truyện. Mình nghĩ rằng nếu như chỉ code một game đơn giản thì điểm cho project này sẽ không cao, nhưng nếu là tổng hợp của nhiều game như thế thì có thể sẽ khác. Tiếp theo, mình sẽ phát triển loại game là dạng game đồ hoạ Pixel, như một số game nổi tiếng: Stardrew Valley , Harvest Town ,… Cuối cùng, khi ý tưởng gốc đã xong xuôi, mình sẽ đi vào phần chi tiết cho từng thử thách.
3. Thử thách thứ nhất
Khi bắt tay vào làm, mình vẫn chưa nghĩ ra được mini-game đầu tiên sẽ là gì cho phù hợp. Cho đến buổi thực hành thứ 2 ở môn này, giảng viên đã cho mình làm 1 bài tập nhỏ là sử dụng hàm Random trong C++ để tạo ra các số từ 1 đến 3, tương ứng với Kéo, Búa, Bao trong trò chơi One - Two - Three (Oẳn tù tì) huyền thoại. Từ đây, mình đã quyết định chưa Oẳn tù tì vào làm một mini-game đầu tiên, với độ khó là dễ, nhưng cũng dựa vào may mắn là chính. Và để thêm phần thú vị cho minigame này, mình đã xây dựng một cốt truyện nhỏ và đồ hoạ phù hợp để tăng tính hình thức và bắt mắt. Một bật mí nhỏ là khi chấm game này cho mình, thầy giảng viên phải chơi mini game này 3 lần mới có thể dành được chiến thắng.
4. Thử thách thứ hai
Thử thách này mình lên ý tưởng từ nhân vật tượng nhân sư Sphinx trong Ai Cập cổ đại. Nhân sư sẽ canh gác lăng mộ và chỉ cho phép người đi qua nếu họ trả lời đúng được câu hỏi của nó. Mình quyết định sẽ đưa nhân vật chính đi qua lăng mộ và phải trả lời câu hỏi của Nhân sư này. Tuy nhiên, để đơn giản (vì đây mới là thử thách số 2) nên nhân vật sẽ có 7 lần đoán số tuổi của Sphinx, với điều kiện là tuổi của Nhân sư là nhỏ hơn 100. Nếu đoán sai thì tất nhiên, nhân vật chính sẽ dừng chân tại đây. Đến đây, nhiều bạn sẽ hiểu được tại sao mình để là 7 lần đoán. Đó là dựa theo thuật toán tìm kiếm nhị phân mà mình đã được học từ môn Nhập môn lập trình.
5. Thử thách thứ ba
Sau khi làm xong 2 thử thách trên, mình nghĩ mình cần nâng cấp độ khó cho game lên một chút, và đòi hỏi thao tác của người chơi nhiều hơn. Trong nhiều bộ phim truy tìm kho báu kinh điển, nhân vật chính sẽ đi qua một đường hầm với nhiều bẫy ở xung quanh, đòi hỏi sự khéo léo và dẻo dai của nhân vật. Với ý tưởng này, mình cũng tạo ra một đường hầm để đi tới chìa khoá của kho báu, với rất nhiều bẫy như rắn độc, hố sâu dung nham, vòng lửa,… đòi hỏi người chơi phải nhanh nhẹn điều khiển để tránh mất máu đi đến đích để nhận chìa khoá.
Phần này đòi hỏi mình làm tỉ mỉ, từ việc điều hướng các vòng lửa, năng lượng cho tới việc xử lý va chạm và cân bằng lượng máu để có được trải nghiệm game tốt nhất.
6. Thử thách cuối cùng
Việc mình kết thúc hành trình ở thử thách thứ 4 vì mình lười :v, nhưng thật ra mình nghĩ việc dừng ở minigame số 4 là đủ để mọi người có thể trải nghiệm toàn bộ game, và có thêm thời gian cho mình hoàn thiện phần code.
Ở phần này, nội dung thử thách là việc trốn chạy khỏi đám zombie bảo vệ kho báu trong một khoảng thời gian nhất định. Đây là thử thách mình tốn nhiều thời gian làm nhất (cũng là thử thách khó nhất) vì phải code rất nhiều phần như tự động bắn, tự động nâng cấp sức mạnh, di chuyển bản đồ khi nhân vật chuyển động… Đây là minigame mình lấy ý tưởng từ game Vampire Survivors, nhưng mình làm đơn giản hơn rất nhiều. Ngoài ra, việc tự tạo các hình ảnh vũ khí, zombie,… cũng khiến mình cảm thấy khá là vất vả. Nhưng sau cùng, mình cũng tạo được 1 bản gọi là tạm được của tựa game này.
7. Những phần phụ khác
Tất nhiên, để tạo được game Truy tìm kho báu này, không thể không kể đến những bản đồ, nhân vật, quái vật, vũ khí,… Về phần nhân vật, quái vật,… mình lấy từ trang OpenGameArt. Đây là trang web chia sẻ miễn phí các spritesheet nhân vật, vũ khí,… thuộc dạng pixel 2D. Ngoài ra, mình cũng sử dụng phần mềm PhotoshopCS6 để chỉnh sửa, cắt ghép các phần sao cho phù hợp, tạo map, menu, bản đồ kho báu cho từng màn.
Bên cạnh đó, mình cũng tạo các cốt truyện nhỏ trong mỗi thử thách, từ việc đi qua kim tự tháp để nhận chỉ dẫn cho đến vượt qua những cạm bẫy để lấy được chìa khoá kho báu, qua đó giúp đảm bảo tính hợp lý về cốt truyện tổng thể cho game.
Tổng kết
Toàn bộ ở trên là những chia sẻ về quá trình làm game Truy Tìm Kho Báu của mình. Mọi người có thể tải mã nguồn của game tại đây. Hy vọng bài viết này sẽ giúp mọi người có một góc nhìn khác về việc lập trình một project game, một góc nhìn hứng thú và sáng tạo hơn.
-
Các mô hình phát triển phần mềm (P2)
Ở phần 2 này, mình sẽ giới thiệu về 2 loại mô hình truyền thống còn lại là Bản mẫu (Prototype) và mô hình xoắn ốc (Spiral Model).
3. Bản mẫu (Prototype)
Thực tế, các yêu cầu đặc tả từ khách hàng rất hiếm khi rõ ràng, đầy đủ ngay từ đầu để thuận tiện cho việc sử dụng mô hình thác nước hay chữ V, nên mô hình bản mẫu được ra đời nhằm giải quyết vấn đề này. Bản mẫu là mô hình phát triển dựa trên việc thiết kế các bản thử của phần mềm theo yêu cầu của khách hàng, và khách hàng tham gia vào quá trình phát triển, từ đó giúp có cái nhìn tổng quát về hệ thống.
Mô hình bản mẫu:
Trong sơ đồ trên, các pha có nhiều điểm tương đồng với mô hình thác nước. Tuy nhiên, ở pha tinh chế bản mẫu, các bản mẫu sau khi được khách hàng đánh giá, sẽ phải tinh chế theo các yêu cầu của khách hàng, rồi xây dựng lại bản mẫu khác đến khi đáp ứng những yêu cầu của khách hàng thì đưa sản phẩm.
Bản mẫu là mô hình có nhiều ưu điểm. Ưu điểm dễ nhận thấy nhất là việc nhanh có sản phẩm thử. Việc thực hiện bản mẫu cần có sản phẩm thử (bản mẫu) để khách hàng có thể đánh giá, từ đó giúp yêu cầu đặc tả kĩ càng hơn. Thứ hai là giải quyết các yêu cầu không rõ ràng. Việc đưa ra 1 bản mẫu, và thu thập yêu cầu của khách hàng dựa trên bản mẫu đó sẽ giúp ích cho khách hàng có thể hình dung ra được sản phẩm họ cần là gì, từ đó sẽ có đặc tả yêu cầu rõ ràng từ khách hàng, giúp cho việc phát triển phần mềm chính xác và đáp ứng được những gì khách hàng đưa ra.
Thực tế hiện nay, các bản mẫu được sử dụng để đề xuất cho khách hàng hoặc là một kĩ thuật thu thập yêu cầu cho các mô hình khác. Có nhiều lí do cho việc này. Thứ nhất, khi dồn chi phí và nhân lực cho việc phát triển bản mẫu, các công đoạn còn lại sẽ thiếu thời gian và chi phí. Nên kết quả là dù có đầy đủ yêu cầu rõ ràng nhưng sản phẩm vẫn gặp vấn đề về chất lượng, nhất là phần tài liệu và code.Thứ hai, việc chú trọng quá vào bản mẫu cho yêu cầu chức năng dẫn đến dễ bỏ qua các yêu cầu phi chức năng và đa số các dự án thường không đạt yêu cầu phi chức năng (ví dụ như không đạt hiệu suất thời gian, độ bảo mật, toàn vẹn dữ liệu, dễ tương tác,…).
4. Mô hình xoắn ốc (Spiral model)
Mô hình xoắn ốc là một mô hình chú trọng vào tính rủi ro của những dự án. Xoắn ốc là sự kết hợp giữa các mô hình Thác nước và Bản mẫu và thêm phần Phân tích rủi ro ở trong. Ở đây, bản mẫu sử dụng để thu thập yêu cầu cho mô hình cũng như phát triển tăng dần, tuần tự. Mỗi giai đoạn trong mô hình xoắn ốc sẽ bắt đầu với yêu cầu, mục đích thiết kế và kết thúc với việc khách hàng kiểm tra tiến độ của sản phẩm.
Đây là mô hình có tỉ lệ thất bại cao khi áp dụng cho các dự án lớn. Bởi vì trước khi thực hiện một dự án, các nhà phân tích rủi ro sẽ nhận thông tin dự án, sau đó đánh giá rủi ro và quyết định xem có nên làm dự án này hay không. Nhưng việc khả thi của phân tích rủi ro là rất thấp vì các công ty phải cần người có chuyên môn cao và kinh nghiệm để có thể phân tích được những loại rủi ro này. Mà trên thị trường, việc các công ti vừa và nhỏ có thể có những con người như thế là rất hiếm gặp.
Ngoài ra, việc quản lý rủi ro, thực hiện các pha nhiều lần cần công sức rất lớn và tài nguyên, con người nên mô hình xoắn ốc được ít dự án sử dụng, chỉ có những công ty lớn với dự án quy mô rộng, nhiều nguồn lực, nhân lực ưu tú và việc phân tích rủi ro cho dự án rất quan trọng thì mới sử dụng mô hình này.
5. Tổng hợp
Ở trên là các mô hình phát triển phần mềm truyền thống, được sử dụng qua rất nhiều dự án. Tuy nhiên, trong thực tế người phát triển sẽ dựa vào những mô hình này mà cải tiến, điều chỉnh sao cho phù hợp nhất đối với dự án của mình.
-
Các mô hình phát triển phần mềm (P1)
Mô hình phát triển phần mềm là một quy trình tiêu chuẩn để phát triển phần mềm. Nó xác định các giai đoạn/ pha trong xây dựng phần mềm, từ lúc bắt đầu định hình yêu cầu cho đến khi phần mềm hoạt động được. Mô hình phát triển đóng vai trò rất quan trọng trong việc giúp các nhà phát triển thực hiện phần mềm một cách có hệ thống và hiệu quả, đảm bảo chất lượng và yêu cầu từ khách hàng.
Có rất nhiều loại mô hình phát triển như thác nước, bản mẫu, RAD, TDD,… Trong bài viết này, mình sẽ giới thiệu về 2 mô hình đầu tiên trên 4 loại mô hình phát triển truyền thống cơ bản là mô hình thác nước (Waterfall Model), mô hình chữ V (V-model), bản mẫu (Prototype) và mô hình xoắn ốc (Spiral Model).
1. Mô hình thác nước
Mô hình thác nước là mô hình phát triển phần mềm theo quy trình tuần tự và liên tiếp. Sau khi kết thúc giai đoạn trước, thì mới chuyển sang giai đoạn sau, không thể có 2 giai đoạn được xử lý song song. Lý do bởi vì kết quả của giai đoạn này sẽ đóng vai trò là đầu vào, yêu cầu của giai đoạn tiếp theo.
Minh hoạ mô hình thác nước:
Trong giai đoạn định nghĩa yêu cầu, các yêu cầu từ khách hàng sẽ được xác định và đặc tả chi tiết trong tài liệu, làm cơ sở để thực hiện giai đoạn thiết kế. Ở giai đoạn thiết kế, các nhà phát triển phải thảo luận, tìm ra yêu cầu của phần cứng, và đưa ra kiến trúc tổng thể của hệ thống phần mềm. Trong pha triển khai, nhà phát triển sẽ lập trình các chương trình nhỏ để tích hợp trong giai đoạn tiếp theo. Cuối cùng, sẽ là việc tích hợp các chương trình và kiểm thử trước khi đưa ra cho khách hàng sản phẩm.
Mô hình thác nước có những ưu điểm nổi trội về chất lượng và thoả mãn các yêu cầu phi chức năng. Thứ nhất là việc dễ học, dễ áp dụng. Việc làm các giai đoạn một cách tỉ mỉ, cẩn thận kèm theo việc viết tài liệu song song sẽ khiến cho phần mềm có tài liệu hướng dẫn kĩ càng, phục vụ cho việc sử dụng và đào tạo khách hàng. Về chất lượng đây là mô hình giúp sản phẩm có chất lượng cao vì các giai đoạn được làm cẩn thận, dành nhiều thời gian cho việc kiểm thử, tích hợp.
Bên cạnh các ưu điểm, mô hình này cũng bao gồm nhiều nhược điểm sau:
Thứ nhất, đây là mô hình vận hành lâu, chi phí cao. Lí do là vì việc phát triển theo mô hình thác nước đảm bảo sản phẩm có chất lượng cao, do đó trong từng giai đoạn, phải làm một cách kĩ càng và cẩn thận bởi lẽ nếu sai phạm ở giai đoạn nào đó, việc quay ngược lên để sửa chữa là rất tốn thời gian. Ngoài ra, việc làm tài liệu, giảng giải các hoạt động ở giai đoạn trước để những người thực hiện giai đoạn sau có thể dễ dàng hiểu cũng tốn rất nhiều công sức của người làm phần mềm.
Thứ hai là chỉ phù hợp với dự án vừa và nhỏ, có yêu cầu rõ ràng từ đầu các dự án nhỏ, vừa thì yêu cầu sẽ đơn giản hơn, do đó dễ thiết kế và thực hành hơn. Các dự án lớn thì có yêu cầu phức tạp, phạm vi dự án có thể thay đổi nên mô hình thác nước là rất khó xác định và đặc tả tổng thể các yêu cầu cùng 1 lúc. Thứ ba, mô hình thác nước sẽ khiến cho việc sản phẩm có chậm. Việc làm tuần tự và phải đảm bảo chất lượng cao dẫn đến tốc độ ra sản phẩm chậm. Do đó ảnh hưởng đến tiến độ của dự án. Ngoài ra, việc ra sản phẩm chậm thì sẽ có ít thời gian hướng dẫn khách hàng sử dụng sản phầm. Điều này sẽ dẫn đến việc trong quá trình sử dụng, khách hàng sẽ vô tình gây ra các lỗi cho phần mềm.
Trong thực tế, việc xây dựng các pha đầy đủ và không mắc phải lỗi nào là điều rất khó xảy ra. Nên khi trong quá trình phát triển, gặp phải vấn đề thì người phát triển phải quay lại các pha trước đó, tìm và sửa lỗi, rồi lại tiếp tục đi theo quá trình tuần tự và liên tục của model.
2. V-model
Về vấn đề kĩ thuật, mô hình thác nước còn có các điểm cần khắc phục là việc kiểm thử vẫn đang quá chung chung, chưa được đề cập rõ ràng. Hơn nữa, việc sinh test muộn (bước cuối cùng mới sinh test để kiểm tra sản phẩm) sẽ dễ đến việc chât lượng bộ test thấp, không đủ tính khách quan. Từ đó khiến chất lượng của phần mềm sẽ không đảm bảo. Do vậy, V-model ra đời nhằm cải tiến những điểm yếu về kĩ thuật này.
Minh hoạ:
Mô hình chữ V cũng bao gồm các pha gần giống như thác nước. Tuy nhiên để tránh việc chung chung trong kiểm thử, mô hình này đã cụ thể các pha kiểm thử là kiểm thử tích hợp, kiểm thử hệ thống và kiểm thử cấp nhận. V-model đưa việc sinh các test song song với các giai đoạn. Ví dụ khi ở giai đoạn đặc tả yêu cầu, khi có yêu cầu, nhà phát triển sẽ sinh bộ test cho việc đặc tả đó, vv. Sau khi cài đặt xong, sẽ lấy những bộ test đó để kiểm tra hiểu quả của từng pha. Việc sinh test này sẽ khách quan hơn rất nhiều so với mô hình thác nước, và chất lượng bộ test cũng sẽ cao hơn. Lý do chính là việc sinh test do chủ quan người phát triển. Nếu như sinh test trước từng pha, test sẽ cụ thể hơn và giúp tăng khả năng phát hiện lỗi hơn rất nhiều. Ngược lại, việc sinh test sau khi có sản phẩm, bộ test sẽ rất chung chung, dễ pass.
Bên cạnh đó, việc kiểm thử được chia ra rất rõ ràng các pha như đã nói ở trên. Cụ thể thì unit test (kiểm thử đơn vị) sẽ chịu trách nhiệm kiểm tra từng thành phần phần mềm như mô hình thác nước. Kiểm thử tích hợp là nhóm các thành phần phần mềm do kiến trúc quy định, để kiểm tra sự tương tác của chúng với nhau. Đối tượng kiểm thử không phải là một hệ thống hoàn chỉnh. Ở pha kiểm thử hệ thống, hệ thống hoàn chỉnh sẽ được đội phát triển kiểm tra, đánh giá trước khi gửi cho khách hàng. Cuối cùng là kiểm thử chấp nhận, pha này sẽ do khách hàng dùng thử sản phẩm và đánh giá cuối cùng.
Tạm kết
Ở trên là 2 trên 4 loại mô hình phát triển phần mềm truyền thống. Ở phần 2, mình sẽ giới thiệu thêm về bản mẫu và mô hình xoắn ốc.