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

package com.maixuanviet.patterns.creational.servicelocator;
 
public interface MessagingService {
     
    String getMessageBody();
 
    String getServiceName();
}

EmailService.java

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

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

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

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

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

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

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ụ: 

com.maixuanviet.patterns.creational.servicelocator.EmailService

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

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:

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

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

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.

Related posts:

Java Program to Compute Discrete Fourier Transform Using Naive Approach
Hướng dẫn Java Design Pattern – Interpreter
Number Formatting in Java
Java Program to Implement Pairing Heap
Hướng dẫn Java Design Pattern – Decorator
Giới thiệu JDBC Connection Pool
Abstract class và Interface trong Java
Spring WebFlux Filters
Working with Kotlin and JPA
Retrieve User Information in Spring Security
Introduction to Spring Cloud Rest Client with Netflix Ribbon
Quick Guide to Spring Controllers
Split a String in Java
Query Entities by Dates and Times with Spring Data JPA
Introduction to Java Serialization
Simple Single Sign-On with Spring Security OAuth2
Java Program to Implement Heap’s Algorithm for Permutation of N Numbers
Java Program to Implement Heap Sort Using Library Functions
Guide to WeakHashMap in Java
Java Program to Generate Randomized Sequence of Given Range of Numbers
Spring Cloud – Tracing Services with Zipkin
Java Program to Compute the Area of a Triangle Using Determinants
Spring Security 5 – OAuth2 Login
Java Program to Implement Trie
Java Program to Implement Bresenham Line Algorithm
LinkedHashSet trong Java hoạt động như thế nào?
Câu lệnh điều khiển vòng lặp trong Java (break, continue)
Setting a Request Timeout for a Spring REST API
Java – Random Long, Float, Integer and Double
Kết hợp Java Reflection và Java Annotations
Java Web Services – Jersey JAX-RS – REST và sử dụng REST API testing tools với Postman
Lấy ngày giờ hiện tại trong Java