Основы борьбы с неявным дублированием кода

в 8:04, , рубрики: Программирование, проектирование, Проектирование и рефакторинг, разработка, рефакторинг, метки: , ,

Код с одной и той же структурой в двух и более местах — верный признак необходимости рефакторинга. Если вам нужно будет что-нибудь изменить в одном месте, то, скорее всего, нужно также сделать то же самое и в других местах. Но есть близкая к 100% вероятность не найти эти места или попросту забыть за них.

Большинство понимает, что многократное повторение кода (или 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. Добавить новый вызов не составит практически никакого труда.

На такой рефакторинг потратится на пару часов больше времени, чем на добавление еще одного вызова API методом копи-паста. Зато в дальнейшем на внесение изменений и отладку будут тратиться считанные минуты, а не дни.

Естественно, получившийся код далеко не идеален, но всё же он гораздо лучше, чем до рефакторинга, а это уже большой
шаг вперед, причем малой кровью. В дальнейшем, при наличии времени, можно и нужно улучшать этот код.

Мораль: заметили дублирование — устраните его немедленно. Сэкономите в будущем кучу времени и нервов.
При этом, устраняя «сложное» дублирование, потратьте время на повторное проектирование небольшой части проекта,
не пишите код сразу.

Подробнее читайте в книжке Фаулера про рефакторинг.

Буду рад конструктивной критике, предложениям и дополнениям к статье. Спасибо за внимание!

Автор: realsugar

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js