Trong bài viết này, tôi sẽ chỉ ra những nhược điểm và thiếu xót trong tư tưởng lập trình OOP mà đại đa số chúng ta thường gặp phải, đồng thời tôi cũng hướng dẫn bạn cách để tối ưu và giảm thiểu những rủi ro đó trong khi làm việc và tạo nên 1 kỹ năng mà bạn có thể áp dụng trong công việc thực tế.

Dispatching - là 1 kỹ thuật lập trình ít phụ thuộc, có tính thay đổi mềm dẻo, dễ nâng cấp - bảo trì mã nguồn do chia tách các phần mã lệnh rõ ràng và nó có thể áp dụng xử lý trong rất nhiều tình huống rắc rối, dễ gây nhầm lẫn và khó bảo trì.

Dispatch dịch nôm na là: gửi đi, phái đi. Và đúng như ngữ nghĩa của nó, tác dụng của kỹ thuật lập trình này là gửi request xử lý mã lệnh từ 1 hay nhiều đối tượng đến 1 đối tượng cụ thể chịu trách nhiệm xử lý mã lệnh phù hợp. Điều này cho phép bạn:

+ Chia tách logic xử lý mã lệnh khỏi phần giao diện. Việc này giúp chương trình của bạn giảm thiểu rủi ro khi giao diện ứng dụng được thiết kế lại hoặc thay đổi, di rời.

+ Chia tách mã lệnh xử lý cụ thể thành các Class tương ứng và chỉ phụ thuộc vào 1 Interface. Việc này giúp bạn lập trình ở mức cao hơn - lập trình trừu tượng và đảm bảo tính loosely-coupling. Trong cuộc sống, chúng ta thường nghĩ rằng những cái gì mà được liên kết chặt chẽ thì sẽ bền vững và chắc chắn hơn: "Một cây làm chẳng nên non. Ba cây chụp lại nên hòn núi cao". Nhưng trong lập trình mềm thì sự phụ thuộc, liên kết càng chặt chẽ càng chắc chắn lại là cơn ác mộng cho lập trình viên.

Để mô tả ngắn gọn và dễ hiểu cho kỹ thuật này, tôi sẽ lấy ví dụ đơn giản về 1 bài toán làm ứng dụng quản lý sinh viên. (Đây là bài toán mà đại đa số sinh viên chúng ta thường gặp phải nên sẽ dễ dàng để nắm bắt và áp dụng kỹ thuật Dispatching).

Chúng ta tạo 1 Project mới và bắt đầu thử nghiệm kỹ thuật này.


I. Vấn đề

Tôi viết demo phần hiển thị menu chọn chức năng và thực thi chức năng tương ứng. Sau đó tôi sẽ phân tích nhược điểm của cách làm này.

package com.blogspot.code4lifevn;

import java.util.Scanner;

/**
 *
 * @author code4lifevn
 */
public class DemoDispatcher {
    
    private static Scanner sc = new Scanner(System.in);
    
    public static void main(String[] args) {
        int option = getChoiceFromMenu();
        
        switch(option) {
            case 1:
                System.out.println("add a student, now");
                break;
            case 2:
                System.out.println("update a student, now");
                break;
            case 3:
                System.out.println("delete a student, now");
                break;
            case 4:
                System.out.println("display a student, now");
                break;
            case 5:
                System.out.println("OK, exit");
                break;
            default:
                System.out.println("Wrong!");
        }
    }
    
    private static int getChoiceFromMenu() {
        System.out.println("Option 1. Add A Student");
        System.out.println("Option 2. Update A Student");
        System.out.println("Option 3. Delete A Student");
        System.out.println("Option 4. Display A Student");
        System.out.println("Option 5. Exit");
        System.out.print("Enter: ");
        int choice = Integer.parseInt(sc.next());
        return choice;
    }
}


Với cách làm trên, chức năng hoàn toàn bị phụ thuộc vào giao diện. Nếu trên tầng giao diện, Option 1 không phải là thêm sinh viên nữa mà lại là xóa sinh viên thì ta lại phải sửa code ở tầng giao diện, việc làm này rất dễ gây những lỗi không đáng có và làm ảnh hưởng tới các chức năng khác. Có thể trong quá trình copy & cut & paste code, di dời từ chỗ này sang chỗ kia, ta phải Refactor lại cũng như sửa chữa rất phức tạp thì ứng dụng mới chạy được. Điều này gây lãng phí thời gian, công sức, tiền bạc và gây khó khăn trong việc nâng cấp ứng dụng.

Do đó, ta phải tách các mã xử lý logic ra khỏi tầng giao diện (Front-End) và chuyển toàn bộ những logic xử lý phức tạp xuống tầng core (Back-End). Dù giao diện có thay đổi hay bị di dời, mã lệnh xử lý logic hoàn toàn không gặp vấn đề gì, dù có thay đổi nâng cấp mã xử lý thì ta cũng chỉ làm việc riêng biệt với class chứa mã xử lý đó mà không phải làm việc với class chứa những mã lệnh không liên quan (điều này giúp tiết kiệm thời gian và giúp tập trung vào phần mã lệnh cần xử lý, giảm thiểu rủi ro cho các phần mã lệnh không liên quan).

Để tối ưu cho phần công việc này, tôi sẽ xử lý như sau.


II. Giải quyết vấn đề

Tôi cần 1 Class có nhiệm vụ tạo ra các Menu Option (tên option, key action) và 1 Interface để mô tả chức năng cần thực hiện. Cấu trúc dự án được thêm vào như sau:



Bây giờ, tôi sẽ viết mã xử lý chi tiết cho từng phần công việc này.

+ Mã xử lý class: com.blogspot.code4lifevn.menu.item.MenuItem

package com.blogspot.code4lifevn.menu.item;

/**
 *
 * @author code4lifevn
 */
public class MenuItem {
    
    //=> Label sẽ được dùng để hiển thị trên GUI
    private String label;
    
    //=> Action đóng vai trò là 1 key để gọi lớp xử lý mã lệnh tương ứng
    private String action;

    public MenuItem(String action, String label) {
        this.action = action;
        this.label = label;
    }

    public void setAction(String action) {
        this.action = action;
    }

    public void setLabel(String label) {
        this.label = label;
    }

    public String getAction() {
        return action;
    }

    public String getLabel() {
        return label;
    }

    @Override
    public String toString() {
        return this.label;
    }
    
}



+ Mã xử lý interface: com.blogspot.code4lifevn.menu.action.MenuAction

package com.blogspot.code4lifevn.menu.action;

/**
 *
 * @author code4lifevn
 */
public interface MenuAction {
    public void execute() throws Exception;
}


Bạn có thể thay đổi hoặc thêm các mô tả cho interface này sao cho phù hợp với công việc của bạn.

Tiếp theo tôi sẽ tạo ra 5 Class implements Interface MenuAction, cấu trúc dự án được thêm vào như sau:



Tôi viết mã thực thi chi tiết cho từng class này như sau:

+ Mã xử lý class: com.blogspot.code4lifevn.menu.action.AddStudentAction

package com.blogspot.code4lifevn.menu.action;

/**
 *
 * @author code4lifevn
 */
public class AddStudentAction implements MenuAction {

    @Override
    public void execute() throws Exception {
        System.out.println("Day la chuc nang them 1 sinh vien moi.\n Hay viet code xu ly cua ban");
    }

}


+ Mã xử lý class: com.blogspot.code4lifevn.menu.action.UpdateStudentAction

package com.blogspot.code4lifevn.menu.action;

/**
 *
 * @author code4lifevn
 */
public class UpdateStudentAction implements MenuAction {

    @Override
    public void execute() throws Exception {
        System.out.println("Day la chuc nang cap nhat thong tin 1 sinh vien.\n Hay viet code xu ly cua ban");
    }

}


+ Mã xử lý class: com.blogspot.code4lifevn.menu.action.DeleteStudentAction

package com.blogspot.code4lifevn.menu.action;

/**
 *
 * @author code4lifevn
 */
public class DeleteStudentAction implements MenuAction {

    @Override
    public void execute() throws Exception {
        System.out.println("Day la chuc nang xoa 1 sinh vien.\n Hay viet code xu ly cua ban");
    }

}


+ Mã xử lý class: com.blogspot.code4lifevn.menu.action.DisplayStudentAction

package com.blogspot.code4lifevn.menu.action;

/**
 *
 * @author code4lifevn
 */
public class DisplayStudentAction implements MenuAction {

    @Override
    public void execute() throws Exception {
        System.out.println("Day la chuc nang hien thi thong tin 1 sinh vien.\n Hay viet code xu ly cua ban");
    }

}


+ Mã xử lý class: com.blogspot.code4lifevn.menu.action.ExitAction

package com.blogspot.code4lifevn.menu.action;

/**
 *
 * @author code4lifevn
 */
public class ExitAction implements MenuAction {

    @Override
    public void execute() throws Exception {
        System.exit(0);
    }

}


Ngay lúc này,  bạn có thể thấy các chức năng được chia tách ra các class rất rõ ràng. Bạn muốn sửa chức năng nào thì chỉ cần "phi" vào class chứa mã lệnh xử lý mà bạn cần. Mọi mã lệnh không liên quan, bạn không cần phải bận tâm.

Chúng ta đã có chia tách thành công các phần mã xử lý thành các class riêng và khiến nó phụ thuộc vào 1 interface chung. Bước tiếp theo, tôi sẽ viết 1 class có nhiệm vụ đăng ký các Action này và quản lý chúng, dispatch tới class phù hợp khi được nhận "chỉ thị" từ tầng GUI. Đặt tên cho class này là ActionDispatcher. (có thể đặt ActionManager cũng hay hay :D)

Cấu trúc dự án được thêm vào như sau:



Viết mã cho class này, tôi sẽ sử dụng 1 vài kỹ thuật liên quan tới Reflection, Collection, Generic để triển khai mã lệnh.

1. Sử dụng Reflection để tối ưu việc khởi tạo đối tượng, chỉ khởi tạo khi thực sự cần.
2. Sử dụng Collection để quản lý các class Action.
3. Sử dụng Generic để đảm bảo tính nhất quán đối tượng.

+ Mã xử lý class: com.blogspot.code4lifevn.dispatcher.ActionDispatcher

package com.blogspot.code4lifevn.dispatcher;

import com.blogspot.code4lifevn.menu.action.AddStudentAction;
import com.blogspot.code4lifevn.menu.action.DeleteStudentAction;
import com.blogspot.code4lifevn.menu.action.DisplayStudentAction;
import com.blogspot.code4lifevn.menu.action.ExitAction;
import com.blogspot.code4lifevn.menu.action.MenuAction;
import com.blogspot.code4lifevn.menu.action.UpdateStudentAction;
import com.blogspot.code4lifevn.menu.item.MenuItem;
import java.util.HashMap;
import java.util.Map;

/**
 *
 * @author code4lifevn
 */
public final class ActionDispatcher {
    //=> @Java 6
//    private Map<String, Class<? extends MenuAction>> actionManager = new HashMap&ltString, Class&lt? extends MenuAction&gt&gt();
    
    //=> @Java 7
    private Map<String, Class<? extends MenuAction>> actionManager = new HashMap<>();
    public ActionDispatcher() {
        this.registerAction("add-student", AddStudentAction.class);
        this.registerAction("update-student", UpdateStudentAction.class);
        this.registerAction("delete-student", DeleteStudentAction.class);
        this.registerAction("display-student", DisplayStudentAction.class);
        this.registerAction("exit", ExitAction.class);
    }
    
    public MenuAction dispatch(MenuItem item) {
        String actionKey = item.getAction();
        Class classAction = this.actionManager.get(actionKey);
        
        MenuAction action = null;
        try {
            action = classAction.newInstance();
        } catch(Exception ex) {
            ex.printStackTrace();
        }
        
        return action;
    }
    
    public void registerAction(String key, Class classAction) {
        this.actionManager.put(key, classAction);
    }

    public Map<String, Class<? extends MenuAction>> getActionManager() {
        return actionManager;
    }
    
}



Cuối cùng, tôi sẽ viết lại mã xử lý cho class chính.

+Mã xử lý class: com.blogspot.code4lifevn.DemoDispatcher

package com.blogspot.code4lifevn;

import com.blogspot.code4lifevn.dispatcher.ActionDispatcher;
import com.blogspot.code4lifevn.menu.action.MenuAction;
import com.blogspot.code4lifevn.menu.item.MenuItem;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

/**
 *
 * @author code4lifevn
 */
public class DemoDispatcher {
    
    public static void main(String[] args) throws Exception {
        ActionDispatcher dispatcher = new ActionDispatcher();
        
        MenuItem item = createGUI();
        MenuAction dispatch = dispatcher.dispatch(item);
        dispatch.execute();
    }
    
    private static MenuItem createGUI() {
        //=> tao 1 list cac menu item
        List<MenuItem> menuItems = new ArrayList<MenuItem>() {
            {
                add(new MenuItem("exit", "Option 5. Exit"));
                add(new MenuItem("add-student", "Option 1. Add A New Student"));
                add(new MenuItem("update-student", "Option 2. Update New Student"));
                add(new MenuItem("delete-student", "Option 3. Delete A New Student"));
                add(new MenuItem("display-student", "Option 4. Display A New Student"));
            }
        };
        
        //=> GUI
        for (MenuItem menuItem : menuItems) {
            System.out.println(menuItem);
        }
        System.out.print("Enter: ");
        
        //=> xu ly tra ve action tuong ung
        Scanner sc = new Scanner(System.in);
        int choice = Integer.parseInt(sc.nextLine());
        MenuItem menuItem = menuItems.get(choice - 1);
        return menuItem;
    }
}


Với mã lệnh xử lý mới cho class chính này, bạn thử thay đổi thứ tự hiển thị của các MenuItem trong List và chạy lại ứng dụng để test xem các chức năng có hoạt động đúng khi thay đổi vị trí như vậy không? Mã xử lý của bạn giờ đây không bị phụ thuộc vào phía Front-End nữa, phía Front-End chỉ có nhiệm vụ call API và sử dụng, việc xử lý như nào phía Back-End sẽ xử lý ở bên dưới.

Bạn hãy thử áp dụng kỹ thuật này 1 vài lần cho quen nhé!


Download Source Code

1 nhận xét:

  1. trọng tâm dispatch thì không thấy, thấy tạo lại giao diện với các phương thức thiết lập thành đối tượng, thất bại nặng nề, lạc đề quá

    Trả lờiXóa

 

code4lifevn team

Thanh niên nghiêm túc :)

Name: Manh Do

Age: years old

Job: Senior Java and Mobile Developer

Country: VietNam

Name: Hung Nguyen

Age: years old

Job: Android Developer

Country: VietNam

Name: Trung PH

Age: years old

Job: Senior iOS and Android Developer

Country: VietNam

Name: Điệp NT

Age: years old

Job: Senior .Net and Android Developer

Country: VietNam