Factory Pattern – Nhà máy của những đối tượng

3249

Bài viết được sự cho phép của tác giả Edward Thien Hoang

FACTORY PATTERN

Chào mọi người, hôm nay mình sẽ tiếp tục loạt bài về Design Pattern với một pattern mới: Factory Pattern. Pattern này sẽ giúp chúng ta có một hướng giải quyết đúng đắn khi đối mặt với việc phải dùng những lệnh if/else trong khối xử lý để tạo ra những đối tượng khác nhau dựa vào điều kiện ban đầu cho trước.

  Factory Function vs. Class
  Factory Method trong thực tiễn

KHI KHÔNG CÓ NHÀ MÁY

Chúng ta bắt đầu câu chuyện tại một công ty IT có tên AZ Company cùng với kiến trúc sư có tuổi nhưng chưa có tên A11. Lúc này A11 đang đảm nhận dự án xây dựng 1 hệ thống có tên là “Course Management System” (CMS) để quản lý các khóa học trực tuyến. Cùng xem A11 sẽ làm thế nào để xây dựng hệ thống này, anh ta am hiểu khá rõ về các khái niệm trong lập trình hướng đối tượng như kế thừa và đa hình, vậy là thiết kế ban đầu ra đời ngay sau đó mà không mất quá nhiều suy nghĩ.
Factory_1

public class CourseController {     public static String COURSE_JAVA = "Java";     public static String COURSE_DOT_NET = ".NET";     public void displayCourse(String inCourseName) {         AbstractCourse course = null;         if(inCourseName.equals(COURSE_JAVA)) {             course = new JavaCourse();         }         else if(inCourseName.equals(COURSE_DOT_NET)) {             course = new DotNetCourse();         }         if(course != null) {             course.display();         }         else {             System.out.println("Nothing is displayed");         }     } } public class FactoryTest {     public static void main(String[] args) {         CourseController controller = new CourseController();         controller.displayCourse(CourseController.COURSE_DOT_NET);     } }

Hệ thống của A11 được đưa vào vận hành và không gặp bất kỳ vấn đề gì và mang lại lợi nhuận khá lớn cho công ty từ những khóa học online. Lúc này sếp của A11 muốn mở thêm một khóa học C/C++. A11 bắt đầu suy nghĩ và anh ta định sẽ vào lớp Controller để thêm 1 dòng if dành cho COURSE_C_PLUS_PLUS. Nhưng ý định đó dừng lại khi anh nghĩ đến việc sếp sẽ mở thêm hàng chục khóa học online khác, và chi phí để anh thay đổi cả lớp CourseController để đáp ứng lại là quá lớn. A11 nghĩ đến nguyên lý SOLID principle OCP (Open Closed Principle) và thấy mình đã vi phạm nó. Một hệ thống luôn phải được thay đổi theo thời gian và phải có cách nào đó để có thể đáp ứng được những thay đổi mà không phải sửa code (thật ra là sửa 1 chút, nhưng sửa đúng chỗ cần sửa). Bạn không thể nào giải phẫu nguyên một chiếc xe chỉ để vá cái bánh xe.

BẮT ĐẦU XÂY DỰNG MỘT NHÀ MÁY ĐƠN GIẢN

Câu chuyện sẽ tiếp tục, do đã học được từ những nguyên lý về thiết kế hướng đối tượng, ngày hôm sau, A11 đã quyết định sẽ tách biệt những phần thay đổi ra khỏi những phần ít bị thay đổi. Cụ thể ở đây là những đoạn if/else để khởi tạo những khóa học sẽ được thay thế bằng 1 “nhà máy” để khởi tạo những đối tượng đó một cách độc lập từ bên ngoài, kết quả của việc khởi tạo sẽ được đưa vào để xử lý cho những đoạn code phía sau.

Nhà máy đơn giản của A11:
Factory_2

public class CourseFactory {
public static AbstractCourse createCourse(String inCourseName) {
AbstractCourse course = null;
if(inCourseName.equals(CourseController.COURSE_JAVA)) {
course = new JavaCourse();
}
else if(inCourseName.equals(CourseController.COURSE_DOT_NET)) {
course = new DotNetCourse();
}
return course;
}
}

public class CourseController {
public static String COURSE_JAVA = "Java";
public static String COURSE_DOT_NET = ".NET";
public void displayCourse(String inCourseName) {
AbstractCourse course = CourseFactory.createCourse(inCourseName);
if(course != null) {
course.display();
}
else {
System.out.println("Nothing is displayed");
}
}
}

Như đã thấy, phần mong manh dễ thay đổi nhất đã bị đưa ra một chỗ khác. A11 có thể tạm quên đi lớp CourseController mỗi khi có yêu cầu thêm mới 1 khóa học mới. Anh ta có thể ngủ ngon cho đến hết đêm nay và đêm mai nữa, ít ra là như vậy.

NHÀ MÁY BỊ QUÁ TẢI

Bây giờ công ty AZ đã thực sự lớn mạnh, bằng việc cung cấp các khóa học online, ngày càng có nhiều học viên đăng ký. Và trong số đó, có một số người đã không hài lòng với các khóa học online kiểu như vậy, họ muốn được học những khóa học offline, khi đó những câu hỏi sẽ được giải đáp nhanh hơn và hiệu quả hơn bằng cách tương tác trực tiếp với các giảng viên. Giám đốc công ty thấy được điều đó, đã mời thêm 1 số giáo sư về và bắt đầu cho học viên đăng ký những khóa học offline. Và tất nhiên A11 cũng có việc làm. Anh phải tiếp tục xây dựng những khóa học offline như vậy để thêm vào hệ thống.

Bây giờ hệ thống của A11 sẽ phải quản lý các khóa học như JavaOnlineCourse, DotNetOnlineCourse, CPlusPlusOnlineCourse, PHPOnlineCourse, JavaOfflineCourse, DotNetOfflineCourse, …

A11 sẽ thêm những khóa học Offline vào như hệ thống như thế nào? Anh suy nghĩ và câu trả lời chính là lớp CourseFactory. Anh sẽ thêm các điều kiện if/else vào để tạo ra những khóa học offline giống như việc anh làm với các khóa học online.

public class CourseFactory {
public static AbstractCourse createCourse(String inCourseName) {
AbstractCourse course = null;
if(inCourseName.equals(CourseController.COURSE_JAVA_ONLINE)) {
course = new JavaOnlineCourse();
}
else if(inCourseName.equals(CourseController.COURSE_DOT_NET_ONLINE)) {
course = new DotNetOnlineCourse();
}
else if(inCourseName.equals(CourseController.COURSE_JAVA_OFFLINE)) {
course = new JavaOfflineCourse();
}
else if(inCourseName.equals(CourseController.COURSE_DOT_NET_OFFLINE)) {
course = new DotNetOfflineCourse();
}
return course;
}
}

Hệ thống vẫn vận hành tốt. Mỗi khi có một khóa học online được mở ra, A11 sẽ vào CourseFactory để sửa, mỗi khi có một khóa học Offline mở ra, A11 cũng sẽ vào CourseFactory để sửa. Và mỗi khi có một loại hình khóa học mới được mở ra, lớp CourseFactory cũng sẽ được để ý đến… Phải chẳng CourseFactory bây giờ lại đang gặp vấn đề tương tự với CourseController ban đầu? Nó cũng bị ảnh hưởng nhiều mỗi khi có một yêu cầu mới (một loại hình khóa học mới). Nhắc lại nguyên lý SOLID Principle SRP – Single Responsibility Principle, mỗi lớp chỉ có một và duy nhất một lý do để thay đổi. Vậy thì hãy làm cho CourseFactory chỉ có một lý do duy nhất để thay đổi bằng cách tách rời những thành phần logic khác nhau trong một lớp ra những thành phần nhỏ hơn bằng những lớp khác. Lúc này A11 lại nghĩ ra một phương án mới đó là mỗi loại hình khóa học sẽ có một “nhà máy” cho riêng nó. Vì vậy trong thiết kế hiện tại sẽ có 2 nhà máy: OnlineFactory và OfflineFactory.

XÂY DỰNG NHIỀU NHÀ MÁY

Thiết kế mới cho hệ thống multi-factory:
Factory_3

public class OfflineCourseFactory {
public static AbstractCourse createCourse(String inCourseName) {
AbstractCourse course = null;
if(inCourseName.equals(CourseController.COURSE_JAVA_OFFLINE)) {
course = new JavaOfflineCourse();
}
else if(inCourseName.equals(CourseController.COURSE_DOT_NET_OFFLINE)) {
course = new DotNetOfflineCourse();
}
return course;
}
}

public class OnlineCourseFactory {
public static AbstractCourse createCourse(String inCourseName) {
AbstractCourse course = null;
if(inCourseName.equals(CourseController.COURSE_JAVA_ONLINE)) {
course = new JavaOnlineCourse();
}
else if(inCourseName.equals(CourseController.COURSE_DOT_NET_ONLINE)) {
course = new DotNetOnlineCourse();
}
return course;
}
}

Chúng ta biết rõ, để đăng ký 1 khóa học online thì chỉ cần đoạn code sau:

AbstractCourse onlineCourse = OnlineCourseFactory.createCourse(CourseController.COURSE_JAVA_ONLINE);

Và đây là đoạn code để đăng ký 1 khóa học offline:

AbstractCourse offlineCourse = OfflineCourseFactory.createCourse(CourseController.COURSE_DOT_NET_OFFLINE);

ĐƯA NHIỀU NHÀ MÁY VÀO SỬ DỤNG

Vấn đề còn lại là tích hợp chúng vào lớp CourseController để tạo ra 1 khóa học theo yêu cầu và display thông tin lên màn hình??? Ngay bây giờ, mình không thể nghĩ ra cách nào để có thể có thể tạo ra 1 khóa học tại hàm displayCourse. Thứ nhất, tại đây, ConurseController không thể quyết định được là sẽ tạo ra 1 khóa học online hay 1 khóa học offline bởi vì OnlineCourseFactory và OfflineCourseFactory là 2 lớp tách biệt. Có thể là chúng ta cần một tham số nữa trong hàm displayCourse để quyết định xem nên tạo loại hình khóa học nào. Lúc đó displayCourse sẽ thành:

public void displayCourse(String inCourseType, String inCourseName) {
AbstractCourse course = null;
if(inCourseType.equals(COURSE_TYPE_ONLINE)) {
course = OnlineCourseFactory.createCourse(inCourseName);
}
else if(inCourseType.equals(COURSE_TYPE_OFFLINE)) {
course = OfflineCourseFactory.createCourse(inCourseName);
}
// else if(inCourseType.equals(...))
if(course != null) {
course.display();
}
else {
System.out.println("Nothing is displayed");
}
}

Bây giờ thì chúng ta lại quay về với bài toán if/else ở phía trên. Mỗi khi có một loại hình khóa học mới được thêm vào, chúng ta lại phải thêm 1 block if/else để tạo ra khóa học ứng với loại hình đó. Tại đây magic sẽ xuất hiện. Thử nghĩ xem nếu có một “nhà máy sản xuất nhà máy” thì thế nào nhỉ? Nghĩa là hãy thử trừu tượng hóa đối tượng Factory, do đó, lớp Controller sẽ không phải phụ thuộc vào các lớp nhà máy cụ thể như: OnlineCourseFactory và OfflineCourseFactory. Nếu các bạn đã đọc qua bài viết “Why extends is evil” hoặc các nguyên lý hướng đối tượng có thể hiểu được chỉ nên làm việc với các interface (abstract) chứ không phải là các lớp cụ thể. Vì vậy đừng ngần ngại mà abstract đối tượng nhà máy Factory.

XÂY DỰNG NHÀ MÁY CỦA NHỮNG NHÀ MÁY

Thiết kế mới cho các đối tượng Factory sẽ như sau:
Factory_4

public abstract class AbstractCourseFactory {
public AbstractCourse createCourse(String inCourseName) {
AbstractCourse course = getCourse(inCourseName);
if(course != null) {
// Do a bunch of things here, such as initializing some information
// for the course before returning it to caller
}
return course;
}

public abstract AbstractCourse getCourse(String inCourseName);
}
public class OfflineCourseFactory extends AbstractCourseFactory {
public AbstractCourse getCourse(String inCourseName) {
AbstractCourse course = null;
if(inCourseName.equals(CourseController.COURSE_JAVA_OFFLINE)) {
course = new JavaOfflineCourse();
}
else if(inCourseName.equals(CourseController.COURSE_DOT_NET_OFFLINE)) {
course = new DotNetOfflineCourse();
}
return course;
}
}
public class OnlineCourseFactory extends AbstractCourseFactory {
public AbstractCourse getCourse(String inCourseName) {
AbstractCourse course = null;
if(inCourseName.equals(CourseController.COURSE_JAVA_ONLINE)) {
course = new JavaOnlineCourse();
}
else if(inCourseName.equals(CourseController.COURSE_DOT_NET_ONLINE)) {
course = new DotNetOnlineCourse();
}
return course;
}
}

(Chú ý là mình đã thay đổi phương thức createCourse thành non-static nhé)

QUANG CẢNH CUỐI CÙNG

OK, hãy tái cấu trúc lại lớp CourseController để có một thiết kế hoàn chỉnh
Factory_5

Toàn bộ source chương trình sẽ như sau:

public class CourseConstants {
public static String COURSE_JAVA_ONLINE = "JavaOnline";
public static String COURSE_DOT_NET_ONLINE = ".NETOnline";
public static String COURSE_JAVA_OFFLINE = "JavaOffline";
public static String COURSE_DOT_NET_OFFLINE = ".NETOffline";
}

public abstract class AbstractCourse {
private String courseName;
private long courseDuration;
public abstract void display();
}

public class DotNetOfflineCourse extends AbstractCourse {
@Override
public void display() {
System.out.println("Information of .NET offline course");
}
}

public class DotNetOnlineCourse extends AbstractCourse {
@Override
public void display() {
System.out.println("Information of .NET online course");
}
}

public class JavaOfflineCourse extends AbstractCourse {
@Override
public void display() {
System.out.println("Information of Java offline course");
}
}

public class JavaOnlineCourse extends AbstractCourse {
@Override
public void display() {
System.out.println("Information of Java online course");
}
}

public abstract class AbstractCourseFactory {
public AbstractCourse createCourse(String inCourseName) {
AbstractCourse course = getCourse(inCourseName);
if(course != null) {
// Do a bunch of things here, such as initializing some information
// for the course before returning it to caller
}
return course;
}

protected abstract AbstractCourse getCourse(String inCourseName);
}

public class OfflineCourseFactory extends AbstractCourseFactory {
public AbstractCourse getCourse(String inCourseName) {
AbstractCourse course = null;
if(inCourseName.equals(CourseConstants.COURSE_JAVA_OFFLINE)) {
course = new JavaOfflineCourse();
}
else if(inCourseName.equals(CourseConstants.COURSE_DOT_NET_OFFLINE)) {
course = new DotNetOfflineCourse();
}
return course;
}
}

public class OnlineCourseFactory extends AbstractCourseFactory {
public AbstractCourse getCourse(String inCourseName) {
AbstractCourse course = null;
if(inCourseName.equals(CourseConstants.COURSE_JAVA_ONLINE)) {
course = new JavaOnlineCourse();
}
else if(inCourseName.equals(CourseConstants.COURSE_DOT_NET_ONLINE)) {
course = new DotNetOnlineCourse();
}
return course;
}
}

public abstract class CourseController {
public void displayCourse(String inCourseName) {
AbstractCourse course = null;
AbstractCourseFactory factory = getCourseFactory();
course = factory.createCourse(inCourseName);
if(course != null) {
course.display();
}
else {
System.out.println("Nothing is displayed");
}
}
protected abstract AbstractCourseFactory getCourseFactory();
}

public class OfflineCourseController extends CourseController {
@Override
protected AbstractCourseFactory getCourseFactory() {
return new OfflineCourseFactory();
}
}

public class OnlineCourseController extends CourseController {
@Override
protected AbstractCourseFactory getCourseFactory() {
return new OnlineCourseFactory();
}
}

public class FactoryTest {
public static void main(String[] args) {
CourseController onlineController = new OnlineCourseController();
onlineController.displayCourse(CourseConstants.COURSE_DOT_NET_ONLINE);

CourseController offlineController = new OfflineCourseController();
offlineController.displayCourse(CourseConstants.COURSE_JAVA_OFFLINE);
}
}

Output:

Information of .NET online course
Information of Java offline course

Cùng nhìn lại toàn bộ những gì A11 đã làm từ những thiết kế ban đầu cho đến bây giờ. Từ việc chỉ quan tâm đến những khóa học được tao ra, anh ta tạo ra một thiết kế với kiểu kế thừa cha/con, và dùng if/else để tạo ra khóa học và display lên màn hình ứng với lựa chọn của người dùng. Sau đó, khi ngày càng có nhiều khóa học được thêm vào, A11 đã nghĩ ra cách phải loại bỏ những lệnh if/else ra khối xử lý chính, và FactoryMethod pattern được áp dụng để xử lý việc này bằng cách tạo ra lớp CourseFactory để “sinh” ra các khóa học theo yêu cầu, điều này đáp ứng được nguyên lý Open Closed Principle. Khi mà ngày càng có nhiều loại hình khóa học khác được thêm vào như Online, Offline, Corporate, … thì việc phải thay đổi lớp CourseFactory ngày càng thường xuyên hơn. Dẫn đến việc nguyên lý Single Responsibility Principle bị phá vỡ. AbstractFactory chính là hướng giải quyết trong lúc này. AbstractFactory sẽ để cho mỗi nhà máy Factory cụ thể tự “sinh” ra những khóa học đúng với trách nhiệm của nhà máy đó. Và đó là tất cả để chúng ta có được thiết kế cuối cùng.

Mình sẽ kết thúc mẫu thiết kế FactoryPattern tại đây. Nên nhớ rằng FactoryPattern gồm 2 loại: FactoryMethod (một nhà máy đơn giản) và AbstractFactory (nhà máy của những nhà máy), tùy theo yêu cầu của hệ thống mà mỗi pattern sẽ được áp dụng.

Bài viết gốc được đăng tải tại edwardthienhoang.wordpress.com

Có thể bạn quan tâm:

Xem thêm Việc làm Developer hấp dẫn trên TopDev