12/08/2018, 14:26

Design pattern in OOP [Part 1]

Chào mọi người, nắm vững các nguyên tắc trong lập trình hướng đối tượng là điều kiện cần để một lập trình viên tạo ra những ứng dụng chất lượng, thế nhưng, muốn những dòng code mình viết ra sạch, đẹp và hiệu quả, thì điều kiện đủ là cần phải nắm vứng các nguyên lý, các mẫu thiết kế hướng đối tượng ...

Chào mọi người, nắm vững các nguyên tắc trong lập trình hướng đối tượng là điều kiện cần để một lập trình viên tạo ra những ứng dụng chất lượng, thế nhưng, muốn những dòng code mình viết ra sạch, đẹp và hiệu quả, thì điều kiện đủ là cần phải nắm vứng các nguyên lý, các mẫu thiết kế hướng đối tượng trong lập trình. Hôm nay, tôi sẽ giới thiệu đến các bạn 3 Design pattern kinh điển mà theo tôi nghĩ, bất kỳ một lập trình viên nào cũng cần biết và áp dụng chúng một cách đúng đắn.

Singleton pattern

Đây là một Design pattern phổ biến và quan trọng hàng đầu mà bất kỳ cuộc interview nào, nhà tuyển dụng cũng thường hỏi về các vấn đề liên quan đến nó. Ngay cái tên cũng gợi lên cho chúng ta đôi chút về mẫu thiết kế này - sự duy nhất. Theo kinh nghiệm của bản thân, phần lớn các lập trình viên chưa có nhiều kinh nghiệm đều biết hoặc đã áp dụng design pattern này, tuy nhiên để áp dụng trong những trường hợp các nhau, ưu nhược điểm hay best practise thì không phải ai cũng nắm rõ.

Singleton Pattern sẽ tạo ra một đối tượng duy nhất và tồn tại trong suốt vòng đời của ứng dụng, nói cách khác, nó chỉ cho phép khởi tạo 1 lần duy nhất. Bởi đặc tính duy nhất của nó, trong một số trường hợp cụ thể, ví dụ như tạo một cầu nối liên kết với database, tạo một lớp controller quản lý và lưu trữ đảm bảo tính duy nhất, hoặc tạo một lớp để cache object ... nó được triển khai vô cùng hiệu quả. Sau đây sẽ là các cách implement pattern này.

Eager initialization

Trong cách triển khai này, đối tượng của lớp được tạo ra ngay khi class được load vào JVM, ví dụ như khi ta gọi một static method hoặc assign giá trị cho một biến static trong class ... Cách này rất đơn giản, tuy nhiên rõ ràng nó có thể được khởi tạo ngay khi chúng ta chưa cần sử dụng đến, và có thể gây lãng phí bộ nhớ.

public class Singleton {

	private static final Singleton instance = new Singleton();

	private Singleton(){}

	public static Singleton getInstance(){
		return instance;
	}

}

Rõ ràng với cách trên, nếu xảy ra lỗi trong quá trình khởi tạo đối tượng, ta không thể xử lý được. Một solution khác là sử dụng static block trong Java và try/catch đoạn khởi tạo.

public class Singleton {
	private static Singleton instance;
	static {
		try {
			instance = new Singleton();
		} catch (Exception e) {
			System.out.print(e);
		}
	}

	private Singleton() {
	}

	public static Singleton getInstance() {
		return instance;
	}
}

Lazy Initialization

Đây là cách triển khai pattern với mục đích chỉ khởi tạo khi cần dùng đến và là cách thường được sử dụng nhất nếu không quan tâm đến multi threading.

public class Singleton {
	private static Singleton instance;
	private Singleton() {
	}
	public static Singleton getInstance() {
		if (instance == null) {
			instance = new Singleton();
		}
		return instance;
	}
}

Tuy nhiên, như đã đề cập ở trên, trong trường hợp nhiều thread khác nhau cùng truy cập đến hàm getInstance(), rõ ràng ở lần khởi tạo đầu tiên, giả sử có nhiều thread cùng truy cập một thời điểm giống nhau, rõ ràng sẽ gây ra sai lệch, và đối tượng sẽ được khởi tạo nhiều hơn một lần. Để tránh khả năng này xảy ra, ta phải thực hiện lock method này lại và chỉ cho một thread truy cập tại một thời điểm, trong Java ta sử dụng từ khóa synchronized. Method getInstance() sẽ được implement lại như sau:

public static synchronized Singleton getInstance() {
		if (instance == null) {
			instance = new Singleton();
		}
		return instance;
	}

Có một lưu ý ở đây là khi đặt từ khóa synchronized ở đầu method, nó đều lock method này bất kỳ khi nào nó được gọi, điều khó gây ảnh hưởng đến performance của ứng dụng. Để giảm thiểu tình trạng đó, ta sẽ modify một chút method này như sau.

public static Singleton getInstance() {
		if (instance == null) {
			synchronized (Singleton.class) {
				if (instance == null) {
					instance = new Singleton();
				}
			}
		}
		return instance;
	}

Bên trên là một số cách phổ biến để implement Singleton pattern, ngoài ra còn một vài cách khác để thực hiện pattern này như sử dụng một helper static nested class hay dùng enum ... dành cho các bạn muốn tìm hiểu thêm.

Builder Pattern

Đây là một pattern cơ bản và không kém phần quan trọng trong lập trình, cũng là một loại Creational pattern, giúp chúng ta khởi tạo các đối tượng clean hơn rất nhiều. Khi tạo một class, bạn không nên khởi tạo hàm dựng với quá nhiều tham số, điều đó là bad solution, vì các lập trình viên không thể biết các tham số nào cần thiết để truyền vào cho việc khởi tạo hàm dựng, mặc khác, hàm dựng với quá nhiều tham số sẽ gây ra rất nhiều confuse trong khi code, nhất là vấn đề ghi nhớ những thuộc tính nào đã được truyền vào khi khởi tạo đối tượng, người lập trình viên phải mất công sức để ghi nhớ hoặc dò tìm, điều đó là cần tránh khỏi. Builder pattern ra đời giúp cho việc khởi tạo một đối tượng clean hơn, dễ đọc hơn, có thể validate ngay khi tạo đối tượng. Một khả năng nữa của pattern này là việc tránh phải tạo quá nhiều hàm dựng, việc đó là cực kỳ nguy hiểm trong việc bảo đảm tính đúng đắn của class được tạo ra. Phần sau đây, tôi sẽ thử so sánh với cách truyền thống và khi sử dụng Builder pattern.

Cách thức truyền thống

Giả sử tôi có class User chứa các thông tin về người dùng, tôi sẽ implement như sau:

public class User {

	private int id;
	private String name;
	private String email;
	private Date birthday;
	private int weight;
	private int height;
	private int groupId;
	private int companyId;

	public int getId() {
		return id;
	}
	public void setId(int id) {
		this.id = id;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
...
}

Một User bắt buộc phải có id, name và email, do đó tôi sẽ tạo hàm dựng chứa 3 tham số này.

public User(int id, String name, String email){
		this.id = id;
		this.name = name;
		this.email = email;
	}

Tuy nhiên, trong một vài trường hợp, User cần phải có weight và height, vậy là tôi phải tạo thêm 1 hàm dựng khác chứa thêm 2 tham số này. Chưa đủ, một vài chỗ tôi lại cần cả groupId và companyId, vậy tôi phải tạo thêm hàm dựng chứa đầy đủ các tham số. Vậy là class User có tới 3 hàm dựng và việc này gây khó khăn cho nhưng developer khác trong việc tìm hàm dựng nào cho thích hợp và đảm bảo tính đúng đắn của class. Buider pattern sinh ra để giải quyết vấn đề này.

Builder pattern

public class User {

	private Builder builder;

	public User(Builder builder){
		this.builder = builder;
	}

	public static class Builder {
		private int id;
		private String name;
		private String email;
		private Date birthday;
		private int weight;
		private int height;
		private int groupId;
		private int companyId;

		public Builder setId(int id) {
			this.id = id;
			return this;
		}

		public Builder setName(String name) {
			this.name = name;
			return this;
		}

		public Builder setEmail(String email) {
			this.email = email;
			return this;
		}

		public Builder setBirthday(Date birthday) {
			this.birthday = birthday;
			return this;
		}
		public Builder setWeight(int weight) {
			this.weight = weight;
			return this;
		}

		public Builder setHeight(int height) {
			this.height = height;
			return this;
		}

		public Builder setGroupId(int groupId) {
			this.groupId = groupId;
			return this;
		}

		public Builder setCompanyId(int companyId) {
			this.companyId = companyId;
			return this;
		}

		public User build(){
			validate();
			return new User(this);
		}

		private void validate(){}

	}

}

Trong cách triển khai trên, bạn tạo một instance của User thông qua một Builder và tùy chọn các attribute cần thiết trong Builder này và đảm bảo tính đúng đắn của class bằng method validate().

User sẽ được khởi tạo như sau:

public static void main(String[] args) {
		User user = new User.Builder().setId(1)
				.setEmail("hieupham@gmail.com")
				.setName("Hieu Pham")
				.setBirthday(Calendar.getInstance().getTime())
				.setHeight(176)
				.setWeight(60).build();
	}

Rõ ràng, Builder pattern sẽ giúp việc khởi tạo object của bạn dễ dàng, đẹp và clean hơn rất nhiều.

Kết luận

Ở bài viết này, tôi giới thiệu 2 pattern cơ bản trong OOP Design, hy vọng các bạn sẽ áp dụng nó đúng đắn và hiệu quả trong công việc và học tập. Hẹn gặp lại ở các phần tiếp theo.

0