Bạn đã từng nghe nói tới Design Pattern chưa? Đã bao giờ đi phỏng vấn mà gặp phải câu hỏi liên quan tới Design Pattern? Mình thì lần nào đi xin việc cũng gặp phải câu hỏi này luôn 😵
Trong bài viết này, chúng ta sẽ cùng nhau tìm hiểu mọi thứ về Design Pattern: định nghĩa, bối cảnh sử dụng design pattern… Tất nhiên là với mỗi design pattern, mình sẽ có ví dụ bằng code minh họa.
Nội dung bài viết gồm có:
- Design Pattern là gì?
- Tại sao nên sử dụng Design Pattern.
- Phân loại Design Pattern
- Cách triển khai các Design Pattern bằng code Java
Nội dung chính của bài viết
Design Pattern là gì
Design Pattern là một kỹ thuật trong lập trình hướng đối tượng. Nó cung cấp cho bạn các “mẫu thiết kế” để giải quyết một vấn đề hay gặp trong thiết kế phần mềm. Các vấn đề mà bạn gặp phải trong quá trình thiết kế phần mềm, có thể bạn đã có giải pháp giải quyết. Tuy nhiên, giải pháp của bạn có thể chưa tối ưu, hoặc nó chưa được trừa tượng hóa để bạn có thể tái sử dụng sau này.
Design Pattern giúp bạn giải quyết vấn đề một cách tối ưu nhất, giúp thiết kế phần mềm của chúng ta linh hoạt, dễ dàng thay đổi và bảo trì hơn.
Tại sao phải sử dụng Design Pattern?
Mỗi lập trình viên sẽ tự có câu trả lời riêng khi nói đến lý do sử dụng design pattern trong mã nguồn của họ. Tuy nhiên, có thể gói gọn lại bằng 2 lý do chính:
- Cung cấp một thuật ngữ tiêu chuẩn để mọi người đều hiểu khi đọc mã nguồn.
- Tránh những sai lầm bị lặp đi lặp lại. Dự án có nhiều người làm, có thể nhiều người đều cùng gặp một vấn đề giống nhau nhưng lại có các cách giải quyết khác nhau. Design Pattern sẽ thống nhất phương án giải quyết và nó là cách giải quyết tối ưu nhất.
Yêu cầu trình độ trước khi học Design Pattern
Design Pattern không dành cho người mới học lập trình. Để có thể ứng dụng các design pattern, bạn cần phải có kinh nghiệm lập trình nhất định.
Muốn tìm hiểu và học được Design Pattern, bạn cần phải hiểu và nắm chắc về lập trình hướng đối tượng (OOP). Đặc biệt là Abstract class, Interface, và static.
Bạn nào mới học lập trình mà muốn nhảy vào tìm hiểu design pattern ngay thì cũng đừng buồn nhé. Đơn giản là bạn nên đọc lại các bài viết về OOP mà mình đã chia sẻ trên VNTALKING nhé, sau đó quay lại bài này.
Phân loại Design Pattern
Cuốn sách Design Patterns – Elements of Reusable Object-Oriented Software được coi là khởi nguồn của khái niệm Design Pattern trong thiết kế phần mềm. Theo quan điểm của các tác giả cuốn sách, design pattern chủ yếu được dựa theo những nguyên tắc của lập trình hướng đối tượng.
Do vậy, chúng ta sẽ dựa vào các yếu tố của OOP để phân loại design pattern. Đến thời điểm hiện tại, có tổng cộng 32 loại design pattern và được chia làm 3 dạng chính.
- Creational Patterns: Các pattern giúp giải quyết các vấn đề liên quan tới khởi tạo đối tượng.
- Structural Patterns: Gồm các patterns giải quyết cho việc xử lý các thành phần của đối tượng. Nó trả lời cho các câu hỏi như: Class đó chứa gì bên trong? Mối liên hệ giữa các class là gì?
- Behavioral Patterns: Gồm các pattern giải quyết các vấn đề liên quan tới hành vi của các đối tượng, chính xác hơn là sự tương tác giữa các đối tượng.
Chi tiết phân loại từng design pattern như hình bên dưới đây:
5 design pattern mà bạn cần phải biết
Hiện tại có rất nhiều design patterns, do vậy để bạn có thể nắm vững được hết tất cả là một thử thách khó khăn. Thông thường, trong quá trình làm việc, khi có vấn đề phát sinh, bạn đi tìm giải pháp. Bạn nên tìm xem đã có design pattern nào giải quyết vấn đề của mình rồi hay chưa? Khi đó, bạn sẽ dần dần vừa thực hành vừa tìm hiểu design pattern đó.
Tuy nhiên, để có kiến thức nền tảng thì dưới đây là 5 design pattern mà bạn nên tìm hiểu trước.
1. Singleton Pattern
Singleton pattern có lẽ là pattern phổ biến nhất và cũng dễ dùng nhất. Đúng như tên gọi, nó chỉ cho phép tạo duy nhất một instance của đối tượng ở bất kỳ thời điểm nào.
Ví dụ ở thế giới thực đó chính là vị trị tổng thống, mỗi quốc gia chỉ cho phép duy nhất 1 người làm tổng thống mà thôi.
Có một vài điều cần lưu ý khi tạo singleton class:
- Hàm constructor cần phải đặt
private
. Mục đích để ngăn các đối tượng khác tạo instance của singleton class qua từ khóa NEW. - Nhược điểm chính của Singleton pattern đó chính khó thực hiện Unit Test. Do đấy, hãy cân nhắc kỹ nơi nào cần dùng đến singleton pattern, nơi nào không.
- Một số Java framework như Spring, các đối tượng được quản lý (gọi là bean), mặc định chúng là singleton.
- Trong JEE 7, bạn có thể sử dụng annotation
@Singleton
để tạo Singleton class.
Ví dụ cách triển khai Singleton pattern.
class Singleton { private static Singleton instance; private Singleton() { } public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } } public class ClassSingleton { public static void main(String[] args) { System.out.println("--- Singleton Pattern ---"); Singleton single1 = Singleton.getInstance(); Singleton single2 = Singleton.getInstance(); if (single1.equals(single2)) { System.out.println("Unique Instance"); } } }
2. Prototype Pattern
Với pattern này, để bạn dễ hiểu hơn, mình sẽ lấy một ví dụ như sau:
Giả sử bạn đang thiết kế game cờ vua. Tất cả các lần bắt đầu game đều có chung một thiết lập ban đầu (màu sắc, vị trí trên bàn cờ…) cho quân vua, quân hậu, quân mã… Cũng như bạn phải tạo layout bàn cờ mỗi khi mở game.
Thay vì phải lặp đi lặp lại việc tạo bàn cờ mỗi lần, chúng ta có thể:
- Tạo riêng một đối tượng chứa tất cả các thiết lập ban đầu.
- Clone đối tượng đó mỗi khi game bắt đầu.
Đối tượng mà chứa các thiết lập ban đầu được gọi là prototype, và cách thiết kế code kiểu này gọi là prototype pattern.
Tóm lại: Về bản chất, prototype pattern là thiết kế cho phép khởi tạo một đối tượng bằng cách sao chép từ một đối tượng khác đã tồn tại thay vì sử dụng toán tử new.
Đối tượng mới là một bản sao có thể giống hoàn toàn hoặc được biến đổi một vài thuộc tính so với đối tượng gốc. Đặc biệt, bạn có thể thoải mái thay đổi dữ liệu trên đối tượng bản sao mà không làm ảnh hưởng đến đối tượng gốc. Nếu bạn chỉ copy đối tượng theo cách thông thường thì với ngôn ngữ lập trình như Java chẳng hạn, khi bạn thay đổi dữ liệu ở đối tượng bản sao thì bản gốc cũng bị thay đổi theo. Vì bạn mới chỉ copy địa chỉ tham chiếu mà thôi.
Ví dụ cách triển khai Singleton pattern.
//Computer.java public class Computer implements Cloneable { private String os; private String office; private String antivirus; private String browser; private String others; public Computer(String os, String office, String antivirus, String browser, String other) { super(); this.os = os; this.office = office; this.antivirus = antivirus; this.browser = browser; this.others = other; } @Override protected Computer clone() { try { return (Computer) super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return null; } @Override public String toString() { return "Computer [os=" + os + ", office=" + office + ", antivirus=" + antivirus + ", browser=" + browser + ", others=" + others + "]"; } public void setOthers(String others) { this.others = others; } } // App.java public class App { public static void main(String[] args) { Computer computer1 = new Computer("Window 10", "Word 2013", "BKAV", "Chrome v69", "Skype"); Computer computer2 = computer1.clone(); computer2.setOthers("Skype, Teamviewer, FileZilla Client"); System.out.println(computer1); System.out.println(computer2); } }
3. Builder Pattern
Chúng ta cùng xem xét ví dụ thực tế sau đây.
Giả sử bạn đi ăn tối với bạn gái tại một nhà hàng “sang chảnh”. Nhân viên bồi bàn đưa cho bạn menu với rất nhiều món ăn hấp dẫn, nào là món khai vị, món chính và món tráng miệng. Bạn có thể tùy chọn ăn đủ cả 3 loại món trên,hoặc chỉ 2 trong 3 loại món. Ví dụ, bạn thích ăn luôn món chính rồi tráng miệng sau. Còn người yêu bạn lại chọn ăn từ từ, ăn khai vị cho sang, rồi mới ăn món chính và kết thúc là tráng miệng.
Trong thiết kế phần mềm cũng có nhiều tình huống tương tự như thế xảy ra. Bạn có thể xây dựng một đối tượng bằng cách tập hợp từ các tùy chọn có sẵn. Hoặc bạn cần tạo đối tượng theo nhiều cách khác nhau. Đây là lúc Builder pattern được áp dụng.
Với bạn nào từng lập trình Android sẽ gặp rất nhiều Builder pattern, điển hình nhất chính là lúc tạo dialog trong Android.
Ví dụ cách triển khai Builder Pattern:
public class BuilderPattern { static class Coffee { private Coffee(Builder builder) { this.type = builder.type; this.sugar = builder.sugar; this.milk = builder.milk; this.size = builder.size; } private String type; private boolean sugar; private boolean milk; private String size; public static class Builder { private String type; private boolean sugar; private boolean milk; private String size; public Builder(String type) { this.type = type; } public Builder sugar(boolean value) { this.sugar = value; return this; } public Builder milk(boolean value) { this.milk = value; return this; } public Builder size(String value) { this.size = value; return this; } public Coffee build() { return new Coffee(this); } } @Override public String toString() { return String.format("Coffee [type=%s, sugar=%s, milk=%s, size=%s]", this.type, sugar, milk, size); } } public static void main(String[] args) { Coffee coffee = new BuilderPattern.Coffee.Builder("Mocha").milk(true).sugar(false).size("Large").build(); System.out.println(coffee); } }
Những ưu điểm của Builder Pattern là:
- Đơn giản hóa việc tạo đối tượng
- Mã nguồn dễ đọc hơn rất nhiều
- Không cho phép sửa đổi các giá trị.
4. Proxy Pattern
Trong đời sống thực, bạn cũng gặp rất nhiều vấn đề liên quan tới proxy. Ví dụ như: Thẻ ATM là một proxy cho tài khoản ngân hàng của bạn. Bất cứ khi nào bạn thực hiện giao dịch bằng thẻ ATM, số tiền tương ứng sẽ bị trừ trong tài khoản ngân hàng.
Tương tự trong lập trình phần mềm cũng vậy. Bạn sẽ gặp phải tình huống cần tương tác với đối tượng từ xa (remote). Trong tình huống như vậy, bạn tạo một đối tượng proxy, với mục đích để tương tác với tất cả các nguồn từ bên ngoài. Thay vì bạn phải làm việc trực tiếp với đối tượng ở xa thì bạn làm việc với đối tượng proxy (đã được định nghĩa lại cho phù hợp mục đích sử dụng).
Proxy sẽ che giấu sự phức tạp liên quan tới việc giao tiếp với đối tượng thực.
Mã nguồn ví dụ:
//Image.java public interface Image { void showImage(); } //RealImage.java public class RealImage implements Image { private String url; public RealImage(String url) { this.url = url; System.out.println("Image loaded: " + this.url); } @Override public void showImage() { System.out.println("Image showed: " + this.url); } } //ProxyImage.java public class ProxyImage implements Image { private Image realImage; private String url; public ProxyImage(String url) { this.url = url; System.out.println("Image unloaded: " + this.url); } @Override public void showImage() { if (realImage == null) { realImage = new RealImage(this.url); } else { System.out.println("Image already existed: " + this.url); } realImage.showImage(); } } //Client.java public class Client { public static void main(String[] args) { System.out.println("Init proxy image: "); ProxyImage proxyImage = new ProxyImage("https://vntalking.com/favicon.ico"); System.out.println("---"); System.out.println("Call real service 1st: "); proxyImage.showImage(); System.out.println("---"); System.out.println("Call real service 2nd: "); proxyImage.showImage(); } }
Kết quả chạy thử chương trình.
Init proxy image: Image unloaded: https:// vntalking.com/favicon.ico --- Call real service 1st: Image loaded: https:// vntalking.com/favicon.ico Image showed: https:// vntalking.com/favicon.ico --- Call real service 2nd: Image already existed: https:// vntalking.com/favicon.ico Image showed: https:// vntalking.com/favicon.ico
Như bạn thấy image chỉ thật sử được load lên khi proxy class gọi hàm.
5. Observer Pattern
Có rất nhiều ví dụ trong thực tế cần sử dụng tới Observer pattern. Đơn giản như việc đăng ký theo dõi (subcriber) một kênh Youtube. Bất cứ khi nào kênh Youtube đó có video mới thì bạn sẽ nhận được thông báo.
Có hai thành phần chính khi implement một Observer design pattern:
- Registration: Nơi các đối tượng quan tâm đăng ký để nhận thông báo.
- Notification: Nơi các đối tượng quan tâm nhận thông báo.
Bạn sẽ hiểu hơn khi đọc đoạn mã ví dụ sau:
public class Observer Pattern { static class VntalkingNotifier { List < VntalkingFan > fans = new ArrayList < VntalkingFan > (); void register(VntalkingFan fan) { fans.add(fan); } void baiVietMoi() { for (SachinFan fan: fans) { fan.announce(); } } } static class VntalkingFan { private String name; VntalkingFan(String name) { this.name = name; } void announce() { System.out.println(name + " notified"); } } public static void main(String[] args) { VntalkingNotifier notifier = new VntalkingNotifier(); notifier.register(new VntalkingFan("Tìm hiểu Design pattern")); notifier.register(new VntalkingFan("Có nên học javascript lúc này?")); notifier.register(new VntalkingFan("Lập trình Java cơ bản")); notifier.baiVietMoi(); } }
Tạm kết
Như vậy là chúng ta đã có cái nhìn tổng quan về Design Pattern. Mỗi một design pattern là một cách tiếp cận để giải quyết vấn đề trong một bối cảnh nhất định.
Bạn có thể tham khảo thêm video bên dưới đây về Design Pattern.
Mình hi vọng qua bài viết các bạn sẽ hiểu hơn về design pattern. Nếu có thắc mắc cứ để lại bình luận bên dưới nhé.
Mình mới thạo mỗi Singleton patterns, haizz.