Большинство понимает, что многократное повторение кода (или copy-paste) в примере ниже — зло:
// // здесь и далее - псевдокод // void DrawCircle(int x, int y, int radius) { /// рисуем круг с центром в точке (x, y) радиусом radius } … DrawCircle(getScreenMetrics().width / 2, getScreenMetrics().height / 2, 100500); DrawCircle(getScreenMetrics().width / 2, getScreenMetrics().height / 2, 100600);
Самый очевидный выход из положения — вынести координаты центра круга в отдельные переменные:
ScreenMetrics metrics = getScreenMetrics(); int centerX = metrics.width / 2; int centerY = metrics.height / 2; DrawCircle(centerX, centerY, 100); DrawCircle(centerX, centerY, 200);
В примере выше дублирование устраняется за две секунды
getScreenMetrics().width / 2, getScreenMetrics().height / 2
В то же время, далеко не все разработчики умеют распознать и устранить неявное дублирование кода.
Возьмем для примера некое мобильное приложение, которое взаимодействует с API сервера и
позволяет авторизованному пользователю просматривать и загружать фотографии:
HttpResponse login(String email, String password) { HttpRequest request = new HttpRequest(); request.setMethod(“GET”); request.setContentType("application/x-www-form-urlencoded"); String parameters = "?login=" + login + "&password=" + password; String uri = "https://mycoolsite.com/api/login" + parameters; request.setUrl(uri); return request.execute(); } ... HttpResponse getPhotos(int userId) { String uri = "https://mycoolsite.com/api/get_photos?user_id=" + user_id; HttpRequest request = new HttpRequest(); request.setMethod("GET"); request.setUrl(uri); request.setContentType("application/x-www-form-urlencoded"); return request.execute(); } … bool uploadPhoto(Bitmap photo, int user_id) { HttpRequest request = new HttpRequest(); HttpBody body = convertBitmapToHttpBody(photo); request.setUrl("https://mycoolsite.com/api/upload_photo?user_id=" + user_id); request.setMethod("POST"); request.setContentType(“multipart/form-data”); request.setHttpBody(body); HttpResponse response = request.execute(); return (response.getStatusCode()== 200); }
… и еще 100500 подобных методов общим количеством, равным числу вызовов API.
Что произойдет, если mycoolsite.com переедет на awesome.net? Придется искать по всему проекту вызовы request.execute() и править строки. И где-нибудь обязательно забудете подправить URL или просто опечатаетесь. В итоге потратите кучу времени, отправите ближе к ночи “исправленный” билд заказчику, но на следующий день ВНЕЗАПНО получите bug report: “Перестал работать upload фотографий”, например.
Если не писать код сразу “в лоб”, а
- создают экземпляр класса HttpRequest
- выставляют необходимые свойства экземпляру (HTTP method, content type)
- задают URL с параметрами
- опционально: добавляют к телу запросы бинарные данные.
Для начала следует вынести общий код в отдельные методы:
public HttpRequest createGetRequest() { HttpRequest request = new HttpRequest(); request.setMethod("GET"); request.setContentType("application/x-www-form-urlencoded"); return request; } public HttpRequest createPostRequest() { HttpRequest request = new HttpRequest() request.setMethod("POST"); request.setContentType("multipart/form-data"); return request; }
Отдельное внимание стоит обратить на передачу параметров и формирование URI запроса.
В изначальной версии кода используется конкатенация строк:
String uri = "https://mycoolsite.com/api/get_photos?user_id=" + user_id;
То есть, для каждого запроса нужно формировать строку URI, а это тоже дублирование кода
и чревато проблемами при внесении изменений. Корневой адрес API и добавление параметров
к URI следует держать в одном месте.
public static final String API_BASE_URL = "https://mycoolsite.com/api/";
Перед формированием URI запроса соберем все параметры и их значения в Map:
Map params = new HashMap<String, String>(); ... public void addParam(Map<String, String> params, String param, String value) { params.put(param, value); } // Перегрузим метод для передачи целочисленных параметров public void addParam(Map<String, String> params, String param, int value) { params.put(param, String.valueOf(value)); }
Теперь, чтобы сформировать полный URI запроса, напишем следующий код:
public String getUri(String path, Map<String, String> params) { StringBuilder query = new StringBuilder("?"); Iterator iterator = params.allKeys().iterator(); while (iterator.hasNext()) { String paramName = iterator.next(); query.add(paramName + "=" + params.get(paramName)); if (iterator.hasNext()) { query.add("&"); } } return API_BASE_URL + path + result.toString(); }
Итак, чтобы выполнить запрос к серверу, нам нужно:
- создать объект запроса:
HttpRequest request = createGetRequest(); // или createPostRequest, в зависимости от ситуации
- сформировать URI запроса:
Map params = new HashMap<String, String>(); ... addParam(params, "user_id", userId); // предположим, что userId задан ранее String uri = getUri("get_photos"); // сформируется следующая строка: https://mycoolsite.com/api/get_photos?user_id=%user_id%
Теперь можно выполнить запрос к серверу, получить ответ, а дальше сделать с ним всё что угодно:
request.setUrl(uri); HttpResponse response = request.execute();
Теперь вынесем всё это добро в отдельный класс. Также унаследуем этот класс от
HttpRequest, так как в отдельных случаях нам нужно пользоваться его методами.
Получим примерно следующий код:
public class MyRequest extends HttRrequest { private static final String API_BASE_URL = "https://mycoolsite.com/api/"; private Map<String, String> mParameters; private MyRequest() { super(); mParameters = new HashMap<String, String>(); } public static MyRequest createGetRequest() { MyRequest request = new MyRequest(); request.setMethod("GET"); request.setContentType("application/x-www-form-urlencoded"); return request; } public static MyRequest createPostRequest() { MyRequest request = new MyRequest() request.setMethod("POST"); request.setContentType("multipart/form-data"); return request; } public void addParam(String name, String value) { mParameters.put(name, value); } public void addParam(String name, int value) { addParam(name, String.valueOf(value)); } public HttpResponse send(String path) { String uri = API_BASE_URL + path getParametersString(); setUrl(uri); execute(); } private String getParametersString() { StringBuilder result = new StringBuilder("?"); Iterator iterator = mParameters.allKeys().iterator(); while (iterator.hasNext()) { String paramName = iterator.next(); result.add(paramName + "=" + mParameters.get(paramName)); if (iterator.hasNext()) { result.add("&"); } } return result.toString(); } }
Теперь код вызовов API будет выглядеть следующим образом:
HttpResponse login(String login, String password) { MyRequest request = MyRequest.createGetRequest(); request.addParam("login", login); request.addParam("password", password); return request.send("login"); } HttpResponse getPhotos(int userId) { MyRequest request = MyRequest.createGetRequest(); request.addParam("user_id", userId); return request.send("get_photos"); } HttpResponse uploadPhoto(Bitmap photo, int userId) { MyRequest request = MyRequest.createPostRequest(); request.addParam(“user_id”, userId); request.setHttpBody(convertBitmapToHttpBody(photo)); return request.send(“upload_photo”); }
Как видим, методы login, getPhotos и uploadPhoto стали короче, проще и понятнее.
Это был пример нетривиального, многошагового рефакторинга. В итоге появился новый класс MyRequest, являющийся оберткой над системным HttpRequest,
На такой рефакторинг потратится на пару часов больше времени, чем на добавление еще одного вызова API методом копи-паста. Зато в дальнейшем на внесение изменений и отладку будут тратиться считанные минуты, а не дни.
Естественно, получившийся код далеко не идеален, но всё же он гораздо лучше, чем до рефакторинга, а это уже большой
шаг вперед, причем малой кровью. В дальнейшем, при наличии времени, можно и нужно улучшать этот код.
Мораль: заметили дублирование — устраните его немедленно. Сэкономите в будущем кучу времени и нервов.
При этом, устраняя «сложное» дублирование, потратьте время на повторное проектирование небольшой части проекта,
не пишите код сразу.
Подробнее читайте в книжке Фаулера про рефакторинг.
Буду рад конструктивной критике, предложениям и дополнениям к статье. Спасибо за внимание!
