Hẳn chúng ta có biết môn võ Karate. Về môn võ này thì các võ sinh, khi học võ thì ít khi được nghe lí thuyết lắm. Chủ yếu là thực hành tại võ đường (dojo), quan sát các thầy và các anh chị đi trước thi triển quyền cước, mỗi bài quyền là một kata. Võ sinh sẽ học qua thầy, qua anh chị, qua bạn bè và tự thực hành, học từ cái đúng và cả từ việc rút kinh nghiệm những lỗi sai nữa. Võ sinh dần sẽ hoàn thiện bản thân, bắt đầu từ các kĩ thuật cơ bản (kihon). Việc học cứ thế diễn theo lối tích lũy từng ngày một. Võ sinh qua đó tắm mình trong triết lí của võ môn thông qua việc làm đi làm lại. Tất nhiên, để việc học hiệu quả và đúng hướng, môn sinh cần thực hiện theo một tập các nguyên tắc rất khắt khe chứ không phải cứ ngồi há mồm ra xem là được. Cứ như thế ngày qua ngày, cơ bắp, kĩ năng, tư duy, kinh nghiệm được hoàn thiện bằng thực hành. Sự tồn tại và phát triển bền bỉ của các võ phái chứng tỏ sự hiệu quả của phương pháp dạy này, học bằng thực hành và vận dụng.
Việc học lập trình cũng có thể được mô phỏng tương tự. Tôi sẽ giới thiệu các bạn phương pháp này. Người ta gọi là Coding Dojo. Chi tiết Coding dojo là gì thì chúng ta có thể tham khảo thêm ở đây: Coding Dojo là gì? – Tạp chí Lập trình (tapchilaptrinh.vn). Chúng ta lưu ý là quá trình dojo cần tuân thủ tuyệt đối TDD – Baby Step nhé.
Theo Codegym.vn thì quá trình Baby Step trông như thế này:
Bài này, chúng ta thử cùng nhau xem một ví dụ nhé.
Kata name: Prime factors Tên bài: Thừa số nguyên tố Factorize a positive integer number into its prime factors. Phân tích một số nguyên dương thành các thừa số nguyên tố của nó: For example: o) 2 -> [2] o) 3 -> [3] o) 4 -> [2,2] o) 6 -> [2,3] o) 9 -> [3,3] o) 12 -> [2,2,3] o) 15 -> [3,5]
Step 1: Write a test
<?php require_once 'Hiker.php'; use PHPUnit\Framework\TestCase; class HikerTest extends TestCase{ public function test_with_1_expect_empty() { $primeFactors = new PrimeFactor(1); $this->assertEquals([], $primeFactors->factors()); } }
Mưu đồ của chúng ta là định làm một lớp có tên là PrimeFactors. Nó trông đơn giản thế này:
Tên hàm này không bắt đầu bằng một động từ, nhưng chuyện đó không quan trọng bằng việc nó dễ hiểu.
Khi chạy test, chúng ta sẽ được trạng thái đỏ (Red bar).
E 1 / 1 (100%) There was 1 error: Error: Call to undefined method PrimeFactor::factors() |
Step 2: Fake it if posible.
Bước này chúng ta chỉ được viết rất ít code thôi, 1 ~ 2 dòng gì đó, nếu không thì cũng không nên quá 5 dòng. Và nếu được, hãy fake, ví dụ như thế này:
class PrimeFactor { public function factors() { return []; } }
Tạm thời gác lại chuyện tại sao hàm factors lại không bắt đầu là một động từ. Chúng ta thấy rằng, qua bước cài đặt này, chúng ta đã hình thành nên thiết kế cơ bản của class PrimeFactor. Bây giờ thì hãy chạy test.
. 1 / 1 (100%) OK (1 test, 1 assertion) |
Đoạn code này hơi ít nội dung nên tạm thời ta chưa có gì để refactor, chúng ta thực hiện tiếp việc viết test nhé. Hãy thêm hàm này.
public function test_factors_with_2_expect_2() {
$primeFactors = new PrimeFactor(2); $this->assertEquals([2], $primeFactors->factors()); } |
Chạy test:
.F 2 / 2 (100%)
There was 1 failure: 1) HikerTest::test_with_2_expect_2 Failed asserting that two arrays are equal. +++ Actual @@ @@ Array ( – 0 => 2 ) |
Hãy thêm chút xíu code nào, nhớ là chỉ chút xíu thôi nhé.
<?php class PrimeFactor { protected $number; public function __construct($num) { $this->number = $num; } public function factors() { if($this->number >1){ return [$this->number]; } return []; } }
Ở đây, chúng ta đã phải thêm một constructor để nhận dữ liệu truyền vào, thêm một câu lệnh if để kiểm tra. Code như này có thể xem là hơi nhiều, nhưng vẫn chấp nhận được. Thực tế, không có qui định bắt buộc thế nào là ít, nhưng tạm qui ước với nhau là dưới 5 dòng code nhé.
Bây giờ thì ta đã có một green bar (Trạng thái xanh).
.. 2 / 2 (100%) OK (2 tests, 2 assertions) |
Bây giờ, chúng ta cứ nhẹ nhàng thêm một test nữa nhé.
public function test_with_4_expect_2_2() { $primeFactors = new PrimeFactor(4); $this->assertEquals([2, 2], $primeFactors->factors()); }
Một số qui định, yêu cầu chúng ta phải thêm một test mà khiến cho code fail, một số khác cho rằng không bắt buộc phải như vậy, nên cứ tùy chúng mình. Ở đoạn code mà ta vừa thêm trên đây, hẳn nhiên là khiến cho code fail rồi.
..F 3 / 3 (100%)
There was 1 failure:
1) HikerTest::test_with_4_expect_2_2 Failed asserting that two arrays are equal. — Expected +++ Actual @@ @@ Array ( – 0 => 2 – 1 => 2 + 0 => 4 ) |
Bây giờ, việc xử lí cho code pass sang xanh cần lưu ý là, chúng ta chỉ làm để pass đến case này thôi nhé.
<?php class PrimeFactor { protected $number; public function __construct($num) { $this->number = $num; } public function factors() { $factors = []; $number = $this->number; while (0 === $number % 2) { $factors[] = 2; $number = $number / 2; } if($number >1){ $factors[] = $number; } return $factors; } }
Vừa rồi, chúng ta đã đưa $this->number ra biến tam và thêm vào một vòng while. Rõ ràng là, đoạn code này chỉ giải chỉ tạo ra các thừa số nguyên tố của 2 mà thôi. Nhưng như vậy cũng đã đủ để tạo thành green bar rồi.
… 3 / 3 (100%)
OK (3 tests, 3 assertions) |
Bây giờ, chúng ta thêm test nữa nhé. Chúng ta lưu ý là code này đã hoạt động đúng tới tận số 8 rồi, để nó fail thì phải dùng đến số 9.
public function test_with_9_expect_3_3() { $primeFactors = new PrimeFactor(9); $this->assertEquals([3, 3], $primeFactors->factors()); }
Đến lúc này thì nó sai mới là đúng.
…F 4 / 4 (100%)
Time: 00:00, Memory: 18.00 MB
There was 1 failure:
1) HikerTest::test_with_9_expect_3_3 Failed asserting that two arrays are equal. — Expected +++ Actual @@ @@ Array ( – 0 => 3 – 1 => 3 + 0 => 9 ) |
Rõ ràng, vì chúng chưa xét trường hợp thừa số nguyên tố là 3, nên ở đây mảng trả về là [9], vì thế nên gây ra lỗi này. Để khắc phục, chúng ta fake một chút xíu nhé. Sẽ hơi khó chịu, nhưng chúng ta sẽ làm sạch nó ngay thôi.
<?php class PrimeFactor { protected $number; public function __construct($num) { $this->number = $num; } public function factors() { $factors = []; $number = $this->number; while (0 === $number % 2) { $factors[] = 2; $number = $number / 2; } while (0 === $number % 3) { $factors[] = 3; $number = $number / 3; } if($number >1){ $factors[] = $number; } return $factors; } }
Vòng while ta mới thêm vào chỉ xét với trường hợp thừa số nguyên tố là 3, như vậy là bằng cách duplicate vòng while, chúng ta có thể giải quyết bài toán này.
…. 4 / 4 (100%)
OK (4 tests, 4 assertions) |
Tất nhiên code hơi thối rồi, chúng ta cần tái cấu trúc một chút cho thơm nhé.
Step 3: Refactoring.
Thực ra, có nhiều chỗ có thể tái cấu trúc từ các bước trước, tuy nhiên, tôi muốn để tại tận bước này mới làm để liệt kê hết một chỗ trong bài viết.
Trong phạm vi bài Kata này, chúng ta có thể tái cấu trúc một số nội dung sau:
Dùng hàm giải thích cho biểu thức khó hiểu
<?php class PrimeFactor { protected $number; protected $param; public function __construct($num) { $this->number = $num; } protected function is($number){ $this->param = $number; return $this; } protected function divisible($factor){ return 0 === $this->param % $factor; } public function factors() { $factors = []; $number = $this->number; while ($this->is($number)->divisible(2)) { $factors[] = 2; $number = $number / 2; } while (0 === $number % 3) { $factors[] = 3; $number = $number / 3; } if($number >1){ $factors[] = $number; } return $factors; } }
Biểu thức toán học khó hiểu đã được thay bằng hàm is()->divisible(), dễ hiểu hơn nhiều.
Tránh lặp code
<?php class PrimeFactor { protected $number; protected $param; public function __construct($num) { $this->number = $num; } protected function is($number){ $this->param = $number; return $this; } protected function divisible($factor){ return 0 === $this->param % $factor; } public function factors() { $factors = []; $number = $this->number; for($divisor = 2; $divisor <= 3; $divisor++){ while ($this->is($number)->divisible($divisor)) { $factors[] = $divisor; $number = $number / $divisor; } } if($number >1){ $factors[] = $number; } return $factors; } }
Bây giờ, ta có thể thêm testNhư ở trên, 2 đoạn code bị lặp lại đã được xử lí. Nhớ là sau khi refactor xong, nhất thiết phải chạy lại test để đảm bảo vẫn ở green bar.
public function test_with_25_expect_5_5() { $primeFactors = new PrimeFactor(25); $this->assertEquals([5, 5], $primeFactors->factors()); }
Ta có thể xử lí dứt điểm vụ này bằng cách:
<?php class PrimeFactor { protected $number; protected $param; public function __construct($num) { $this->number = $num; } protected function is($number){ $this->param = $number; return $this; } protected function divisible($factor){ return 0 === $this->param % $factor; } public function factors() { $factors = []; $number = $this->number; for($divisor = 2; $divisor <= sqrt($this->number); $divisor++){ while ($this->is($number)->divisible($divisor)) { $factors[] = $divisor; $number = $number / $divisor; } } if($number >1){ $factors[] = $number; } return $factors; } }
Chỗ này, nếu thích ta có thể refactor một chút như thế nàyMột số bạn có thể không hiểu sqrt($this->number) là gì, thực ra đó là cận trên của ước số đối với một số bất kì.
<?php class PrimeFactor { protected $number; protected $param; public function __construct($num) { $this->number = $num; } protected function is($number){ $this->param = $number; return $this; } protected function divisible($factor){ return 0 === $this->param % $factor; } protected function greatestDivisor() { return sqrt($this->number); } public function factors() { $factors = []; $number = $this->number; for($divisor = 2; $divisor <= $this->greatestDivisor(); $divisor++){ while ($this->is($number)->divisible($divisor)) { $factors[] = $divisor; $number = $number / $divisor; } } if($number > 1){ $factors[] = $number; } return $factors; } }
Vậy là chúng ta đã cùng nhau xem xét một kĩ thuật học tập và đào tạo lập trình. Chúng ta cũng nắm sơ lược kĩ thuật TDD – Baby step.
Chúng ta có thể tham khảo thêm cuốn Test-Driven Development By Example của Kent Beck để tìm hiểu thêm về TDD.Bài kata này cơ bản đến đây là xong, class PrimeFactor của chúng ta có thể phân tích thừa số nguyên tố cho cho các số nguyên dương lớn hơn 1. Chúng ta có thể bổ sung thêm một số case nữa cho những trường hợp đặc biệt, nếu cần, nhưng xin phép kết thúc ở đây được rồi.
Nguyên tắc của việc học này tích lũy cho nên nó khá hiệu quả và tạo kết quả lâu dài cho mọi người tham gia. Chúng ta có thể thử áp dụng tại nhóm của mình xem hiệu quả đến đâu rồi phản hồi vào comment cho mình với nhé.