Để trở nên hiệu quả trong công việc, kĩ sư phần mềm cần phải rèn giũa các kĩ năng giải quyết vấn đề của bản thân và thành thạo các ngón nghề phức tạp, yêu cầu hàng năng trời học hỏi và luyện tập. Mặc cho những người mới gia nhập nghĩ gì, thì thực tế là việc hiểu một ngôn ngữ lập trình, một framework hoặc kể cả là các giải thuật KHÔNG phải là phần khó trong việc xây dựng phầm mềm.
Lấy ví dụ, hiểu các ngôn ngữ lập trình thì dễ, đặc biệt là các ngôn ngữ hướng thủ tục giống như C. Chỉ có 32 keywords trong C, và ý nghĩa của chúng thì rất dễ để hiểu:
auto | double | int |
break | else | long |
case | enum | register |
char | extern | return |
const | float | short |
continue | for | signed |
default | goto | sizeof |
do | if | static |
Đồng thời C cũng có 14 chỉ thị tiền xử lý, cũng không quá khó để hiểu:
#define | #error | #import |
#elif | #if | #include |
#else | #ifdef | #line |
#ifndef | #pragma |
Hiểu về ngôn ngữ lập trình thì không khó, nhưng ghép nối các chỉ thị với nhau để tạo thành một dòng lệnh có nghĩa thì phức tạp hơn rất nhiều. Về mặt concept, nó giống như việc viết lách với ngôn ngữ của loài người - như là tiếng Anh chẳng hạn. Từng từ trong tiếng Anh có thể hiểu được một cách dễ dàng, nhưng cách để ghép chúng lại cùng một chỗ để tạo ra các câu và đoạn văn giàu ý nghĩa, đúng ngữ pháp và dễ hiểu thì lại là vấn đề khác. Lúc đó chúng ta sẽ phải học hỏi từ nhiều chỗ, nhiều người, đồng thời tốn rất nhiều thời gian để luyện tập.
Việc xây dựng phần mềm, tương tự như vậy, sẽ yêu cầu nhiều hơn về kỹ năng giải quyết vấn đề so với viết code hoặc là hiểu biết về công nghệ. Nhưng để giỏi trong việc giải quyết vấn đề, chúng ta sẽ cần rất nhiều thời gian để luyện tập và tích luỹ kinh nghiệm. Một người kỹ sư phần mềm, trước hết, là một người giải quyết vấn đề, và sau đó, mới là một thợ code. Những thứ như ngôn ngữ lập trình, framework, giải thuật là các công cụ mà chúng ta có thể nắm được thông qua việc học tập. Còn giải quyết vấn đề, thì phức tạp hơn và khó học hơn, khi chúng ta cần phải luyện tập trong thời gian dài và cần cả người hướng dẫn nữa.
Thông qua luyện tập, kỹ sư phần mềm học được cách suy nghĩ để cho phép họ tìm ra các giải pháp hiệu quả cho các vấn đề cần giải quyết. Việc học thường diễn ra một cách tự nhiên trong quá trình làm việc, nhưng nó mất thời gian. Bạn có thể tăng tốc quá trình học của mình bằng cách xác định và tập trung cải thiện các kĩ năng cần thiết để suy nghĩ giống như một kĩ sư phần mềm có nhiều kinh nghiệm.
Trong bài viết này, thông qua một vấn đề không liên quan đến kỹ thuật, tôi sẽ giải thích 5 kĩ năng giải quyết vấn đề mà bạn cần học. Và vấn đề có tên là: Làm thế nào để pha cà phê cho bốn người với 4 khẩu vị khác nhau?
Có bốn người - A, B, C và D muốn uống cà phê, và họ có yêu cầu cụ thể như sau:
Bạn sẽ phải tìm ra một cách thức, hoặc giải thuật, để làm cà phê giống như yêu cầu và nhanh nhất có thể. Mục tiêu là tìm ra một cách để bạn có thể phục vụ cà phê cho cả 4 người cùng một lúc, và càng nóng càng tốt.
Làm cà phê cho 4 người theo yêu cầu là yêu cầu tổng thể cho vấn đề chúng ta đang nói đến trong ví dụ này. Tuy nhiên, bạn không thể đơn giản là làm cà phê với một thao tác duy nhất được. Như bất kì người nào biết cách pha cà phê sẽ nói cho bạn, bạn sẽ cần phải chia nhỏ công việc ra nhiều task nhỏ hơn. Mỗi task sẽ cần phải đủ đơn giản để có thể dễ dàng xử lý.
Dưới đây là danh sách các task cần phải hoàn thành để có thể pha cà phê thoả mãn yêu cầu đưa ra:
Khả năng chia nhỏ công việc thành các task là một điều tự nhiên với con người, và là một kĩ năng cần thiết để hoàn thành công việc. Đồng thời việc làm ra danh sách các task ở trên không yêu cầu bất kì một kĩ năng học tập hoặc luyện tập cụ thể nào khác ngoài việc biết cách làm cà phê. Nói cách khác, ai cũng có thể làm ra được danh sách đó, dù ít hay nhiều.
Tuy nhiên, sự khác nhau giữa các vấn đề gặp phải hàng ngày như là pha cà phê so với các yêu cầu phức tạp hơn như là xây dựng phần mềm ở chỗ, các bước dùng để xây dựng phần mềm hiếm khi được trau dồi và luyện tập một cách cẩn thận. Để có thể đưa ra được danh sách các task cần thiết cho việc tạo ra một phần cụ thể của phần mềm yêu cầu chúng ta phải làm nó rất nhiều lần trước đó. Việc này không thường xuyên xảy ra lắm trong thế giới phần mềm. Và đó cũng là lý do tại sao mà các kĩ sư phần mềm có kinh nghiệm không nhảy ngay lập tức vào viết danh sách các task. Thay vào đó, họ chia vấn đề ra thành các phần đơn giản và cụ thể hơn.
Kĩ sư sẽ xác định vấn đề cần giải quyết là Cái gì, và danh sách các task để giải quyết các vấn đề là Làm thế nào. Trong ví dụ về việc pha cà phê, danh sách các mục tiêu phụ (phần Cái gì) có thể được xác định như sau:
Để ý rằng không hề có mô tả cụ thể cho các công việc cần phải làm trong task, chỉ có mô tả cho kết quả cần đạt được và các task phụ thuộc.
Chúng ta cũng có thể tạo một biểu đồ để trực quan hoá danh sách ở trên. Các hình hộp đại diện cho các mục tiêu phục, và các mũi tên đi đến từng hình đại diện cho các yêu cầu cần thoả mãn để nhiệm vụ phụ có thể hoàn thành:
Trong trường hợp công việc là pha cà phê, việc viết ra list các task cần hoàn thành là vừa đủ. Danh sách sẽ hoạt động tốt với các vấn đề đơn giản, và bạn biết cách để giải quyết vấn đề đó mà không cần lên kế hoạch quá nhiều. Tuy nhiên trong việc xây dựng phần mềm, hiếm khi chúng ta gặp các vấn đề đơn giản như vậy.
Lên kế hoạch cho các mục tiêu phụ cho phép chúng ta trừu tượng hoá kết quả với hành động, và đó là một sự khác biệt quan trọng. Ví dụ như ở mục tiêu phụ số #4: chúng ta có phễu lọc được đổ đầy cà phê, có thể sẽ khiến chúng ta phải đến cửa hàng và mua phễu lọc. Điều cần quan tâm ở đây là mục tiêu cần đạt được, là cái gì cần hoàn thành, trong khi làm thế nào là một chuỗi các lựa chọn phù hợp với hoàn cảnh (Vd đã có phễu lọc chưa hay chúng ta phải tự đi mua).
Các mục tiêu phụ phía trên có thể hoàn thành lần lượt theo danh sách, tuy nhiên điều đó không thực sự tối ưu. Sự phụ thuộc giữa các mục tiêu này cho chúng ta một gợi ý về các công việc cần hoàn thành theo một trật tự nhất định và các công việc không cần theo thứ tự.
Lấy ví dụ, bạn có thể bắt đầu xay hạt cà phê, và trong khi máy xay cà phê đang hoạt động, chúng ta có thể đổ nước vào máy pha cà phê. Nguyên nhân là vì việc cho nước vào máy pha cà phê không bắt buộc chúng ta phải có cà phê đã được xay xong. Đồng thời, việc xay cà phê chỉ yêu cầu chúng ta một thao tác bật máy, còn lại là thời gian chờ máy xay hoạt động xong. Tuy nhiên nếu máy xay cà phê là thủ công và bắt buộc phải động tay động chân, thì đó hoàn toàn là một câu chuyện khác.
Hơn nữa, trong khi chờ máy pha cà phê hoàn thành công việc của nó, chúng ta có thể lấy cốc ra khỏi chạn bếp. Thực tế thì, không gì phụ thuộc vào việc cốc cần phải sẵn sàng cho đến khi chúng ta bắt đầu đổ các thành phần của từng cốc cà phê vào.
Bạn cũng có thể cho đường và kem vào cốc rỗng, trước khi cả cà phê làm xong. Làm như thế này sẽ khiến việc khuấy cốc cà phê không còn cần thiết nữa. Nếu bạn đổ nhiều cà phê vào một cốc đã có sẵn một chút kem và đường, các thành phần sẽ tự hoà trộn với nhau một cách tự nhiên mà không cần phải khuấy. Ngược lại, nếu bạn đổ đường hay kem vào một cốc đã có nhiều cà phê, bạn cần phải khuấy để chúng hoà trộn vào với nhau.
Chúng ta sẽ sắp xếp lại danh các task với mục tiêu tạo ra tối đa các tác vụ có thể làm song song, nhằm rút ngắn thời gian và bỏ qua một số bước không cần thiết. Danh sách các task với thứ tự tối ưu sẽ là.
Hoặc có thể biểu diễn dưới dạng biểu đồ tuần tự như sau:
Để ý rằng thứ tự của các task không ảnh hưởng đến danh sách các mục tiêu phụ. Mặc dù thứ tự thực hiện phụ thuộc và cần chỉ dẫn bởi các mục tiêu phụ, nhưng không làm thay đổi các mục tiêu đó. Các task không có sự phụ thuộc có thể sắp xếp theo bất kỳ thứ tự nào, và chúng ta sẽ tận dụng điều đó để tối đa hoá việc thực hiện nhiều task một lúc.
Thêm một điều nữa, việc nghĩ về các mục tiêu phụ đầu tiên có thể được hoàn thành nhanh chóng vì chúng ta không cần thiết phải quyết định sẽ hoàn thành chúng như thế nào. Khi đó việc chia nhỏ vấn đề ra thành nhiều phần nhỏ hơn sẽ hiệu quả hơn và khiến các công việc có thể hoàn thành một cách độc lập.
Mục tiêu làm cà phê như trong ví dụ là khá cụ thể và trong phạm vi hẹp. Nếu như vấn đề đổi thành bạn phải pha cà phê cho 5 người thay vì 4, và một số người thì muốn có si-rô vani trong cà phê, và một người thì lại muốn bỏ hết caffein, thì danh sách các mục tiêu phụ và công việc sẽ cần phải làm lại.
Lập trình viên được học cách thiết kế các giải pháp để họ không phải tái thiết kế chúng mỗi khi có một vài tham số thay đổi. Họ cũng học được cách trừu tượng hoá các vấn đề để một giải pháp có thể giải quyết được bất kỳ bộ vấn đề nào tương tự như vấn đề đã làm.
Trong ví dụ về việc pha cà phê, bạn có thể muốn trừu tượng hoá mục tiêu phụ để hoàn thành việc pha cà phê cho N người chứ không cố định là 4 người. Đồng thời bạn cũng muốn trừu tượng hoá các thành phần thêm vào cà phê như là kem hay đường, và gọi nó là phần thêm. Danh sách các thành phần này có thể thay đổi theo thời gian, như là si-rô, các loại hạt, rượu vodka và bất cứ thứ gì người ta có thể cho vào cà phê. Việc trừu tượng hoá như này cho phép số lượng X các phần thêm có thể thêm vào N cốc cà phê.
Một kiểu trừu tượng khác có thể là loại cà phê. Cà phê có và không có caffein là 2 loại rõ ràng nhất, nhưng cũng có thể có các loại khác như là cà phê rang cháy, rang vừa, rang nhẹ, cà phê Columbian, cà phê pha cho buổi sáng, ...
Trong khi đó máy pha cà phê chỉ có thể pha một loại cà phê trong một thời điểm, bạn có thể thấy việc này có thể khiến vấn đề trở nên phức tạp như thế nào. Nếu một vài trong số 4 người muốn uống cà phê thường, và số còn lại uống cà phê không có caffein, yêu cầu phục vụ cà phê cho cả 4 người trong cùng 1 lúc và đảm bảo cà phê càng nóng càng tốt sẽ trở nên rất phức tạp để hoàn thành. Ở thời điểm đó, bạn sẽ phải trừu tượng hoá định nghĩa về số lượng máy cà phê bạn có - Y cái nếu bạn có Y loại cà phê khác nhau. Đồng thời, nếu như bạn có Y máy pha cà phê hoạt động độc lập thì việc thực hiện các tác vụ song song sẽ yêu cầu thêm chút tinh tế. Lúc này, lập trình viên có thể muốn trừu tượng hoá số lượng người pha cà phê. Nếu bạn có K người pha cà phê hoạt động cùng nhau, thì mọi việc sẽ thay đổi như thế nào?
Đến một lúc, bạn có thể nhận ra cà phê cũng chỉ là một loại thành phần của cốc cà phê, vì thế sao phải làm nó khác biệt với các phần thêm khác nhỉ? Bạn có thể quyết định cà phê cũng là một thành phần thêm vào, và loại bỏ hoàn toàn các dữ liệu cụ thể cho cà phê. Tuy nhiên, cà phê yêu cầu xử lý và chuẩn bị một cách đặc biệt, do đó tất cả các phần thêm khác cũng nên cho phép có xử lý và chuẩn bị chung. Ví dụ, nếu rượu nóng đánh với trứng là một phần thêm, sẽ có thể có hẳn một luông xử lý mô phỏng việc làm ra nó. Nhưng ở thời điểm này, bạn có thể trừu tượng hoá sự chuẩn bị của bất kỳ thứ gì, và do đó rượu nóng đánh trứng cũng chỉ đơn thuần là một thứ mà bạn có thể tạo ra.
Liệu bạn đã bắt đầu thấy đau đầu chưa? Chào mừng đến với thế giới xoắn quẩy của việc trừu tượng quá. Bạn có thể thấy rằng việc trừu tướng hoá có thể làm phức tạp bức tranh tổng thể nhanh đến thế nào nếu như bạn khiến nó đi quá giới hạn? Chúng ta bắt đầu với một kiểu cà phê, 4 người cần phục vụ, thành phần thêm vào cố định và một người pha chế. Công việc thật đơn giản và tự nhiên. Nếu bạn thử trừu tượng hoá bất kì mặt nào của việc làm cà phê và suy ngẫm đến khả năng có thể có N người để phục vụ, X loại thành phần thêm vào, Y loại cà phê, K người pha chế... thì mọi thứ bỗng trở nên thật phức tạp, đặc biệt khi bạn muốn tối ưu chi phí và tốc độ thực hiện. Bạn đã đi từ việc làm cà phê sang làm bất kỳ sản phẩm nào dưới ánh mặt trời sử dụng bất kì quy trình nào. Wao!!!
Một kĩ sư phần mềm cầu toàn sẽ cố để trừu tượng hoá mọi thứ và kết thúc với một cỗ máy phức tạp rất nhiều so với mục tiêu ban đầu là phục vụ cà phê cho một gia đình 4 người. Một kĩ sư phần mềm có kinh nghiệm, thì ngược lại, sẽ trừu tượng hoá các thứ có thể giải quyết các trường hợp thuộc về bản chất, và có thể suy nghĩ đến các khả năng trừu tượng hoá khác, nhưng chỉ với mục đích để đảm bảo các quyết định ở thời điểm hiện tại sẽ không gây cản trở cho các nhu cầu trong tương lai.
Khi nào chúng ta cần dùng trừu tượng hoá? Đó là một nghệ thuật yêu cầu rất nhiều luyện tập. Kinh nghiệm thực tế là bạn chỉ nên trừu tượng hoá các trường hợp mà bạn nghĩ rằng nó sẽ chỉ yêu cầu ngắn hạn, và hãy cố để không gây cản trở việc trừu tượng hoá trong tương lai với các suy nghĩ cụ thể ở hiện tại có tầm nhìn hẹp.
Không việc gì phải phát minh lại cái bánh xe.
Không phải mọi thứ đều yêu cầu phải làm từ đầu. Kĩ sư phần mềm có kinh nghiệm luôn luôn cân nhắc sử dụng các công cụ đã có trước khi bắt đầu thiết kế một giải pháp từ đầu.
Ví dụ, thay vì phải pha cà phê, bạn có thể đến ngay Starbuck mua lấy 4 cốc và mang về cho bạn mình? Nếu bạn không có cốc, cà phê, máy pha cà phê, thì việc đến Starbucks là một giải pháp rẻ và nhanh hơn rất nhiều, đặc biệt nếu bạn không có ý định pha cà phê hàng ngày trong thời gian dài.
Tìm và sử dụng các giải pháp sẵn có là một trong các kĩ năng mà lập trình viên cần có khi giải quyết vấn đề.
Sau nhiều năm luyện tập, lập trình viên có kinh nghiệm bắt đầu suy nghĩ về phần mềm, và cách giải quyết vấn đề theo luồng xử lý dữ liệu trong một hệ thống. Trong vấn đề về máy pha cà phê, hãy suy nghĩ về luồng của nước, cà phê, cốc và các thành phần thêm vào, bắt đầu từ nguồn của chúng cho đến mục tiêu cuối cùng (Cốc cà phê).
Nước bắt đầu từ bồn, cà phê bắt đầu từ một túi đóng gói sẵn trong bếp của bạn - trước đó là ở một cửa hàng thực phẩm. Cốc bắt đầu từ chạn đựng cốc và các thành phần thêm vào thì xuất phát từ nhiều nguồn khác nhau tuỳ thuộc vào nó là cái gì.
Các nguyên liệu này (dữ liệu) đi theo một dòng chảy thông qua một loạt các bước xử lý, biến đổi và pha trộn. Trạng thái đầu tiên là nguyên liệu thô được xác định ở chỗ chứa của từng thành phần, và trạng thái cuối cùng là một loạt các cốc cà phê được pha chế thành công. Và địa chỉ cuối cùng là trong miệng của 1 ai đó.
Suy nghĩ theo luồng dữ liệu cho phép chúng ta trực quan hoá mục tiêu quan trọng nhất và các mục tiêu phụ với một series các hình khối và mũi tên. Các hình khối đại diện cho từng hành động ảnh hưởng đến các nguyên liệu trong luồng của hệ thống, và các mũi tên sẽ giống như các đường ống để vận chuyển các nguyên liệu từ nơi này đến nơi khác.