Trong bài trước, mình đã tổng hợp lại các design pattern cơ bản nhất. Bài này sẽ tiếp tục với các creational pattern.
I. Creational Pattern
1. Chức năng
- Khởi tạo object
- Cung cấp 1 cơ chế đơn giản, chính quy và có thể kiểm soát được việc khởi tạo object
- Đảm bảo tính bao đóng về các chi tiết trong việc class nào được khởi tạo và các instances này được khởi tạo ra sao
- Khuyến khích sử dụng interface để hạn chế coupling
2. Các pattern chính
- Factory Method
- Singleton
- Abstract Factory
- Prototype
- Builder
II. Các Creational Pattern
1. Factory Method
Nguyên nhân sử dụng
Trong quá trình khởi tạo object, việc chọn kiểu đúng từ cây phân cấp của các class không phải lúc nào cũng xác định chính xác được. Việc này đôi khi phải phụ thuộc vào nhiều yếu tố như:
- Trạng thái mà ứng dụng đang chạy
- Cấu hình của ứng dụng
- Sự mở rộng các yêu cầu hay nâng cấp, cải tiến
- …
Các yếu tố khác nhau sẽ yêu cầu khởi tạo các object phù hợp. Thông thường, mỗi client sẽ phải lựa chọn 1 class chính xác từ cây phân cấp để sử dụng dịch vụ.
Việc lựa chọn như trên sẽ dẫn tới 1 số hạn chế:
- Tất cả các client object đều phải triển khai các tiêu chí lựa chọn class dẫn tới tăng tính coupling giữa client và service provider.
- Khi mà thay đổi các tiêu chí lựa chọn class thì tất cả các client object đều cần phải có các thay đổi tương ứng
- Bởi vì các tiêu chí lựa chọn class sẽ cần tất cả các yếu tố có thể ảnh hưởng tới quá trình lựa chọn class, việc triển khai ở phía client sẽ có thể có những sai sót trong các câu lệnh điều kiện
- Nếu các class trong cây phân cấp có các điều kiện khởi tạo khác nhau, việc triển khai ở phía client sẽ rất phức tạp
- Client cần biết tất cả các class và chức năng của các class trong cây phân cấp
Để giải quyết các yếu tố này, việc sử dụng Factory Method sẽ giúp bao đóng các chức năng cần thiết trong việc lựa chọn và khởi tạo object. Chức năng của method này là:
- Lựa chọn class thích hợp từ cây phân cấp dựa theo context của ứng dụng và các yếu tố ảnh hưởng khác
- Khởi tạo object tương ứng trả về với kiểu của lớp cha
Cách làm này sẽ có các ưu điểm như:
- Client object sẽ sử dụng factory method để khởi tạo các instance tương ứng mà không cần xử lý với rất nhiều các tiêu chí lựa chọn
- Factory method khởi tạo các object khác nhau với các điều kiện khác nhau và client object sẽ không cần phải quan tâm tới các vấn đề phức tạp này
- Client object không cần phải biết tất cả các class trong cây phân cấp khi factory method đã làm phần việc đó và trả về object thích hợp
Có 2 hướng để triển khai Factory Method
- Triển khai 1 interface/ abstract class với factory method. Các class kế thừa từ đây sẽ tự triển khai logic khởi tạo object
- Triển khai một factory method mặc định. Các class con khác nhau nếu cần thiết sẽ tự override lại để triển khai logic riêng
Ví dụ
Ở đây, mình chọn một ví dụ đơn giản là Logger. Chương trình sẽ có 1 setting trong việc sử dụng file log hay console log. Tuỳ theo cấu hình này mà thực hiện việc ghi log.
publicinterfaceLogger{publicvoidlog(String message);}
publicclassFileLoggerimplementsLogger{@Overridepublicvoidlog(String message){System.out.println("Log message to log.txt");//file log logic}}
publicclassConsoleLoggerimplementsLogger{@Overridepublicvoidlog(String message){System.out.println("log message to console");//log message logic}}
The setting store in the properties file của ứng dụng
FileLogging = 0
Triển khai factory method
publicclassLoggerFactory{publicLoggercreateLogger(){if(isFileLoggingEnable()){returnnewFileLogger();}else{returnnewConsoleLogger();}}privatebooleanisFileLoggingEnable(){//check the FileLogging properties in application setting}}
2. Singleton
Nguyên nhấn sử dụng
Đôi khi, có những class chúng ta cần 1 và chỉ 1 instance của nó trong suốt vòng đời của ứng dụng. Ví dụ, với object kết nối tới cơ sở dữ liệu của ứng dụng chỉ cần 1 và chỉ 1 để đảm bảo hiệu quả nhất. Do đó, Singleton pattern được sử dụng để đảm bảo rằng việc khởi tạo duy nhất 1 instance của class:
- Có quyền truy cập công khai tới đối tượng này để tất cả các object trong chương trình đều có thể sử dụng
- Ngăn không cho các object khác có thể khởi tạo singleton object
Do đó, để khởi tạo 1 Singleton class ta cần
- Một private constuctor để đảm bảo việc khởi tạo object này chỉ được thực hiện bởi chính class
- Một static public access:
- public để tất cả object có thể truy cập
- static đảm bảo rằng các object có thể sử dụng object này mà không cần khởi tạo
Ví dụ
publicclassFileLoggerimplementsLogger{privatestaticFileLogger logger;privateFileLogger(){}@Overridepublicsynchronizedvoidlog(String message){System.out.println("Log message to log.txt");//file log logic}publicstaticLoggergetLogger(){if(logger ==null){
logger =newFileLogger();}return logger;}}
3. Abstract Factory
Tương tự Factory Pattern, Abstract Factory cũng có những đặc điểm như:
- Có một cây phân cấp class tạo bởi các class con với cùng 1 class cha
- Được dùng khi client muốn khởi tạo một object kiểu của class cha nhưng không biết (hoặc k cần biết) chính xác class con nào được khởi tạo.
- Che dấu các cơ chế bên trong trong việc khởi tạo object khỏi client
Tuy nhiên, với Abstract Factory, các khái niệm này sẽ có 1 chút nâng cấp. Abstract Factory cung cấp 1 interface để khởi tạo 1 họ các object.
Nguyên nhân sử dụng
Abstract factory thường được sử dụng trong trường hợp client object muốn khởi tạo một trong một nhóm các class liên quan tới nhau mà không cần biết chính xác class nào cần khởi tạo. Việc sử dụng interface này giúp hạn chế việc lặp lại các interface trong khởi tạo instance. Các factory cụ thể sẽ triển khai từ interface này và khởi tạo các object theo logic của riêng mình. Client sẽ sử dụng các factory class này mà không cần quan tâm chính xác class nào sẽ được khởi tạo.
Việc sử dụng abstract factory thường có:
- Một họ hay một nhóm các class liên quan, phụ thuộc nhau
- Cần 1 nhóm các factory class triển khai interface mà abstract factory cung cấp.
- Kiểm soát hay cung cấp truy cập tới 1 nhóm các class liên quan, phụ thuộc
- Việc triển khai interface sẽ theo logic cụ thể của nhóm class mà nó kiểm soát
Ví dụ
Abstract Factory thường được sử dụng cho các thư viện hay framework. Ví dụ dễ thấy nhất là về hệ thống JDBC driver. Mỗi driver sẽ chứa các class kế thừa các interface Connection, Statement và ResultSet. Một tập class của các driver khác nhau sẽ khởi tạo các class khác nhau. Tập class của Oracle JDBC driver hiển nhiên sẽ khác tập class chứa trong DB2 JDBC driver.
4. Prototype
Nguyên nhân sử dụng
- Khởi tạo 1 hoặc 1 loạt các object giống nhau hoặc chỉ khác nhau ở trạng thái
- Khởi tạo từ đầu từng object sẽ tốn thời gian và cần nhiều quá trình
Cách sử dụng prototype pattern:
- Khởi tạo 1 object như một object mẫu
- Tạo các object khác thông qua copy object mẫu và thực hiện các thay đổi cần thiết
Thông thường trong java, các class đều kế thừa 1 hàm clone()
từ java.lang.Object
.
Shallow Copy và Deep Copy
Shallow Copy | Deep Copy |
---|---|
Các thuộc tính nguyên thuỷ được giữ nguyên | Các thuộc tính nguyên thuỷ được giữ nguyên |
Các object tầng trên cùng được sao chép | Các object tầng trên cùng được sao chép |
Các object tầng dưới chỉ được sao chép con trỏ | Các object tầng dưới được sao chép |
Ví dụ
- Shallow Copy
publicclassPeopleimplementsCloneable{privateint age;privateString name;privateCar car;publicPeople(int age,String name,Car car){this.age = age;this.name = name;this.car = car;}@OverrideprotectedObjectclone(){returnnewPeople(age, name, car);}}classCar{privateString description;publicCar(String description){this.description = description;}publicStringgetDescription(){return description;}}
- Deep Copy
publicclassPeopleimplementsCloneable{privateint age;privateString name;privateCar car;publicPeople(int age,String name,Car car){this.age = age;this.name = name;this.car = car;}@OverrideprotectedObjectclone(){returnnewPeople(age, name,newCar(car.getDescription());}}classCar{privateString description;publicCar(String description){this.description = description;}publicStringgetDescription(){return description;}}
5. Builder
Nguyên nhân sử dụng
Thông thường, khi khởi tạo object, việc khởi tạo sẽ do hàm constructor của class thực hiện. Đối với các class mà việc khởi tạo đơn giản và giống nhau về quá trình khởi tạo. Tuy nhiên, hướng tiếp cận này sẽ gặp khó khăn nếu quá trình khởi tạo object phức tạp, cần nhiều bước do:
- quá trình khởi tạo gắn với object => tăng kích thước của class, giảm tính modular
- Nếu cần thêm hay thay đổi về logic triển khai => phải sửa lại các đoạn code có sẵn
Do đó, đối với các class mà việc khởi tạo phức tạp, người ta sẽ hướng tới việc tách các quá trình khởi tạo object ra khỏi class sang một class tách biệt là builder. Việc sử dụng builder pattern có một số ưu điểm sau:
- giảm kích thước của object
- nếu cần thêm hay sửa logic triển khai có thể sửa hoặc thêm builder
- quá trình khởi tạo sẽ độc lập các thành phần của object với nhau giúp tăng khả năng kiểm soát trong quá trình khởi tạo object
Mô hình chung
Mô hình đơn giản nhất cho kiểu thiết kế này là:
Khi đó quá trình khởi tạo 1 object sẽ thực hiện đơn theo các bước
- client khởi tạo object builder
- client khởi tạo các thành phần của object (
creatComponentX()
) - client gọi tới
getObject()
để lấy object mong muốn
Tuy nhiên, hướng tiếp cận này vẫn có các hạn chế
- tất cả các client đều cần biết về logic khởi tạo object
- nếu logic khởi tạo thay đổi, tất cả các client sẽ phải thay đổi theo (coupling)
Do đó, một khái niệm mới được đưa ra để giải quyết vấn đề này là Director. Class này sẽ đảm nhận việc gọi tới các phương thức cần thiết để khởi tạo nên object. Các client khác nhau sẽ sử dụng Director để tạo object mong muốn và khi object đã khởi tạo xong chỉ cần gọi tới getObject()
của builder class để lấy được object mình cần. Quá trình khởi tạo sẽ như sau
- client khởi tạo object builder mình cần
- client khởi tạo object director với builder đã tạo
- client gọi tới hàm
build
của director - Director dựa theo builder sẽ gọi tới các hàm khởi tạo các thành phần của object
- clietn gọi tới
getObject()
để lấy được object mong muốn sau khi quá trình khởi tạo kết thúc
Ví dụ
Ở đây mình sẽ demo với một ví dụ đơn giản, chỉ thể hiện chức năng của builder nhưng chưa thể hiện hết ưu nhược điểm của nó.
Đầu tiên là các class dữ liệu
- Họ các sản phẩm loại A
publicclassProductA{@OverridepublicStringtoString(){returnthis.getClass().getSimpleName().toString();}}
publicclassProductA1extendsProductA{}
publicclassProductA2extendsProductA{}
- Họ các sản phẩm loại B
publicclassProductB{@OverridepublicStringtoString(){returnthis.getClass().getSimpleName().toString();}}
publicclassProductB1extendsProductB{}
publicclassProductB2extendsProductB{}
- Class
menu
sử dụng các sản phẩm khác nhau cho mỗi menu
publicclassMenu{privateProductA productA;privateProductB productB;//other componentspublicMenu(){}publicProductAgetProductA(){return productA;}publicvoidsetProductA(ProductA productA){this.productA = productA;}publicProductBgetProductB(){return productB;}publicvoidsetProductB(ProductB productB){this.productB = productB;}@OverridepublicStringtoString(){return"Menu{"+"productA="+ productA +", productB="+ productB +'}';}}
Tiếp theo là các class builder
publicinterfaceBuilder{voidaddProductA();voidaddProductB();MenugetMenu();voidgenerateData();}
publicclassMenuBuilderAimplementsBuilder{privateMenu menu =newMenu();@OverridepublicvoidaddProductA(){
menu.setProductA(newProductA1());}@OverridepublicvoidaddProductB(){
menu.setProductB(newProductB1());}@OverridepublicvoidgenerateData(){//generate necessary data of the object}@OverridepublicMenugetMenu(){return menu;}}
publicclassMenuBuilderBimplementsBuilder{privateMenu menu =newMenu();@OverridepublicvoidaddProductA(){
menu.setProductA(newProductA2());}@OverridepublicvoidaddProductB(){
menu.setProductB(newProductB2());}@OverridepublicvoidgenerateData(){//generate necessary data of the object}@OverridepublicMenugetMenu(){return menu;}}
Đối với trường hợp không sử dụng Direction, mọi việc khởi tạo với builder sẽ do client đảm nhận, do đó, ta có thể test đơn giản như sau
publicclassTest{publicstaticvoidmain(String[] args){Menu menu;//client create concrete builderMenuBuilderA menuBuilderA =newMenuBuilderA();//client init object's components
menuBuilderA.addProductA();
menuBuilderA.addProductB();
menuBuilderA.generateData();//client receive expected object
menu = menuBuilderA.getMenu();System.out.println(menu);}}
Trường hợp sử dụng Director, ta sẽ sử dụng thêm 1 class Director
đảm nhận phần việc khởi tạo các thành phần của object
publicclassDirector{privatefinalBuilder builder;publicDirector(Builder builder){this.builder = builder;}publicvoidbuild(){
builder.addProductA();
builder.addProductB();
builder.generateData();}}
Khi đó, công việc của client sẽ là khởi tạo director với builder tương ứng và gọi tới build()
mà không cần quá quan tâm đến cấu trúc, logic của builder. Ngoài ra, client có thể thay đổi object mong muốn bằng cách tạo mới một director mới thay vì phải thay đổi toàn bộ đoạn code khởi tạo các thành phần của object.
publicclassTest{publicstaticvoidmain(String[] args){Menu menu;//client create concrete builderMenuBuilderA menuBuilderA =newMenuBuilderA();//client init object's componentsDirector director =newDirector(menuBuilderA);
director.build();//client receive expected object
menu = menuBuilderA.getMenu();System.out.println(menu);}}
Nguồn: viblo.asia