Hướng dẫn Java Design Pattern – Service Locator

Trong các ứng dụng, chúng ta thường gặp trường hợp một class Client phụ thuộc vào một service hoặc một component là những lớp cụ thể (concrete class) trong lúc chạy ứng dụng.

Sự phụ thuộc của class Client vào các service này sẽ có một số vấn đề cần phải giải quyết:

  • Nếu thay thế hoặc cập nhật các service phụ thuộc, chúng ta cần thay đổi mã nguồn của class Client.
  • Các concrete class của một phụ thuộc có thể thay đổi tại thời điểm run-time hay không?
  • Các class cần phải viết những đoạn code cho việc quản lý, khởi tạo các phụ thuộc.
  • Khó khăn khi viết Unit Test do các lớp được kết hợp chặt chẽ.

Giải pháp cho vấn đề này chính là áp dụng Service Locator pattern, nó tạo ra một class chứa các tham chiếu đến các service và nó đóng gói các xử lý nghiệp vụ để xác định các service.

1. Service Locator Pattern là gì?

The service locator pattern is a design pattern used in software development to encapsulate the processes involved in obtaining a service with a strong abstraction layer. This pattern uses a central registry known as the “service locator” which on request returns the information necessary to perform a certain task.

The ServiceLocator is responsible for returning instances of services when they are requested for by the service consumers or the service clients.

Service Locator là một design pattern thông dụng cho phép tách rời (decouple) một class với các dependency (hay được gọi là service) của nó. Service Locator có thể coi là một đối tượng trung gian trong việc liên kết class và các dependency.

Service Locator pattern mô tả cách để đăng ký và lấy các dependency để sử dụng. Thông thường Service Locator được kết hợp với Factory Pattern hoặc Dependency Injection Pattern để có thể tạo ra các instance của service.

2. Cài đặt Service Locator Pattern như thế nào?

Các thành phần tham gia Service Locator Pattern:

  • Service : các Service thực tế sẽ xử lý các request từ Client. Đối tượng Service ban đầu được tìm bởi ServiceLocator và trả lại kết quả theo yêu cầu.
  • Service Locator : là một điểm liên lạc duy nhất để trả về các Service từ bộ đệm (Cache).
  • Cache : một đối tượng để lưu trữ các tham chiếu đến Service để sử dụng lại chúng sau này.
  • Initial Context : tạo và đăng ký tham chiếu đến các Service trong bộ đệm (Cache).
  • Client : là đối tượng gọi các Service thông qua ServiceLocator.

2.1. Ví dụ sử dụng Service Locator Pattern

MessagingService.java

1
2
3
4
5
6
7
8
package com.maixuanviet.patterns.creational.servicelocator;
 
public interface MessagingService {
     
    String getMessageBody();
 
    String getServiceName();
}

EmailService.java

1
2
3
4
5
6
7
8
9
10
11
12
package com.maixuanviet.patterns.creational.servicelocator;
 
public class EmailService implements MessagingService {
 
    public String getMessageBody() {
        return "This is message body of Email message";
    }
 
    public String getServiceName() {
        return "EmailService";
    }
}

SMSService.java

1
2
3
4
5
6
7
8
9
10
11
12
package com.maixuanviet.patterns.creational.servicelocator;
 
public class SMSService implements MessagingService {
 
    public String getMessageBody() {
        return "This is message body of SMS message";
    }
 
    public String getServiceName() {
        return "SMSService";
    }
}

InitialContext.java

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.maixuanviet.patterns.creational.servicelocator.example1;
 
public class InitialContext {
 
    public MessagingService lookup(String serviceName) {
        if (serviceName.equalsIgnoreCase("EmailService")) {
            return new EmailService();
        } else if (serviceName.equalsIgnoreCase("SMSService")) {
            return new SMSService();
        }
        return null;
    }
}

Cache.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.maixuanviet.patterns.creational.servicelocator.example1;
 
import java.util.ArrayList;
import java.util.List;
 
public class Cache {
 
    private static final List<MessagingService> SERVICES = new ArrayList<>();
 
    public MessagingService getService(String serviceName) {
        for (MessagingService messagingService : SERVICES) {
            if (messagingService.getClass().getSimpleName().equals(serviceName)) {
                return messagingService;
            }
        }
        return null;
    }
 
    public void addService(MessagingService newService) {
        SERVICES.add(newService);
    }
}

ServiceLocator.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.maixuanviet.patterns.creational.servicelocator.example1;
 
public final class ServiceLocator {
 
    private static Cache cache = new Cache();
     
    private ServiceLocator() {
        throw new IllegalAccessError("Can't construct this class directly");
    }
 
    public static MessagingService getService(String serviceName) {
 
        MessagingService service = cache.getService(serviceName);
 
        if (service != null) {
            System.out.println("Get service from cache: " + serviceName);
            return service;
        }
 
        System.out.println("Create a new service and add to cache: " + serviceName);
        InitialContext context = new InitialContext();
        service = context.lookup(serviceName);
        cache.addService(service);
        return service;
    }
}

ServiceLocatorPatternExample.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.maixuanviet.patterns.creational.servicelocator.example1;
 
public class ServiceLocatorPatternExample {
 
    public static void main(String[] args) {
        MessagingService service = ServiceLocator.getService("EmailService");
        System.out.println(service.getMessageBody());
 
        MessagingService smsService = ServiceLocator.getService("SMSService");
        System.out.println(smsService.getMessageBody());
 
        MessagingService emailService = ServiceLocator.getService("EmailService");
        System.out.println(emailService.getMessageBody());
    }
}

MessagingService.java

1
2
3
4
5
6
Create a new service and add to cache: EmailService
This is message body of Email message
Create a new service and add to cache: SMSService
This is message body of SMS message
Get service from cache: EmailService
This is message body of Email message

2.2. Ví dụ cãi tiến Service Locator Pattern với Reflection

Trong ví dụ trên, lớp InitialContext chịu trách nhiệm khởi tạo các class dựa trên đối số serviceName. Cách làm này chưa được tốt, mỗi khi có service mới chúng ta phải sửa đổi code của class này để khởi tạo Service tương ứng. Chúng ta có thể sử dụng sửa đổi class này lại bằng cách nhận đối số là một serviceName có đầy đủ cả package của nó, sau đó sử dụng Reflection để khởi tạo instance cho Service. Ví dụ: 

1
com.maixuanviet.patterns.creational.servicelocator.EmailService

Code của InitialContext được viết lại như sau:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.maixuanviet.patterns.creational.servicelocator.example2_reflection;
 
import com.maixuanviet.patterns.creational.servicelocator.MessagingService;
 
public class InitialContext {
 
    public MessagingService lookup(String serviceName) {
        try {
            Class<MessagingService> clazz = (Class<MessagingService>) Class.forName(serviceName);
            Object newInstance = clazz.newInstance();
            return cast(newInstance, clazz);
        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;
    }
     
    public static <T> T cast(Object obj, Class<T> clazz) {
        if (clazz.isAssignableFrom(obj.getClass())) {
            return clazz.cast(obj);
        }
        throw new ClassCastException("Failed to cast instance.");
    }
}

Class Cache của chúng ta cũng cần sửa lại đôi chút:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.maixuanviet.patterns.creational.servicelocator.example2_reflection;
 
import java.util.ArrayList;
import java.util.List;
 
import com.maixuanviet.patterns.creational.servicelocator.MessagingService;
 
public class Cache {
 
    private static final List<MessagingService> SERVICES = new ArrayList<>();
 
    public MessagingService getService(String serviceName) {
        for (MessagingService messagingService : SERVICES) {
            if (messagingService.getClass().getCanonicalName().equals(serviceName)) {
                return messagingService;
            }
        }
        return null;
    }
 
    public void addService(MessagingService newService) {
        SERVICES.add(newService);
    }
}

ServiceLocator.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.maixuanviet.patterns.creational.servicelocator.example2_reflection;

import com.maixuanviet.patterns.creational.servicelocator.MessagingService;

public final class ServiceLocator {

    private static Cache cache = new Cache();
   
    private ServiceLocator() {
        throw new IllegalAccessError("Can't construct this class directly");
    }

    public static MessagingService getService(String serviceName) {

        MessagingService service = cache.getService(serviceName);

        if (service != null) {
            System.out.println("Get service from cache: " + serviceName);
            return service;
        }

        System.out.println("Create a new service and add to cache: " + serviceName);
        InitialContext context = new InitialContext();
        service = context.lookup(serviceName);
        cache.addService(service);
        return service;
    }
}

ServiceLocatorPatternExample.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.maixuanviet.patterns.creational.servicelocator.example2_reflection;

import com.maixuanviet.patterns.creational.servicelocator.EmailService;
import com.maixuanviet.patterns.creational.servicelocator.MessagingService;
import com.maixuanviet.patterns.creational.servicelocator.SMSService;

public class ServiceLocatorPatternExample {

    public static void main(String[] args) {
        MessagingService service = ServiceLocator.getService(EmailService.class.getCanonicalName());
        System.out.println(service.getMessageBody());

        MessagingService smsService = ServiceLocator.getService(SMSService.class.getCanonicalName());
        System.out.println(smsService.getMessageBody());

        MessagingService emailService = ServiceLocator.getService(EmailService.class.getCanonicalName());
        System.out.println(emailService.getMessageBody());
    }
}

3. Lợi ích của Service Locator Pattern là gì?

Ưu điểm:

  • Nó tạo và giữ một thể hiện của các class Service trong một ServiceLocator duy nhất.
  • Tách rời một class với các dependency của nó, nhờ đó có thể dễ dàng thay thế các dependency tại thời điểm run-time mà không cần phải re-compile hay thậm chí là restart ứng dụng.
  • Các dependency sẽ được sử dụng dưới dạng interface, đảm bảo không sử dụng các class cụ thể (concrete) của dependency.
  • Dễ dàng test các class do không phụ thuộc vào các dependency.
  • Ứng dụng có thể được chia ra các phần ít bị ràng buộc (loose coupling) với nhau. Các đoạn code để tạo, quản lý dependency được tách riêng ra khỏi các class.

Nhược điểm:

  • Khó khăn khi debug và phát hiện lỗi do Client không biết được phần còn lại của hệ thống, Service nào sẽ được thực thi, mọi thứ được đăng ký với Service Locator.
  • Service Locator phải là duy nhất, nên có thể gặp hiện tượng thắt cổ chai.
  • Service Locator che dấu các lớp được đăng ký với Client, do đó có thể gặp lỗi run-time thay vì compile-time khi Service không tồn tại.
  • Service Locator làm cho code khó bảo trì hơn so với việc sử dụng DI (Dependency Injection). Các concrete class vẫn có phụ thuộc vào ServiceLocator, ServiceLocator chịu trách nhiệm tạo ra các phụ thuộc của nó. Với DI chỉ được gọi 1 lần để tiêm phụ thuộc vào một số lớp chính. Các lớp mà lớp chính này phụ thuộc vào sẽ đệ quy các phụ thuộc của chúng, cho đến khi có một đối tượng hoàn chỉnh. DI dễ viết Unit Test hơn.

4. Sử dụng Service Locator Pattern khi nào?

  • Khi cần tách rời một class với các dependency của nó.
  • Khi cần quản lý tập trung việc khởi tạo các Service.

Lời kết:

Service Locator Pattern là một mẫu đơn giản để giúp chúng ta giảm sự phụ thuộc giữa các service trong ứng dụng với việc áp dụng nguyên lý Inversion of Control. Tuy nhiên, trong trường hợp sử dụng các lớp trong nhiều ứng dụng, Dependency Injection là sự lựa chọn tốt hơn.

Service Locator cũng là một thuật ngữ hay được nhắc đến cùng với nguyên lý Dependency Inversion trong SOLID, IoC Container, DI container. Service Locator chính là một cách thực hiện của IoC Container. Trong các bài viết tiếp theo chúng ta sẽ cùng tìm hiểu về các khái niệm liên quan này.

Be the first to comment

Leave a Reply

Your email address will not be published.


*