5
\$\begingroup\$

I'm working on a "framework" for Telegram bots and right now everything "just works". I'm now in the process of rethinking about what I wrote and one of the things I dislike a lot is how I implemented the API client.

Right now I have an interface (TelegramBotApi) which maps every API endpoint to a method. This interface is implemented by this class:

public class TelegramBotRestApi implements TelegramBotApi {

private final ObjectMapper mapper;
private final RestTemplate restTemplate;

private final UriTemplate apiUriTemplate;
private final UriTemplate fileUriTemplate;

public TelegramBotRestApi(String token, ObjectMapper mapper, RestTemplate restTemplate) {
    this.mapper = mapper;
    this.restTemplate = restTemplate;
    this.apiUriTemplate = new UriTemplate("https://api.telegram.org/bot" + token + "/{method}");
    this.fileUriTemplate = new UriTemplate("https://api.telegram.org/file/bot" + token + "/{file_path}");
}

@Override
public User getMe() throws TelegramBotApiException {

    TelegramBotRestApiCall.Builder<User> builder = new TelegramBotRestApiCall.Builder<User>("getMe", apiUriTemplate,
            mapper, restTemplate, User.class);

    builder.setHttpMethod(HttpMethod.GET);

    return builder.build().call();

}

@Override
public List<Update> getUpdates(Integer offset, Integer limit, Integer timeout) throws TelegramBotApiException {

    TelegramBotRestApiCall.Builder<Update[]> builder = new TelegramBotRestApiCall.Builder<Update[]>("getUpdates",
            apiUriTemplate, mapper, restTemplate, Update[].class);

    builder.setParam("offset", offset, false).setParam("limit", limit, false).setParam("timeout", timeout, false);

    builder.setContentType(MediaType.MULTIPART_FORM_DATA);

    Update[] result = builder.build().call();
    Arrays.sort(result);

    return Collections.unmodifiableList(Arrays.asList(result));
}

@Override
public Message sendMessage(ChatId chatId, String text, ParseMode parseMode, Boolean disableWebPagePreview,
        Boolean disableNotification, Integer replyToMessageId, AbstractKeyboardMarkup replyMarkup)
        throws TelegramBotApiException {

    TelegramBotRestApiCall.Builder<Message> builder = new TelegramBotRestApiCall.Builder<Message>("sendMessage",
            apiUriTemplate, mapper, restTemplate, Message.class);

    builder.setParam("chat_id", chatId, true).setParam("text", text, true).setParam("parse_mode", parseMode, false)
            .setParam("disable_web_page_preview", disableWebPagePreview, false)
            .setParam("disable_notification", disableNotification, false)
            .setParam("reply_to_message_id", replyToMessageId, false).setParam("reply_markup", replyMarkup, false);

    builder.setContentType(MediaType.MULTIPART_FORM_DATA);
    return builder.build().call();
}

// many more methods
} 

This class is using another class (TelegramBotRestApiCall) to build the actual call and then execute it:

public class TelegramBotRestApiCall<T> {

private final RestTemplate rest;

@SuppressWarnings("rawtypes")
private static final Map<Class, ParameterizedTypeReference> typeRefs = initializeParameterizedTypeReferences();

@SuppressWarnings("rawtypes")
private static Map<Class, ParameterizedTypeReference> initializeParameterizedTypeReferences() {

    Map<Class, ParameterizedTypeReference> tmp = new HashMap<Class, ParameterizedTypeReference>();

    tmp.put(User.class, new ParameterizedTypeReference<Response<User>>() {
    });

    tmp.put(Update[].class, new ParameterizedTypeReference<Response<Update[]>>() {
    });

    tmp.put(Message.class, new ParameterizedTypeReference<Response<Message>>() {
    });

    tmp.put(UserProfilePhotos.class, new ParameterizedTypeReference<Response<UserProfilePhotos>>() {
    });

    tmp.put(TelegramFile.class, new ParameterizedTypeReference<Response<TelegramFile>>() {
    });

    tmp.put(Boolean.class, new ParameterizedTypeReference<Response<Boolean>>() {
    });

    tmp.put(Chat.class, new ParameterizedTypeReference<Response<Chat>>() {
    });

    tmp.put(ChatMember.class, new ParameterizedTypeReference<Response<ChatMember>>() {
    });

    tmp.put(ChatMember[].class, new ParameterizedTypeReference<Response<ChatMember[]>>() {
    });

    tmp.put(Integer.class, new ParameterizedTypeReference<Response<Integer>>() {
    });

    tmp.put(BooleanOrMessage.class, new ParameterizedTypeReference<Response<BooleanOrMessage>>() {
    });

    // for methods without an output (ie. sendChatAction)
    tmp.put(String.class, new ParameterizedTypeReference<Response<String>>() {
    });

    return tmp;
}

private final ObjectMapper mapper;
private final String method;
private final UriTemplate uriTemplate;
private final HttpMethod httpMethod;
private final HttpEntity<?> entity;
@SuppressWarnings("rawtypes")
private final Class payloadType;

@SuppressWarnings("rawtypes")
private TelegramBotRestApiCall(String method, UriTemplate uriTemplate, ObjectMapper mapper, RestTemplate rest,
        HttpMethod httpMethod, HttpEntity<?> entity, Class payloadType) {
    this.method = method;
    this.uriTemplate = uriTemplate;
    this.httpMethod = httpMethod;
    this.mapper = mapper;
    this.entity = entity;
    this.payloadType = payloadType;
    this.rest = rest;
}

@SuppressWarnings("unchecked")
public T call() throws TelegramBotApiException {

    Response<T> res = null;

    try {
        res = (Response<T>) rest.exchange(uriTemplate.expand(method), httpMethod, entity, typeRefs.get(payloadType))
                .getBody();
    } catch (HttpStatusCodeException he) {
        Integer statusCode = null;

        try {
            res = mapper.readValue(he.getResponseBodyAsString(), Response.class);
            statusCode = he.getStatusCode() != null ? he.getStatusCode().value() : null;
        } catch (IOException e) {
        }

        throw new TelegramBotApiException(he, statusCode);

    } catch (RestClientException e) {
        throw new TelegramBotApiException(e);
    }

    if (!res.ok)
        throw new TelegramBotApiException(res.description, res.errorCode);

    return res.result;
}

public static class Builder<T> {

    private final ObjectMapper mapper;
    private final RestTemplate rest;
    private final String method;
    private final UriTemplate uriTemplate;
    private final MultiValueMap<String, Object> body = new LinkedMultiValueMap<String, Object>();
    private final HttpHeaders headers = new HttpHeaders();
    @SuppressWarnings("rawtypes")
    private final Class payloadType;

    private HttpMethod httpMethod;
    private HttpEntity<?> entity;

    @SuppressWarnings("rawtypes")
    public Builder(String method, UriTemplate uriTemplate, ObjectMapper mapper, RestTemplate rest, Class payloadType) {
        this.method = method;
        this.uriTemplate = uriTemplate;
        this.mapper = mapper;
        this.rest = rest;
        this.payloadType = payloadType;
        this.httpMethod = HttpMethod.POST;
    }

    public Builder<T> setParam(String name, Object param, boolean required) {

        if (required && param == null)
            throw new InvalidParameterException(
                    String.format("(%s) Null value is not allowed for field: %s", this.method, name));

        if (param != null)
            this.body.add(name, param);

        return this;
    }

    public Builder<T> setHttpMethod(HttpMethod method) {
        this.httpMethod = method;
        return this;
    }

    public Builder<T> setContentType(MediaType type) {
        this.headers.setContentType(type);
        return this;
    }

    public TelegramBotRestApiCall<T> build() {

        if (this.body.size() > 0 || this.headers.size() > 0) {
            this.entity = new HttpEntity<Object>(body, headers);
        }

        return new TelegramBotRestApiCall<T>(method, uriTemplate, mapper, rest, httpMethod, entity, payloadType);
    }

}

}

I feel like this is actually a complete mess and would like to redesign it in a more "clean" way.

One of my main goals is the implementation of the API constraints (string parameters length for example) which I'm almost ignoring right now (I'm only checking for mandatory parameters).

One of the ideas floating in my mind is to create a class for each API method, keep mandatory parameters in the constructor and validate parameters inside the setters. With this change I could then have a single method which would handle every call.

I would be really grateful if you could point me to the "right" direction (I know there's no "right" direction but advice are welcome).

For anyone wondering, the whole project is hosted on github.

\$\endgroup\$
0

1 Answer 1

2
+50
\$\begingroup\$

You are right, the code looks messy. Here are some hints that concern only the fragments listed in the question (I looked briefly inside the project repo, but it's too huge to analyze here).

TelegramBotRestApiCall

  • The multiple raw type warnings can be avoided easily by adding a generic type parameter to payloadType field, making it Class<T> payloadType, with similar changes to the arguments of the two constructors.

  • In this particular case I do not like the idea of typeRefs map for holding ParameterizedTypeReference instances. Looking inside the definition of this object, which is quite lightweight, I think that there is no reason to pre-instantiate them for each class. These instances can be created on-the-fly when necessary. The main drawback of this typeRefs collection is the evolution of API: each time when you (or someone else) add a new object into your data model, initializeParameterizedTypeReferences method will need to be updated and this is not very obvious to find.

I'd suggest to replace this method with just

public static <U> ParameterizedTypeReference<Response<U>> getTypeReference(Class<U> targetClass) {
  return new ParameterizedTypeReference<Response<U>>() {};
}
  • The builder pattern implementation looks ugly. There are as many constructor arguments in the builder object, as in the target one, while one of the ideas of the pattern is exactly avoiding to have too many constructor arguments. So, there should be methods like method(String method), uriTemplate(UriTemplate uriTemplate), mapper(ObjectMapper mapper) etc. Setter-like methods as setContentType should also be renamed to contentType. I'd keep the set prefix only for setParam method, because the target parameter is passed by name in the arguments.

  • The builder class definition itself can be extracted into separate file. This will allow to eliminate long declarations like TelegramBotRestApiCall.Builder<T>, replacing them with something like ApiCallBuilder<T>. Or import Builder directly into the classes using it.

  • The call method also looks messy, especially due to the nested try-catches, which is something to avoid. The IOException caught in the inner block is lost without trace; the assignment of res is useless because of the exception thrown in the block.

So I can suggest this simplification to the call method:

public T call() throws TelegramBotApiException {
  try {
    final Response<T> res = rest.exchange(uriTemplate.expand(method), httpMethod, entity, getTypeReference(payloadType))
          .getBody();
    if (!res.ok) {
      throw new TelegramBotApiException(res.description, res.errorCode);
    }
    return res.result;
  } catch (HttpStatusCodeException he) {
    throw new TelegramBotApiException(he, 
                                      he.getStatusCode() != null ? he.getStatusCode().value() : null);
  } catch (RestClientException e) {
    throw new TelegramBotApiException(e);
  }
}

TelegramBotRestApi

  • The bodies of the methods should be changed after refactoring the Builder class as mentioned above.

  • URL parameters for UriTemplates construction should be extracted into dedicated (.properties, .json, .conf etc) files and loaded from there. If someday https://api.telegram.org/bot changes to https://api.telegram.org/v1/bot, you won't be pleased to recompile everything, instead of just changing the configuration file.

  • I've had a look into TelegramBotApi interface and I can say that there are many things to improve, for instance:

1) There are too many methods doing very different things. It should be split into smaller ones, by action semantics: user management, sending resources, messaging, etc.

2) Too many arguments for the majority of the methods. Try to reduce them by wrapping things into dedicated objects.

3) There are many methods like:

Message sendVideo(ChatId chatId, Resource video, Integer duration, Integer width, Integer height, String caption,
        Boolean disableNotification, Integer replyToMessageId, AbstractKeyboardMarkup replyMarkup) throws TelegramBotApiException;


Message sendVoice(ChatId chatId, Resource voice, Integer duration, Boolean disableNotification, Integer replyToMessageId, AbstractKeyboardMarkup replyMarkup) throws TelegramBotApiException;

They have ate least five similar arguments. It really seems that instead of having this pullulation of methods there should be an abstraction like send(chatId, resource, etc).

\$\endgroup\$
2
  • \$\begingroup\$ Thank you. About the send* methods: I was thinking about having a single send() method and then use data objects which extend an abstract class to pass the endpoint name and the parameters, this could let me to check constraints (mandatory parameters, param length,etc..) before doing the call.Do you think it would be an antipattern? \$\endgroup\$ Commented Aug 5, 2016 at 14:47
  • \$\begingroup\$ just remembered why that ugly map exists, posting a link to stackoverflow just in case you were interested: stackoverflow.com/questions/21987295/… \$\endgroup\$ Commented Aug 6, 2016 at 21:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.