Đừng để bị Design Patterns “dắt mũi”

Design Patterns có thể trở thành đồng minh tốt nhất khi được sử dụng một cách chính xác. Tuy nhiên, nếu dùng sai thì nó có thể sẽ hại đời chúng ta! Vậy nên ta cần phải tìm hiểu để làm thế nào sử dụng chúng một cách chính xác.
Vì design pattern là một giải pháp được xác định trước cho một vấn đề cụ thể, đã được chứng minh qua thời gian và được biết đến bởi software community, nên bạn cần áp dụng nó ngay khi bạn có cơ hội, càng sớm càng tốt.
  Depositphotos_29100055_l-2015-1-768x768
Tuy nhiên, làm sao biết khi nào tốt nhất để sử dụng một design pattern? Bài viết dưới đây sẽ cho các bạn hiểu rõ hơn về tầm quan trọng trong việc đánh giá khả năng của những thay đổi trong các phần code, cũng như hiểu được mục đích của pattern mà chúng ta đang sử dụng. Từ đó bạn sẽ dễ dàng đưa ra quyết định phù hợp.
Có 3 điều mà tôi muốn đề cập đến. Hãy cùng xem nhé!

Nếu bạn biết áp dụng các nguyên tắc thiết kế object-oriented, design pattern sẽ tự khắc xuất hiện một cách tự nhiên

Tôi tin rằng việc biết các nguyên tắc thiết kế object-oriented và áp dụng tốt như SOLIDKISS và YAGNI thì sẽ quan trọng hơn nhiều so với design pattern. Nếu bạn áp dụng những nguyên tắc này, design pattern sẽ tự nhiên mà xuất hiện.
Ví dụ bạn đang mở một app nhỏ đang hiển thị một bản nhạc và giả sử bây giờ chúng tôi sẽ chỉ vẽ các nốt nhạc. Trong số các class khác nhau của hệ thống, chúng ta sẽ thực hiện như sau:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<?php class NoteDrawer { public function drawHalfNote() { $this->drawNoteHead('white');
        $this->drawStem();
    }

    public function drawQuarterNote()
    {
        $this->drawNoteHead('black');
        $this->drawStem();
    }
}
Như bạn có thể thấy thì có sự trùng lặp rõ ràng ở đây. Sự khác biệt duy nhất đó chính là trong tham số áp dụng cho các phương pháp drawNoteHead. Cách tốt nhất để thoát khỏi sự trùng lặp thì cần phải làm như sau:
  1. Tạo một phương thức drawNote chung chung và copy đoạn giữa từ một trong những chức năng sẵn có.
  2. Biến đổi các tham số của phương pháp drawNoteHead thành một phương pháp mới. Sau đó, phương pháp này có thể được ghi đè bởi các children classes, vì vậy father class giữ basic logic và children classes giữ sự khác biệt.
  3. Thay thế tất cả drawHalfNote và drawQuarterNote với phương pháp drawNote mới của child class tương ứng.
Tiếp theo nhé!
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php abstract class NoteDrawer { public function drawNote() { $this->drawNoteHead($this->getHead());
        $this->drawStem();
    }

    abstract protected function getHead();
}

class HalfNoteDrawer extends NoteDrawer
{
    protected function getHead()
    {
        return 'white';
    }
}

class QuarterNoteDrawer extends NoteDrawer
{
    protected function getHead()
    {
        return 'black';
    }
}
Vẫn có thể có những giải pháp khác, nhưng ta cần hiểu mình muốn gì, đó là: loại bỏ sự trùng lặp code.
Hmm, bạn đoán được đó là gì không?
Bạn vừa apply Template pattern rồi đó!
Chỉ cần như thế mà không phải nghĩ gì nhiều. Đó chính là lợi ích của nguyên tắc thiết kế object-oriented.
Bạn có thể không biết hết design pattern trên thế giới này, nhưng miễn là bạn biết nguyên tắc thiết kế object-oriented tốt, thì bạn không cần phải lo nghĩ gì nhiều.
Tất nhiên, nếu bạn biết về Template pattern, bạn có thể nhìn thấy nó ngay lập tức. Đây chính xác sẽ là kịch bản hoàn hảo cho các template pattern khi bạn apply pattern phù hợp. Rõ ràng, chúng cũng sẽ làm cho cuộc sống của bạn dễ dàng hơn.

Nhiều khi bạn chẳng cần nó đâu~

Đôi khi chúng ta nhìn vào một đoạn code xấu xí, chúng ta sẽ lập tức nhận ra chỉ có pattern “cứu cánh” và làm cho nó tốt hơn.
Nhưng điều gì sẽ xảy ra nếu nó có tác dụng và bạn không cần phải thay đổi nó?
Hãy quay lại ứng dụng hiển thị bản nhạc lúc nãy, nhưng với một kịch bản hơi khác một chút. Hãy tưởng tượng rằng mình muốn tạo ra một class cho từng loại khóa của âm nhạc. App này sẽ hỗ trợ tất cả các khóa nhạc dựa trên 3 điều cơ bản: C, F và G.
Ta có một chức năng trong code mà sẽ trả về đúng các khóa của âm nhạc. Nó trông như thế này:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php

function getClef($note, $octave)
{
    switch ($note) {
        case 'C':
            switch ($octave) {
                case 1: return new C1Clef();
                case 2: return new C2Clef();
                case 3: return new C3Clef();
                case 4: return new C4Clef();
                case 5: return new C5Clef();
                default: throw new UnknownClefException();
            }
        case 'F':
            switch ($octave) {
                case 3: return new F3Clef();
                case 4: return new F4Clef();
                case 5: return new F5Clef();
                default: throw new UnknownClefException();
            }
        case 'G': 
            switch ($octave) {
                case 1: return new G1Clef();
                case 2: return new G2Clef();
                default: throw new UnknownClefException();
            }
        default: throw new UnknownClefException();
    }
}
Bạn đã thấy có vài code xấu xí và có thể là bạn đang cưc kỳ muốn chỉnh sửa cấu trúc của nó lại phải không? Có lẽ bạn đang nghĩ đến việc áp dụng Factory Method pattern vì nó phù hợp và chắc chắn làm cho code sạch hơn.
Nhưng mà nếu tôi nói với bạn rằng đoạn code này sẽ không bao giờ thay đổi thì sao nhỉ?
Tôi không phải là một nhạc sĩ, vì vậy tôi có thể sai, nhưng có vẻ không thực tế khi trong tương lai sẽ phát minh thêm các nốt nhạc mới (???) Nếu không bổ sung các nốt nhạc mới thì không việc gì phải thay đổi đoạn code này.
Không có  chuyện code “dỏm” vì nếu nó chạy tốt thì bạn không cần phải thay đổi nó làm gì, cấu trúc lại code (với một design pattern hoặc các kỹ thuật khác) chỉ làm phát sinh chi phí không cần thiết mà thôi.

Chi phí của việc áp dụng design pattern

Có rất nhiều design pattern khác nhau, nhưng hầu hết trong số đó đều có điểm chung: khi bạn apply, bạn cần phải đặt ra một số cấu trúc. Nói cách khác, bạn cần phải add class và (hoặc) tương tác code.
Trong ví dụ đầu tiên, cấu trúc này bao gồm một abstract class được mở rộng thêm bởi hai children class. Hơn nữa, để cho các code cũ sử dụng các class mới, bạn cũng cần phải thêm các update không liên quan trực tiếp đến các design pattern.

Làm sao bạn có thể biết khi nào dùng design pattern là tốt nhất?

Đó là câu hỏi triệu đô! Điều đó phụ thuộc vào 2 thứ: đoạn code đó sẽ thay đổi thế nào về sau, và mục đích của pattern là gì.

Khả năng thay đổi của những đoạn code.

Những ví dụ chúng ta đã xem chứng tỏ sự quan trọng của việc xem xét những thay đổi để đi đến quyết định có dùng design pattern hay không.
Trong ví dụ đầu, nó thật sự là cần thiết để dùng pattern. Bởi vì sau cùng chúng ta không thể dừng lại ở bản nhạc mà chỉ có nốt 1/2 và 1/4, đúng vậy không? Và do việc thêm nhiều loại nốt nhạc sẽ lặp lại nhiều code hơn, nên template pattern cần phải được áp dụng để tránh lặp code.
Trong ví dụ thứ 2, thì lại là điều hoàn toàn ngược lại. Vì chúng ta giả thiết một cách an toàn là không có thêm khóa và nốt nhạc, nên doạn code này không cần phải thay đổi, và vì vậy không cần sử dụng design pattern.

Mục đích của pattern

Sẽ là không đủ nếu chỉ biết một pattern được triển khai thế nào; chúng ta cũng cần biết lý do đằng sau sự tồn tại của pattern đó.
Nói một cách khác, chúng ta cần biết một pattern được tạo ra để làm gì. Chúng ta cần hiểu một cách toàn diện về loại bài toán mà nó giải quyết. Nếu chúng ta biết điều này, sẽ dễ dàng hơn cho chúng ta để biết khi nào hợp lý để dùng nó.
Để đưa ra quyết định dùng design pattern hay không, có 2 cuốn sách tôi muốn giới thiệu:
  • Head First Design Patterns: Nếu bạn chưa nghe về design patterns hoặc mới bắt đầu, đây là cuốn sách bạn phải đọc. Nó giải thích các design pattern được sử dụng nhiều nhất theo cách rất dễ hiểu.
  • Design Patterns: Elements of Reusable Object-Oriented Software: Đây là một cuốn kinh điển và cũng là cuốn phải đọc. Nó được viết như là danh mục các design pattern hay dùng, được tổ chức theo 3 nhóm chính: creational, structural, and behavioral.
Thật không may, có một vấn đề nữa mà chúng ta thường gặp phải là nhận biết và áp dụng pattern. Khi bạn viết code từ đầu, sẽ dễ dàng để nhận ra sự cần thiết của 1 pattern. Tuy nhiên, khi bạn viết tiếp một đống code cũ, sẽ khó hơn để sử dụng design pattern.
Bạn cần phải chắc chắn bạn hiểu đầy đủ về đoạn code trước khi thay đổi. Đây sẽ là cách dễ nhất hoặc là kinh nghiệm đau đớn nhất tùy thuộc vào đoạn code viết mạch lạc hay rối rắm ra làm sao.
May mắn cho chúng ta, có một cuốn sách nữa có thể giúp xử lý vấn đề này: Refactoring to Patterns. Cuốn này sẽ cho bạn biết làm thế nào để sử dụng code có sẵn và tích hợp design patterns vào nó.
Những cuốn sách trên và nhiều cuốn khác nữa được giới thiệu trong các bài viết của John mà tôi rất muốn giới thiệu đến bạn.

Kết

Conclusion-768x432
Design patterns không phải là mục tiêu tối thượng của việc lập trình. Thực tế, tôi không nghĩ có điều đó. Chúng đơn giản là một công cụ mà có thể làm cho code của chúng ta gọn gàng hơn và dễ hiểu cũng như bảo trì khi được ứng dụng đúng.
Vậy thì bạn nghĩ khi nào nên dùng design pattern, và khi nào thì không? Hãy cho chúng tôi biết ý kiến của bạn. Comment bên dưới!
(simpleprogrammer.com)

No comments:

Post a Comment

The Ultimate XP Project

  (Bài chia sẻ của tác giả  Ryo Amano ) Trong  bài viết  số này, tôi muốn viết về dự án phát triển phần mềm có áp dụng nguyên tắc phát triển...