How to follow Single Responsibility principle in m

2019-06-15 01:59发布

问题:

I am using RestTemplate as my HttpClient to execute URL and the server will return back a json string as the response. Customer will call this library by passing DataKey object which has userId in it.

  • Using the given userId, I will find out what are the machines that I can hit to get the data and then store those machines in a LinkedList, so that I can execute them sequentially.
  • After that I will check whether the first hostname is in block list or not. If it is not there in the block list, then I will make a URL with the first hostname in the list and execute it and if the response is successful then return back the response. But let's say if that first hostname is in the block list, then I will try to get the second hostname in the list and make the url and execute it, so basically, first find the hostname which is not in block list before making the URL.
  • Now, let's say if we selected first hostname which was not in the block list and executed the URL and somehow server was down or not responding, then I will execute the second hostname in the list and keep doing this until you get a successful response. But make sure they were not in the block list as well so we need to follow above point.
  • If all servers are down or in block list, then I can simply log and return the error that service is unavailable.

Below is my DataClient class which will be called by customer and they will pass DataKey object to getData method.

public class DataClient implements Client {

    private RestTemplate restTemplate = new RestTemplate(new HttpComponentsClientHttpRequestFactory());
    private ExecutorService service = Executors.newFixedThreadPool(15);

    public Future<DataResponse> getData(DataKey key) {
        DataExecutorTask task = new DataExecutorTask(key, restTemplate);
        Future<DataResponse> future = service.submit(task);

        return future;
    }
}

Below is my DataExecutorTask class:

public class DataExecutorTask implements Callable<DataResponse> {

    private DataKey key;
    private RestTemplate restTemplate;

    public DataExecutorTask(DataKey key, RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
        this.key = key;
    }

    @Override
    public DataResponse call() {
        DataResponse dataResponse = null;
        ResponseEntity<String> response = null;

        MappingsHolder mappings = ShardMappings.getMappings(key.getTypeOfFlow());

        // given a userId, find all the hostnames 
        // it can also have four hostname or one hostname or six hostname as well in the list
        List<String> hostnames = mappings.getListOfHostnames(key.getUserId());

        for (String hostname : hostnames) {
            // If host name is null or host name is in local block list, skip sending request to this host
            if (ClientUtils.isEmpty(hostname) || ShardMappings.isBlockHost(hostname)) {
                continue;
            }
            try {
                String url = generateURL(hostname);
                response = restTemplate.exchange(url, HttpMethod.GET, key.getEntity(), String.class);

                if (response.getStatusCode() == HttpStatus.NO_CONTENT) {
                    dataResponse = new DataResponse(response.getBody(), DataErrorEnum.NO_CONTENT,
                            DataStatusEnum.SUCCESS);
                } else {
                    dataResponse = new DataResponse(response.getBody(), DataErrorEnum.OK,
                            DataStatusEnum.SUCCESS);
                }

                break;
                // below codes are duplicated looks like
            } catch (HttpClientErrorException ex) {
                HttpStatusCodeException httpException = (HttpStatusCodeException) ex;
                DataErrorEnum error = DataErrorEnum.getErrorEnumByException(httpException);
                String errorMessage = httpException.getResponseBodyAsString();
                dataResponse = new DataResponse(errorMessage, error, DataStatusEnum.ERROR);

                return dataResponse;
            } catch (HttpServerErrorException ex) {
                HttpStatusCodeException httpException = (HttpStatusCodeException) ex;
                DataErrorEnum error = DataErrorEnum.getErrorEnumByException(httpException);
                String errorMessage = httpException.getResponseBodyAsString();
                dataResponse = new DataResponse(errorMessage, error, DataStatusEnum.ERROR);

                return dataResponse;
            } catch (RestClientException ex) {
                // if it comes here, then it means some of the servers are down so adding it into block list
                ShardMappings.blockHost(hostname);
            }
        }

        if (ClientUtils.isEmpty(hostnames)) {
            dataResponse = new DataResponse(null, DataErrorEnum.PERT_ERROR, DataStatusEnum.ERROR);
        } else if (response == null) { // either  all the servers are down or all the servers were in block list
            dataResponse = new DataResponse(null, DataErrorEnum.SERVICE_UNAVAILABLE, DataStatusEnum.ERROR);
        }

        return dataResponse;
    }
}

My block list keeps-on getting updated from another background thread every 1 minute. If any server is down and not responding, then I need to block that server by using this -

ShardMappings.blockHost(hostname);

And to check whether any server is in block list or not, I use this -

ShardMappings.isBlockHost(hostname);

I am returning SERVICE_UNAVAILABLE if servers are down or in block list,on the basis of response == null check, not sure whether it's a right approach or not.

I am not following Single Responsibility Principle here I guess at all. Can anyone provide an example what is the best way to use SRP principle here.

After thinking a lot, I was able to extract hosts class like given below but not sure what is the best way to use this in my above DataExecutorTask class.

public class Hosts {

    private final LinkedList<String> hostsnames = new LinkedList<String>();

    public Hosts(final List<String> hosts) {
        checkNotNull(hosts, "hosts cannot be null");
        this.hostsnames.addAll(hosts);
    }

    public Optional<String> getNextAvailableHostname() {
        while (!hostsnames.isEmpty()) {
            String firstHostname = hostsnames.removeFirst();
            if (!ClientUtils.isEmpty(firstHostname) && !ShardMappings.isBlockHost(firstHostname)) {
                return Optional.of(firstHostname);
            }
        }
        return Optional.absent();
    }

    public boolean isEmpty() {
        return hostsnames.isEmpty();
    }
}

回答1:

Your concern is valid. First, let's see what the original data executor do:

First, it is getting list of hostnames
Next, it loops through every hostnames that do the following things:
    It checks whether the hostname is valid to send request.
    If not valid: skip. 
    Else continue.
        Generate the URL based on hostname
        Send the request
        Translate the request response to domain response
        Handle exceptions
If the hostnames is empty, generate an empty response
Return response

Now, what can we do to follow SRP? As I can see, we can group those operations into some groups. What I can see is, these operations can be split into:

HostnameValidator:        checks whether the hostname is valid to send request
--------------
HostnameRequestSender:    Generate the URL
                          Send the request
--------------
HttpToDataResponse:       Translate the request response to domain response
--------------
HostnameExceptionHandler: Handle exceptions

That is, one approach to de-couple your operations and to follow SRP. There is also other approach, for example to simplify your operations:

First, it is getting list of hostnames
If the hostnames is empty, generate an empty response
Next, it loops through every hostnames that do the following things:
    It checks whether the hostname is valid to send request
    If not valid: remove hostname
    Else: Generate the URL based on hostname
Next, it loops through every valid hostnames that do the following things:
    Send the request
    Translate the request response to domain response
    Handle exceptions
Return response

Then it can also be split into:

HostnameValidator:        checks whether the hostname is valid to send request
--------------
ValidHostnameData:        Getting list of hostnames
                          Loops through every hostnames that do the following things:
                              Checks whether the hostname is valid to send request
                              If not valid: remove hostname
                              Else: Generate the URL based on hostname
--------------
HostnameRequestSender:    Send the request
--------------
HttpToDataResponse:       Translate the request response to domain response
--------------
HostnameExceptionHandler: Handle exceptions

Of course there are also other way to do it. And I leave the implementation details blank because there is many way to implement it.