056.1 Bài 1
Chứng chỉ: |
Open Source Essentials |
---|---|
Phiên bản: |
1.0 |
Chủ đề: |
056 Cộng tác và Giao tiếp |
Mục tiêu: |
056.1 Công cụ Phát triển |
Bài học: |
1 trên 1 |
Giới thiệu
Có tới hàng ngàn công cụ phát triển phần mềm cho cả mã nguồn mở lẫn các sản phẩm độc quyền. Đương nhiên rồi! Các lập trình viên rất thích phát triển các công cụ phục vụ cho bản thân và các đồng nghiệp của họ; vì thế nên rất tự nhiên khi họ dành nhiều thời gian để cố gắng tìm ra một công cụ có thể hoạt động một cách tốt hơn, loại bỏ một số phiền toái khỏi quy trình làm việc hoặc giúp việc triển khai trở nên dễ dàng hơn.
Bài học này sẽ tập trung vào quy trình phát triển và giải thích cách các loại công cụ phát triển khác nhau được đưa vào quy trình này. Sẽ có rất ít các công cụ cụ thể được nêu tên vì bất kỳ công cụ nào trong số chúng cũng có thể bị coi là lỗi thời hoặc không còn thông dụng và có thể được thay thế bằng một công cụ được yêu thích mới vào thời điểm bạn đọc bài học này.
Mục tiêu Phát triển
Các công cụ lập trình đôi khi sẽ phải giải quyết nhiều mục tiêu xung đột với nhau. Sau đây là một số mục tiêu phát triển mẫu:
-
Tạo ra các chương trình có hiệu suất cao và chính xác.
-
Tạo ra các chương trình chạy nhanh.
-
Tạo ra các chương trình có khả năng mở rộng tốt.
-
Tạo ra các chương trình có khả năng thích ứng cao với nhiều kiểu người dùng, nhiều thiết bị (như máy tính xách tay, điện thoại di động và máy tính bảng) và cácmôi trường khác nhau.
-
Tăng tốc quá trình phát triển.
-
Tăng tốc việc triển khai các tính năng mới phát triển hoặc sửa lỗi.
-
Giảm bớt các tác vụ tẻ nhạt cũng như số lượng lỗi chính tả lập trình lọt qua giai đoạn thử nghiệm của chương trình.
-
Hỗ trợ mã và các thiết bị cũ mà tổ chức gặp khó khăn khi thay thế.
-
Làm việc cùng với các công cụ quen thuộc khác.
-
Cho phép khôi phục dễ dàng trong trường hợp có lỗi hoặc thay đổi kế hoạch.
Các quy trình được mô tả trong bài học này cũng như các công cụ phát triển chính là kết quả của những mục tiêu này (bên cạnh nhiều mục tiêu khác nữa).
Quy trình Phát triển Chung
Phần này sẽ so sánh hai mô hình chung có tầm quan trọng trong lịch sử: mô hình thác nước (đã được giới thiệu trong bài học trước) và tích hợp liên tục/phân phối liên tục (CI/CD).
Mô hình Thác nước
Mặc dù mô hình thác nước vẫn được sử dụng rộng rãi — đặc biệt là bởi các tổ chức lớn — nhưng nó đã không còn được nhiều nhà phát triển ưa chuộng nữa. Mô hình này phổ biến từ những năm 1950 đến những năm 1970. Nhiều tính chất của mô hình này ngày nay vẫn có thể được tìm thấy rộng rãi (chẳng hạn như các định nghĩa chính thức về yêu cầu và các chương trình kiểm thử ngay trước khi phát hành — tức quy trình đảm bảo chất lượng).
Ngay cả vào thời điểm thuật ngữ “thác nước” được đặt ra cho mô hình này, nó đã bị mất uy tín rộng rãi. Các vấn đề gồm có:
-
Việc thay đổi các yêu cầu trở nên khó khăn một khi quá trình phát triển bắt đầu.
-
Các yêu cầu và thiết kế thường bị hiểu sai vì văn bản thuần ngôn ngữ thông thường có thể sẽ khá mơ hồ. Vấn đề này có thể dẫn đến việc các sản phẩm không đáp ứng được các yêu cầu dự kiến và mất nhiều tháng để khắc phục.
-
Quy trình này diễn ra tương đối chậm. Một bản phát hành mới có thể chỉ được thực hiện mỗi năm một lần hoặc thậm chí là ít hơn.
-
Các lỗi xuất phát từ tương tác giữa các mô-đun khác nhau rất khó phát hiện cho đến giai đoạn cuối của quy trình và điều này có thể dẫn đến càng nhiều sự chậm trễ hơn cho dự án.
-
Quy trình này tạo ra rào cản giữa các bộ phận khác nhau của tổ chức; điều này không tốt cho chất lượng sản phẩm cũng như tinh thần đồng đội của tổ chức.
Nguyên tắc Tích hợp Liên tục/Phân phối Liên tục (CI/CD)
Mô hình thác nước đã gặp phải thách thức vào những năm 1970 bởi một loạt các phong trào khởi nguồn cho mô hình được ca tụng nhất ngày nay (nếu không muốn nói là được sử dụng rộng rãi nhất): CI/CD (Continuous Integration/Continuous Delivery). Các giai đoạn của quá trình áp dụng các hoạt động CI/CD bao gồm Tuyên ngôn Agile được phát hành vào năm 2001, SCRUM và DevOps. Mô hình mới này được xây dựng dựa trên các nguyên tắc giao tiếp chặt chẽ giữa các thành viên trong nhóm phát triển, sự tham gia của người dùng cuối hoặc khách hàng, quy trình triển khai nhanh chóng các bản sửa lỗi và tính năng mới cũng như lịch trình kiểm thử liên tục và nghiêm ngặt để duy trì chất lượng trong một môi trường thay đổi nhanh chóng — đôi khi thậm chí là hỗn loạn.
CI/CD tự động hóa càng nhiều bước càng tốt trong các giai đoạn từ viết mã đến triển khai chương trình đã được hoàn thiện cho người dùng cuối. CI/CD yêu cầu phải định nghĩa chuẩn từng bước và thay thế một hành động của con người (chẳng hạn như cài đặt phần mềm thủ công) bằng một quy trình do một chương trình chạy. Do đó, CI/CD là một phần của phong trào “mọi thứ dưới dạng mã” và “cơ sở hạ tầng dưới dạng mã”.
Hơn nữa, một khi nhóm đã đưa các quy trình của mình vào mã, các quy trình này có thể được sửa đổi, nâng cấp và ghi lại theo trình tự thời gian giống như mã. Bất kỳ thứ gì do nhóm phát triển viết (cho dù là chương trình, quy trình tự động hay tài liệu) đều sẽ được lưu trữ trong hệ thống kiểm soát phiên bản. Điều này sẽ được thảo luận trong bài học sắp tới.
Khi một số quy trình được tự động hóa, chúng có thể chạy theo trình tự. Do đó, các nhóm phát triển thường nói về đường ống (pipeline) CI/CD — tức là một bước thành công trong đường ống sẽ tự động kích hoạt một hoặc nhiều quy trình tiếp theo. Đường ống phải được giám sát theo phương thức tự động để bất kỳ lỗi nào trong quá trình chạy đều sẽ khiến đường ống dừng lại và thông báo lỗi tới các thành viên trong nhóm phát triển.
Mặc dù các phần sau đây sẽ thảo luận về CI và CD một cách riêng biệt nhưng chúng thường được kết hợp với nhau và ranh giới giữa chúng sẽ không thật sự rõ ràng.
Tích hợp Liên tục (CI)
Tích hợp liên tục đề cập đến việc kết hợp nhanh các thay đổi nhỏ vào mã. Kho lưu trữ trung tâm được sử dụng để tạo sản phẩm cho người dùng cuối được gọi là kho lưu trữ cốt lõi (hoặc core repo). Mỗi lập trình viên sẽ tạo ra một không gian làm việc cá nhân (thường được gọi là hộp cát - sandbox) trên máy tính của họ và họ sẽ tải (pull) các tệp mình cần sửa hoặc nâng cấp từ kho lưu trữ cốt lõi.
Lập trình viên có thể sử dụng máy tính xách tay hoặc máy tính để bàn cá nhân (được gọi là hệ thống phát triển cục bộ) để tải xuống các phần có liên quan của kho lưu trữ cốt lõi rồi tải lên các thay đổi sau khi thử nghiệm cục bộ. Ngoài ra, lập trình viên có thể tận dụng xu hướng phổ biến được gọi là “điện toán đám mây” và làm việc trên hệ thống chia sẻ do tổ chức của họ chạy và quản lý — tức một hệ thống phát triển từ xa.
Người lập trình thường kiểm tra lỗi bằng một trình gỡ lỗi (debugger) — một chương trình riêng biệt chạy công việc của lập trình viên trong một môi trường được kiểm soát. Chúng ta sẽ thảo luận về trình gỡ lỗi và các công cụ phát hiện lỗi khác ở các phần sau.
Để phát hiện lỗi sớm nhất có thể trong quá trình phát triển, lập trình viên cũng sẽ chạy các thử nghiệm trên mã trước khi tải nó lên kho lưu trữ cốt lõi. Chúng được gọi là thử nghiệm đơn vị vì mỗi thử nghiệm sẽ tập trung vào một phần tử nhỏ của mã (ví dụ như liệu liệu một hàm có tăng bộ đếm như dự định hay không).
Giai đoạn cuối cùng trong CI là kiểm tra các thay đổi mà lập trình viên đưa vào kho lưu trữ cốt lõi. Ở giai đoạn này, các công cụ sẽ chạy các thử nghiệm tích hợp để đảm bảo lập trình viên không làm hỏng bất kỳ thứ gì trong toàn bộ dự án.
Bất kể tự động hóa đã phát triển đến mức nào, một số thành viên chuyên gia của nhóm vẫn nên can thiệp tại thời điểm tích hợp để đảm bảo sự thay đổi sẽ phù hợp với mong muốn của cả nhóm. Các thử nghiệm tự động có thể xác định rằng không có phần bị lỗi và thậm chí là liệu thay đổi có tạo ra hiệu ứng cần thiết trong chương trình hay không. Tuy nhiên, các thử nghiệm này không thể kiểm tra tất cả các nguyên tắc được nhóm coi là quan trọng.
Do đó, trước khi tiến hành triển khai, thành viên trong nhóm nên kiểm tra về vấn đề bảo mật, tuân thủ các tiêu chuẩn mã hóa, tài liệu phù hợp và các nguyên tắc khác (không có gì đáng ngạc nhiên khi cũng có các công cụ để thực hiện một vài trong số các nhiệm vụ này; chúng ta sẽ tìm hiểu về chúng trong các phần sau của bài học này).
Có rất nhiều thử nghiệm được tiến hành trong CI và chúng phải diễn ra một cách nhanh chóng và đáng tin cậy để hỗ trợ tốc độ phát triển hiện đại. Do đó, các công cụ hiện đại để tích hợp mã và chạy thử nghiệm phải được tự động hóa. Một lập trình viên phải có khả năng chạy toàn bộ bộ thử nghiệm đơn vị thông qua một lệnh hoặc một cú nhấp chuột duy nhất.
Nhìn chung, một thử nghiệm tích hợp một phần hoặc toàn phần sẽ chạy bất cứ khi nào lập trình viên tải mã lên kho lưu trữ cốt lõi. Bất kỳ lỗi nào được phát hiện thông qua các thử nghiệm đều có thể nhanh chóng được báo cáo tới lập trình viên.
Phân phối Liên tục (CD)
Như đã được giải thích trong phần trước, CI bao gồm các quy trình tự động tạo ra một phiên bản mới của chương trình trong kho cốt lõi. Khái niệm Phân phối liên tục đề cập đến bất kỳ điều gì xảy ra sau giai đoạn đó để đưa phiên bản mới ra bên ngoài và để người dùng cuối có thể hưởng lợi từ nó. D trong CD đôi khi được mở rộng thành “phát triển” (development) hoặc “triển khai” (deployment) bên cạnh định nghĩa “phân phối” (delivery).
Nhiệm vụ chính của CD là kiểm tra mã một cách kỹ lưỡng và tải mã đó lên máy tính của người dùng hoặc một kho lưu trữ ứng dụng.
Giai đoạn CD sẽ chạy một loạt các thử nghiệm để đảm bảo tối đa rằng sản phẩm có thể hoạt động tốt. Một bộ thử nghiệm tích hợp lớn hơn có thể được chạy cùng với một số thử nghiệm khác để xác định tác động của chúng đến người dùng, hiệu suất cũng như bảo mật. Phần sau của bài học này sẽ mô tả về một số thử nghiệm như vậy.
Thử nghiệm nên được thực hiện trên các hệ thống khác với các hệ thống sản xuất phục vụ người dùng. Một lỗi nghiêm trọng không chỉ có thể làm sập hệ thống sản xuất mà còn có thể làm hỏng dữ liệu người dùng. Hơn nữa, các thử nghiệm sẽ cạnh tranh lẫn nhau về thời gian hệ thống và làm giảm hiệu suất cho người dùng.
Vì vậy, để thử nghiệm, tốt nhất là thiết lập một môi trường điện toán hoàn chỉnh tương tự như môi trường sản xuất của dự án với các phần cứng và phần mềm tương tự. Môi trường trung gian nơi được sử dụng để chạy thử nghiệm trước khi triển khai thường được gọi là môi trường dàn dựng (staging environment).
Tất nhiên, việc tạo môi trường thử nghiệm có cùng quy mô với môi trường sản xuất là không thực tế; tuy nhiên, các yếu tố thiết yếu của môi trường sản xuất (như cơ sở dữ liệu) vẫn phải được đưa vào.
Một yêu cầu của tự động hóa CD là phân biệt giữa môi trường thử nghiệm và môi trường sản xuất. Tất cả các cài đặt và quy trình của mã đều phải được điều chỉnh theo môi trường mục tiêu.
CD cung cấp tiềm năng phát triển nhanh tốt nhất khi mã là một dịch vụ chạy trên hệ thống riêng của tổ chức. Ví dụ: nếu là một sàn bán lẻ cung cấp một trang web tương tác, chúng ta sẽ có quyền truy cập vào tất cả các máy chủ web của mình. Nếu muốn, chúng ta cũng có có thể cập nhật chúng nhiều lần trong ngày và nhanh chóng khôi phục các thay đổi nếu chúng gây ra sự cố cho người dùng.
Các Công cụ Phát triển Phần mềm phổ biến
Phần này sẽ trình bày về các loại công cụ phổ biến được các nhóm lập trình sử dụng ở ba cấp độ: tạo mã, thử nghiệm và triển khai.
Trình Biên dịch
Một công cụ lập trình cơ bản là trình biên dịch (complier) được sử dụng để chuyển đổi các ngôn ngữ lập trình cấp cao và phức tạp thành các lệnh chạy trên bộ xử lý của máy tính.
Các máy tính sẽ chạy mã máy gồm các chuỗi bit (một và không) mà bộ xử lý chuyển thành lệnh và dữ liệu: tải giá trị từ bộ nhớ vào thanh ghi, thêm giá trị của hai thanh ghi, v.v. Bộ xử lý được phân biệt bởi các định dạng và tập lệnh (set of instructions) khác nhau mà chúng có; vì vậy, chúng cũng có mã máy khác nhau. Các nhà cung cấp luôn cố gắng phát minh ra các bộ xử lý mới sử dụng cùng một mã máy để cho phép khách hàng chạy các chương trình cũ của họ trên các bộ xử lý mới. Khi các nhà cung cấp làm như vậy, chúng ta cần nhắc đến họ (families) của bộ xử lý.
Trong những năm đầu của lập trình, giai đoạn tiếp theo sẽ là hợp ngữ (assembly language) được sử dụng để cung cấp các thuật ngữ mà con người có thể đọc được (chẳng hạn như ADD) cho mỗi lệnh. Các lập trình viên sẽ viết bằng hợp ngữ và gửi mã của họ đến một công cụ gọi là trình dịch hợp ngữ (assembler) để dịch hợp ngữ thành mã máy. Mã máy cũng có cách gọi khác là ngôn ngữ máy.
Sau đó, các ngôn ngữ cấp cao hơn sẽ được tạo ra. Ngày nay, chúng ta có thể tạo các luồng điều khiển phức tạp mà thậm chí không cần chỉ định các chi tiết cho chúng; chúng ta chỉ cần chỉ ra kết quả mà mình mong muốn. Các chương trình này cần có một trình biên dịch để chuyển mã nguồn thành mã máy. Trình biên dịch có thể thực hiện các phép biến đổi và tối ưu hóa một cách khá thông minh.
Có khá nhiều các ngôn ngữ có sẵn nhiều trình biên dịch khả dụng. Chúng ta có thể tìm thấy rất nhiều trình biên dịch cho các ngôn ngữ phổ biến như C và Java. Trong cộng đồng mã nguồn mở và tự do, hầu hết mọi người đều sử dụng GCC (GNU Compiler Collection) hoặc trình biên dịch LLVM cho C và C++.
Ban đầu, trình biên dịch sẽ biên dịch từng tệp mã nguồn một để tạo ra một tệp đối tượng trung gian, sau đó kết hợp tất cả các tệp đối tượng thành một chương trình bằng cách gọi một công cụ khác có tên là trình liên kết (linker). Trình biên dịch hiện đại có thể biên dịch nhiều tệp cùng một lúc để thực hiện tối ưu hóa vượt qua ranh giới các tệp.
Trình biên dịch được sử dụng để biên dịch thành hợp ngữ; điều này có thể sẽ hữu ích vì mỗi loại bộ xử lý máy tính sẽ hỗ trợ một mã máy khác nhau, nhưng chúng cũng có thể hỗ trợ cùng một hợp ngữ. Hợp ngữ cũng có rất nhiều biến thể. Bước cuối cùng trong biên dịch và liên kết là tạo ra mã máy. Cũng giống như liên kết, trình biên dịch hiện đại sẽ biết cách lắp ráp mã thành mã máy.
Có một giai đoạn khác trong nhiều ngôn ngữ hiện đại là mã trung gian, thường được gọi là mã byte (byte code). Mã này đã được biên dịch thành một định dạng nhị phân đủ cao cấp để có thể di động. Ví dụ: ngôn ngữ Java được biên dịch thành mã byte để chương trình có thể được tải lên nhiều loại bộ xử lý khác nhau.
Trong quá trình tạo mã byte từ mã nguồn, trình biên dịch đã thực hiện phần lớn công việc. Sau đó, mỗi máy tính sẽ lưu trữ phiên bản chương trình riêng của mình được gọi là máy ảo để hoàn tất quá trình chuyển đổi từ mã byte thành một tập lệnh mà máy ảo có thể thực thi. Đôi khi mã byte cũng được biên dịch thành mã máy. Việc chuyển từ mã byte sang một tập lệnh thuận tiện hơn cho người dùng cuối so với việc chuyển từ ngôn ngữ lập trình cấp cao sang mã máy. Đây là một lợi thế lớn cho Java khi nó được phát minh vì các nhà thiết kế muốn nó chạy trong một trình cắm máy ảo cho các trình duyệt web nơi bộ xử lý và môi trường hoạt động của người dùng có thể nói là khá đa dạng.
Các nhà thiết kế Java đã quảng bá lợi ích của mã byte thông qua câu khẩu hiệu tiếp thị: “Biên dịch một lần, chạy mã mọi nơi”. Một số lợi thế khác của mã byte dần xuất hiện sau đó. Các ngôn ngữ mới có thể được tạo ra để cung cấp một trải nghiệm rất khác cho các lập trình viên (hy vọng làm cho công việc lập trình trở nên dễ dàng hơn và tạo ra các mã dễ bảo trì hơn) trong khi tạo ra cùng một mã byte mà máy ảo Java hỗ trợ. Các hàm từ các ngôn ngữ này có thể dễ dàng kết hợp vào các chương trình Java hiện có.
Có một số ngôn ngữ (chẳng hạn như Python) mà chúng ta không cần phải biên dịch mã nguồn: chúng ta chỉ cần nhập các câu lệnh vào một công cụ xử lý được gọi là trình thông dịch (interpreter) và công cụ này sẽ chuyển đổi mã trực tiếp thành mã máy và thực thi mã đó. Trình thông dịch luôn chậm hơn so với trình biên dịch; tuy nhiên, hiện nay, một số trình thông dịch đã đủ hiệu quả để không gây ra nhiều ảnh hưởng đến hiệu suất. Tuy nhiên, một số thư viện phổ biến trong ngôn ngữ Python sẽ có bao gồm cả những hàm mà các nhà phát triển có thể truy cập được bằng ngôn ngữ thông dịch nhưng được lập trình bằng ngôn ngữ C để tăng tốc độ thực thi.
Nhiều ngôn ngữ được thông dịch cũng cung cấp cả trình biên dịch. Chúng sẽ tạo ra mã byte hoặc mã máy; điều này sau đó sẽ giúp chúng thực thi nhanh hơn trình thông dịch.
Có nhiều công cụ dựng để giúp lập trình viên quản lý các tệp và hàm. Một chương trình phức tạp có thể chứa tới hàng trăm tệp và lập trình viên sẽ cần phải biên dịch các kết hợp tệp khác nhau bằng các tùy chọn biên dịch khác nhau tại các thời điểm khác nhau (ví dụ như để hỗ trợ gỡ lỗi). Một công cụ xây dựng sẽ cho phép lập trình viên lưu trữ các tùy chọn và kết hợp tệp khác nhau và dễ dàng chọn loại bản dựng mong muốn. Maven và Gradle là các công cụ xây dựng phổ biến cho Java và các ngôn ngữ liên quan.
Công cụ tạo Mã
Lập trình viên không cần phải bắt đầu viết mã từ những ký tự đầu tiên. Gần đây, nhiều dịch vụ đã xuất hiện để tạo mã dựa trên các mô tả thuần văn bản của người dùng về những gì họ cần. Đây là một dạng AI tạo sinh (generative AI). Cũng giống như các dịch vụ tương tự khác, một số người cũng phàn nàn rằng nó sử dụng công sức của các lập trình viên trước đây mà không trả một khoản phí nào cho họ và tạo ra các mã nguồn yếu hơn (cho đến nay). Bên cạnh những tranh cãi về đạo đức, nhiều lập trình viên cho biết việc tạo mã tự động đã cải thiện đáng kể năng suất của họ.
Một dạng tạo mã đã có trong nhiều ngôn ngữ lập trình trong nhiều năm là tái cấu trúc (refactoring). Tái cấu trúc sẽ kiểm tra cả một chương trình lớn và có thể dễ dàng phát triển thành một mớ hỗn độn các tệp và hàm theo thời gian. Quy trình tái cấu trúc sẽ luân chuyển các hàm để tạo ra một cấu trúc logic hơn cho chương trình nhằm theo đuổi khả năng bảo trì được cải thiện và giảm tỷ lệ trùng lặp mã nguồn.
Một số lập trình viên sẽ được giao nhiệm vụ tái tạo chức năng của mã khác. Có thể họ sẽ phải viết một chương trình mới để thay thế một chương trình cũ bằng mã nguồn bị thiếu phải ngừng hoạt động. Hoặc họ cũng có thể bắt chước một chương trình của đối thủ cạnh tranh. Loại nghiên cứu này được gọi là kỹ thuật đảo ngược (reverse engineering). Một công cụ hữu ích cho mục đích này chính là trình dịch ngược (disassembler); nó sẽ chuyển mã máy thành hợp ngữ. Ngoài ra, chúng ta còn có trình dịch ngược cho mã byte.
Trình gỡ lỗi
Hầu hết các lập trình viên đều sẽ dành nhiều thời gian để gỡ lỗi hơn là viết mã. Con người khó có thể luôn luôn suy nghĩ logic một cách toàn diện; do đó, chúng ta luôn có khả năng quên mất một số chi tiết mà máy tính yêu cầu để chạy chương trình theo cách mà chúng ta muốn. Vì thế mà mã của chúng ta thường sẽ thất bại trong những lần thử nghiệm đầu tiên. Chúng ta có thể hưởng lợi rất nhiều từ trình gỡ lỗi (debugger) khi đối mặt với vấn đề này.
Trình gỡ lỗi sẽ hỗ trợ các công tác nghiên cứu chuyên sâu để có thể cắt giảm đi nhiều giờ làm việc của quá trình tìm lỗi.
Một lập trình viên có thể yêu cầu chương trình dừng tại một số vị trí quan trọng trong chương trình (một điểm nghỉ, còn được gọi là breakpoint) — chẳng hạn như phần đầu của một hàm hoặc một vòng lặp. Trình gỡ lỗi có thể hiển thị giá trị của các biến và thậm chí là các thanh ghi máy tính tại điểm hiện tại trong chương trình. Lập trình viên cũng có thể chạy từng câu lệnh một và xem kết quả (chạy bước đơn - single stepping). Nếu lập trình viên muốn xem khi nào một biến thay đổi và cách nó thay đổi trong quá trình chạy, họ có thể đặt một điểm quan sát (watchpoint).
Trình gỡ lỗi nổi bật nhất trong thế giới mã nguồn mở và tự do — đặc biệt là đối với C và C++ — là trình gỡ lỗi GNU; nó hoạt động cùng với trình biên dịch GNU đã đề cập tới ở trên. Các ngôn ngữ khác cũng sẽ có các trình gỡ lỗi chuyên dụng của chúng.
Công cụ Phân tích
Mặc dù việc gỡ lỗi thường có thể phát hiện ra lỗi một cách nhanh chóng nhưng tốt hơn hết là chúng ta nên loại bỏ các lỗi chính tả và các vấn đề cơ bản khác trong những giai đoạn đầu quá trình lập trình. Có nhiều công cụ phân tích rất tinh xảo để kiểm tra một chương trình. Công cụ Phân tích tĩnh sẽ kiểm tra mã của chương trình. Công cụ Phân tích động sẽ chạy chương trình và kiểm tra các vấn đề trong quá trình thực thi.
Một trong những dạng phân tích tĩnh sớm nhất được gọi là trình phân tích xơ (linter). Nó có thể phát hiện ra các vấn đề như khi chúng ta gán giá trị của một biến dấu phẩy động cho một số nguyên. Điều này có thể có hoặc không tạo ra vấn đề và có thể hoặc không thể bị trình biên dịch phát hiện. Trong quá trình sản xuất, nó có thể dẫn đến các kết quả không chính xác.
Ngày nay, hầu hết các trình biên dịch đều có thể thực hiện công việc của một trình phân tích xơ. Một số trình biên dịch (chẳng hạn như trình biên dịch cho ngôn ngữ Rust) nổi tiếng vì tính nghiêm ngặt trong việc từ chối mã được viết một cách yếu kém.
Nhiều loại công cụ phân tích khác sẽ chạy biệt lập với trình biên dịch. Ví dụ: các công cụ phân tích bảo mật có thể tìm ra các vấn đề khiến một chương trình có khả năng cao sẽ bị hack. Một trong các lỗi phổ biến của các nhà phát triển có thể kể đến là khi mã của họ gọi một hàm và không kiểm tra xem hàm đó có trả về lỗi hay không.
Môi trường Phát triển Tích hợp (IDE)
Có rất nhiều lập trình viên sử dụng trình soạn thảo văn bản để nhập và chỉnh sửa mã của họ. Trình soạn thảo văn bản khác với trình xử lý văn bản vốn có nhiều định dạng (như in nghiêng và in đậm, dấu đầu dòng và danh sách được đánh số, v.v.). Trình xử lý văn bản sẽ tạo các ra văn bản không có trang trí; đây là điều mà các ngôn ngữ lập trình yêu cầu.
Ngoài ra, chúng ta còn có các công cụ tinh vi dành riêng cho việc giúp các lập trình viên phát triển các chương trình của họ. Các công cụ này sẽ cảnh báo về ngôn ngữ lập trình đang sử dụng. Ví dụ: nếu chúng ta bắt đầu nhập tên của một biến hoặc hàm, công cụ có thể gợi ý một ví dụ hoàn chỉnh. Các công cụ này có thể kiểm tra lỗi trong khi chúng ta viết mã, định dạng mã theo một cách nhất quán và gọn gàng, chạy các công cụ phân tích và trình gỡ lỗi, biên dịch mã, xử lý các phiên tải vào hệ thống kiểm soát phiên bản và xử lý các tác vụ khác. Do đó, chúng được gọi là môi trường phát triển tích hợp (Integrated Development Environments - IDE).
Eclipse là một IDE mã nguồn mở rất phổ biến.
Các Phương pháp Thử nghiệm Phần mềm phổ biến
Thử nghiệm là một phần quan trọng trong phát triển phần mềm. Các lập trình viên sẽ chạy các thử nghiệm đơn vị khi họ phát triển mã. Các loại thử nghiệm khác thường sẽ chạy khi một lập trình viên tải mã trở lại kho lưu trữ cốt lõi hoặc trong quá trình triển khai.
Thử nghiệm Đơn vị
Chúng ta đã thấy rằng một lập trình viên phải hết sức tìm kiếm hết sức cẩn thận các lỗi trước khi gửi mã để tích hợp vào kho lưu trữ cốt lõi. Việc thử nghiệm đơn vị rất quan trọng trong việc phát hiện lỗi.
Việc viết các thử nghiệm này vừa là một môn nghệ thuật, vừa là một môn khoa học; khối lượng mã thử nghiệm có thể sẽ vượt qua cả khối lượng của mã sản xuất. Thậm chí còn có một mô hình phát triển được gọi là phát triển theo hướng thử nghiệm (Test-Driven Development - TDD); trong đó, các lập trình viên sẽ viết các thử nghiệm trước khi viết mã mà họ muốn thử nghiệm. Những người ủng hộ TDD cho rằng nó sẽ lấp đầy các lỗ hổng trong quy trình thử nghiệm và giúp đảm bảo rằng mã sẽ thực hiện những gì mà lập trình viên muốn nó thực hiện.
Việc kiểm tra các sai sót trong quá trình thực thi chương trình cũng quan trọng không kém so với việc kiểm tra các tình huống hoạt động đúng cách. Nếu người dùng hoặc một phần khác của chương trình gửi dữ liệu đầu vào không hợp lệ cho một hàm, điều quan trọng là hàm phải phát hiện được ra vấn đề và báo cáo một thông báo lỗi phù hợp.
JUnit là một công cụ mã nguồn mở phổ biến được dùng để chạy các thử nghiệm đơn vị trên các chương trình Java.
Thử nghiệm Tích hợp, Hồi quy và Khói
Trong khi các thử nghiệm đơn vị tập trung vào các hành động riêng lẻ của các hàm cụ thể, nhóm phát triển cũng nên chạy các thử nghiệm ở cấp độ cao hơn để đảm bảo tổng thể sản phẩm hoạt động một cách bình thường. Các thử nghiệm thường sẽ mô phỏng hành vi của người dùng. Ví dụ: trong ứng dụng quản lý nhà hàng, các thử nghiệm có thể kiểm tra xem người dùng có nhận được món mà họ đã đặt hay không.
Khi một thay đổi trong một chương trình làm hỏng một số hàm hoạt động bình thường trước đó, lỗi này sẽ được gọi là hồi quy (regression) và các thử nghiệm sẽ được gọi là thử nghiệm hồi quy.
Khi một nhóm phát triển chuẩn bị phát hành một sản phẩm, giai đoạn thử nghiệm đầu tiên thường sẽ rất ngắn. Nhóm phát triển sẽ kiểm tra các hoạt động quan trọng nhất do một chương trình thực hiện và sẽ dừng thử nghiệm nếu có bất kỳ lỗi nào phát sinh, từ đó có thể tiết kiệm được thời gian. Loại thử nghiệm này được gọi là thử nghiệm khói (smoke test) vì một ứng dụng dễ gặp trục trặc như vậy rất giống như một thiết bị kém chất lượng dễ bị bắt lửa.
Một số sản phẩm sẽ liên quan tới tương tác của người dùng; các ứng dụng web và di động là những ví dụ phổ biến nhất. Do đó, nhiều công cụ đã được tạo ra để mô phỏng tương tác của người dùng. Thử nghiệm sẽ chạy tự động và kích hoạt mã (mã này sẽ chạy nếu người dùng nhấn nút). Selenium là một công cụ phổ biến trong danh mục này.
Thử nghiệm Chấp nhận
Các chương trình có giao diện người dùng cần một mức thử nghiệm bổ sung vượt qua mục tiêu chứng minh rằng chúng phản ứng đúng với một số đầu vào nhất định. Các chương trình cũng phải có một bố cục hợp lý trên màn hình. Thử nghiệm chấp nhận sẽ kiểm tra tác động của chương trình lên người dùng. Các nhóm đảm bảo chất lượng thường sẽ chạy các thử nghiệm này sau khi thử nghiệm tích hợp và hồi quy đã chứng minh rằng chương trình đã chính thức chính xác và đáp ứng được kỳ vọng của người dùng.
Thử nghiệm Bảo mật
Bảo mật là một khía cạnh vô cùng quan trọng và ngay cả một lỗi bảo mật nhỏ cũng có thể khiến một tổ chức phải chịu những thiệt hại rất lớn. Chúng ta đã thấy rằng các lập trình viên có thể chạy các công cụ phân tích để kiểm tra tính bảo mật của mã. Trong giai đoạn kiểm tra đảm bảo chất lượng, các thử nghiệm cũng có thể xác định xem chương trình có lỗ hổng hay không. Các thử nghiệm sẽ chạy chương trình với các đầu vào độc hại và đảm bảo rằng chương trình sẽ từ chối đầu vào mà không bị lỗi, thực hiện các hành động không mong muốn hoặc tiết lộ các thông tin nhạy cảm.
Một loại thử nghiệm đã được chứng minh là có giá trị trong một số tình huống là thử nghiệm mờ (fuzz testing). Khuôn khổ thử nghiệm này sẽ chỉ tạo ra các chuỗi rác ngẫu nhiên và gửi chúng đi làm đầu vào cho chương trình. Điều này nghe có vẻ như sẽ chỉ lãng phí thời gian nhưng nó lại thường phát hiện ra các lỗ hổng mà các thử nghiệm thông thường không phát hiện được.
Thử nghiệm Hiệu suất
Sau khi chương trình được đánh giá là hoạt động một cách chính xác bằng các thử nghiệm khác, các nhóm phát triển nên xác định xem chương trình có chạy đủ nhanh hay không. Thử nghiệm hiệu suất yêu cầu một môi trường tương tự như môi trường mà người dùng sẽ tương tác với chương trình. Ví dụ: nếu người dùng gửi yêu cầu từ xa qua mạng thì thử nghiệm hiệu suất cũng nên được thực hiện qua một mạng từ xa.
Một số thư viện lập trình sẽ được thử nghiệm thông qua các mức chuẩn mực (benchmarks): các thử nghiệm tiêu chuẩn được sử dụng để so sánh các thư viện khác nhau hoặc các phiên bản khác nhau của cùng một thư viện.
Môi trường Triển khai Chung
Một công cụ CI/CD sẽ cho phép lập trình viên xây dựng các đường ống vô cùng phức tạp. Giống như các chương trình máy tính, một đường ống có thể chứa nhiều thử nghiệm và nhánh. Bằng cách phân nhánh, chúng ta có thể thực hiện một tập hợp các hoạt động này trong môi trường thử nghiệm và một tập hợp khác trong môi trường sản xuất. Chúng ta có thể sử dụng công cụ này để tự động cài đặt cơ sở dữ liệu chính xác hoặc các phần mềm khác cần thiết cho các chương trình khác nhau.
CD sẽ chạy chồng lên DevOps. Trong các môi trường đám mây, các công cụ CD và DevOps sẽ tạo ra các hệ thống máy tính ảo theo thủ tục tự động với tất cả các thành phần cần thiết để các chương trình có thể chạy. Các công cụ tự động (đôi khi được gọi là các công cụ phối hợp - orchestration tools) sẽ kiểm tra các lỗi của hệ thống ảo và tự động khởi động lại chúng.
Công việc chính của một công cụ CD là khởi chạy và thực hiện từng bước thông qua từng đường ống. Công cụ này sẽ kiểm tra các kết quả của từng giai đoạn của đường ống và lựa chọn xem phải tiếp tục hay dừng lại. Công cụ này cũng sẽ cho phép chúng ta lập lịch và ghi lại các hoạt động của nó.
Thông thường, chúng ta sẽ phải chạy một tác vụ nhiều lần với các thay đổi nhỏ nhặt. Ví dụ: các nhóm phát triển sẽ phân biệt giữa việc triển khai vào môi trường thử nghiệm và việc triển khai vào môi trường sản xuất. Do đó, các công cụ CD sẽ cung cấp các tham số mà chúng ta có thể điền vào với các giá trị khác nhau khi chạy đường ống.
Đôi khi một tiến trình sẽ yêu cầu các lệnh được nhập theo cách truyền thống vào cửa sổ dòng lệnh. Do đó, một công cụ CD sẽ cung cấp các cơ chế để chạy các lệnh tùy ý. Thông thường, nó sẽ cung cấp các móc nối như preprocess
để chạy các lệnh trước một giai đoạn của đường ống và postprocess
để chạy các lệnh sau một giai đoạn của đường ống.
Jenkins có lẽ là công cụ mã nguồn mở phổ biến nhất đối với trường hợp phối hợp được mô tả trong phần này.
Bài tập Hướng dẫn
-
Một số cách để kiểm tra tính bảo mật của một chương trình là gì?
-
Tại sao bạn lại phải viết nhiều thử nghiệm đơn vị cho một hàm chương trình duy nhất?
Bài tập Mở rộng
-
Nhóm phát triển của bạn đã thừa hưởng một ứng dụng cũ chạy chậm và khó để thêm tính năng. Có cách nào có thể cải thiện ứng dụng mà không phải vứt bỏ và viết lại một ứng dụng mới từ đầu hay không?
-
Trong các dự án lớn, thường sẽ xảy ra trường hợp Nhóm A muốn một tính năng được triển khai trong một phần của hệ thống do Nhóm B duy trì, nhưng Nhóm B lại không coi tính năng đó là ưu tiên. Làm thế nào để Nhóm A có thể mã hóa tính năng đó như một phần của dự án của Nhóm B?
Tóm tắt
Bài học này đã nêu ra các loại công cụ được sử dụng trong suốt quá trình phát triển: trình biên dịch và các công cụ tạo mã khác, trình phân tích, các thử nghiệm và các công cụ CI/CD tự động hóa tích hợp và phân phối. Có nhiều tùy chọn cho từng hoạt động này và một công cụ phổ biến hiện nay có thể dễ dàng bị thay thế sau một năm. Việc hiểu rõ cách tất cả các công cụ này kết hợp với nhau trong quá trình phát triển sẽ giúp chúng ta xác định được rõ những yếu tố cần thiết đối với nhu cầu của mình.
Đáp án Bài tập Hướng dẫn
-
Một số cách để kiểm tra tính bảo mật của một chương trình là gì?
Đầu tiên, các chuyên gia — là chính con người — có thể kiểm tra mã.
Có nhiều công cụ phân tích tĩnh và động hiện có có thể phát hiện các hoạt động lập trình kém khiến chương trình có nguy cơ bị đe dọa về bảo mật.
Các công cụ khác sẽ gửi dữ liệu đầu vào độc hại đến các chương trình đang chạy và kiểm tra phản ứng của chúng.
-
Tại sao bạn lại phải viết nhiều thử nghiệm đơn vị cho một hàm chương trình duy nhất?
Một hàm chương trình thường phải chạy trên nhiều loại đầu vào khác nhau và mỗi loại đều xứng đáng có một thử nghiệm riêng. Ví dụ: hàm có thể xử lý giá trị đầu vào bằng 0 theo một cách đặc biệt. Bạn cũng cần dự đoán đầu vào không hợp lệ và viết các thử nghiệm để cho thấy hàm có thể xử lý đầu vào đó một cách phù hợp.
Đáp án Bài tập Mở rộng
-
Nhóm phát triển của bạn đã thừa hưởng một ứng dụng cũ chạy chậm và khó để thêm tính năng. Có cách nào có thể cải thiện ứng dụng mà không phải vứt bỏ và viết lại một ứng dụng mới từ đầu hay không?
Đầu tiên, hãy đảm bảo rằng dự án đang được kiểm soát phiên bản nếu nó chưa có sẵn ở đây.
Sau khi thêm một tính năng mới, hãy chạy các thử nghiệm hồi quy để xác định xem chương trình gặp lỗi ở đâu và chỉ định các lập trình viên để tìm ra hàm nào chịu trách nhiệm cho lỗi đó. Các hàm này có thể được thay thế một cách có chọn lọc.
Thử nghiệm hiệu suất có thể xác định các hàm cụ thể đang chạy chậm để từ đó, bạn có thể tập trung nỗ lực vào việc sửa hoặc thay thế các mã kém hiệu quả nhất.
Nếu mã được viết bằng một ngôn ngữ không còn phổ biến nữa, hãy cân nhắc về việc thêm các tính năng được viết bằng một ngôn ngữ mà nhóm ưa thích. Hãy đảm bảo rằng các hàm trong ngôn ngữ mới có thể được tích hợp với các hàm cũ.
-
Trong các dự án lớn, thường sẽ xảy ra trường hợp Nhóm A muốn một tính năng được triển khai trong một phần của hệ thống do Nhóm B duy trì, nhưng Nhóm B lại không coi tính năng đó là ưu tiên. Làm thế nào để Nhóm A có thể mã hóa tính năng đó như một phần của dự án của Nhóm B?
+ Nhóm B có thể cho phép Nhóm A tạo một nhánh mới, mã hóa tính năng và gửi nhánh đó cho Nhóm B để hợp nhất vào dự án của mình. Tuy nhiên, Nhóm A không thể được trao quyền để làm bất cứ điều gì mà họ muốn. Nhóm B phải cung cấp tài liệu và trợ giúp để Nhóm A tuân thủ các tiêu chuẩn của dự án. Một thành viên của Nhóm B cũng phải xem xét nội dung gửi của Nhóm A và chạy tất cả các hình thức thử nghiệm tích hợp thông thường.
+ Hình thức cộng tác này đôi khi được gọi là InnerSource vì nó giống với mã nguồn mở nhưng sẽ chỉ diễn ra trong một tổ chức duy nhất.