Спроектировать и реализовать класс-сервис для сокращения URL-адресов

7. Спроектировать и реализовать сервис сокращения URL

Условие задачи:
Необходимо спроектировать и реализовать простой сервис сокращения URL.

Сервис должен уметь:

  • принимать длинный URL и возвращать короткий;

  • сохранять соответствие между коротким идентификатором и исходным URL;

  • по короткому URL возвращать исходный;

  • гарантировать уникальность создаваемых коротких идентификаторов.

Для учебной реализации данные можно хранить в памяти.


Спойлеры к решению

Подсказки
💡 Для хранения соответствия shortCode → originalUrl можно использовать Map.
💡 Случайная генерация кода требует отдельно обрабатывать коллизии.
💡 Более простой вариант — генерировать уникальный числовой ID и кодировать его в Base62.
💡 Если один и тот же URL сокращается несколько раз, можно возвращать уже существующий короткий URL.
💡 Для многопоточного использования обычного HashMap недостаточно.

Решение

Определим интерфейс сервиса:

public interface UrlShortenerService {

    String shorten(String originalUrl);

    Optional<String> resolve(String shortUrl);
}

Простая потокобезопасная реализация в памяти:

public class InMemoryUrlShortenerService implements UrlShortenerService {

    private static final String BASE_URL = "https://short.ly/";
    private static final char[] BASE62 =
            "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
                    .toCharArray();

    private final AtomicLong sequence = new AtomicLong();

    private final Map<String, String> codeToUrl =
            new ConcurrentHashMap<>();

    private final Map<String, String> urlToCode =
            new ConcurrentHashMap<>();

    @Override
    public String shorten(String originalUrl) {
        Objects.requireNonNull(originalUrl);

        String code = urlToCode.computeIfAbsent(
                originalUrl,
                url -> {
                    String newCode = encode(sequence.incrementAndGet());
                    codeToUrl.put(newCode, url);
                    return newCode;
                }
        );

        return BASE_URL + code;
    }

    @Override
    public Optional<String> resolve(String shortUrl) {
        Objects.requireNonNull(shortUrl);

        if (!shortUrl.startsWith(BASE_URL)) {
            return Optional.empty();
        }

        String code = shortUrl.substring(BASE_URL.length());

        return Optional.ofNullable(codeToUrl.get(code));
    }

    private String encode(long value) {
        StringBuilder result = new StringBuilder();

        while (value > 0) {
            int remainder = (int) (value % BASE62.length);
            result.append(BASE62[remainder]);
            value /= BASE62.length;
        }

        return result.reverse().toString();
    }
}

Пример использования:

public static void main(String[] args) {
    UrlShortenerService service =
            new InMemoryUrlShortenerService();

    String shortUrl = service.shorten(
            "https://example.com/some/very/long/url"
    );

    System.out.println(shortUrl);

    Optional<String> originalUrl =
            service.resolve(shortUrl);

    System.out.println(originalUrl.orElse("URL not found"));
}

Основное хранилище:

Map<String, String> codeToUrl

хранит соответствие:

короткий код → исходный URL

Обратная карта:

Map<String, String> urlToCode

позволяет при повторном сокращении того же URL вернуть уже существующий код.

Для создания короткого идентификатора используется последовательность:

sequence.incrementAndGet()

и преобразование числа в Base62:

1  → 1
61 → Z
62 → 10
63 → 11
...

Такой подход не требует случайной генерации и проверки коллизий: каждое новое значение счётчика уникально.

Для хранения используются ConcurrentHashMap, а для генерации ID — AtomicLong, поэтому базовая реализация подходит для вызовов из нескольких потоков.

В реальном сервисе вместо хранения данных в памяти понадобилось бы постоянное хранилище с уникальным ограничением на короткий код. Также обычно добавляют срок жизни ссылок, статистику переходов и распределённую генерацию идентификаторов.