Пример использования функционального программирования во избежание дублирования кода

от
Java    функциональное программирование, лямбды, конфиг, properties, config

Допустим, перед нами стоит задача загрузить конфиг приложения. Есть внутренний конфиг, который хранится в файле app.properties внутри jar-файла, и есть внешний — хранится в пользовательской директории ~/.config/app.conf.
Приложение при запуске читает внешний конфиг. Если какого-то параметра в нём нет, будет браться значение из внутреннего. Если внешнего конфига вообще нет — читается внутренний.

Для чтения внутреннего конфига используется класс ResourceBundle и его метод getString(String key), для внешнего — Properties и метод getProperty(String key, String defaultValue), который как раз поможет установить значение по умолчанию, если оно отсутствует в конфиге.

Можно решить задачу так:
  1. final Config config = new Config(); // сюда будем собирать параметры
  2. final ResourceBundle res = ResourceBundle.getBundle(resourceName); // внутренний конфиг
  3. final Path externalPath = Paths.get(System.getProperty("user.home"), ".config", "app.conf"); // внешний конфиг
  4. try (InputStream extConfig = Files.newInputStream(externalPath)) {
  5.     // Загружаем внешний конфиг
  6.     final Properties props = new Properties();
  7.     props.load(extConfig);
  8.     config.setLocale(props.getProperty("locale", res.getString("locale")));
  9.     config.setToken(props.getProperty("token", res.getString("token")));
  10.     config.setMaxConnections(Integer.parseInt( props.getProperty("max-connections", res.getString("max-connection")) ));
  11. } catch (IOException ex) {
  12.     // Заполняем настройки из внутреннего конфига, раз внешний не удалось загрузить
  13.     config.setLocale(res.getString("locale"));
  14.     config.setToken(res.getString("token"));
  15.     config.setMaxConnections(Integer.parseInt( res.getString("max-connection") ));
  16. }
Код плох тем, что здесь много повторов, а значит нетрудно допустить ошибку или что-то пропустить.

Давайте взглянем на эти две строчки:
config.setLocale(props.getProperty("locale", res.getString("locale")));
config.setLocale(res.getString("locale"));

Отличия лишь в аргументе функции setLocale. То есть, если сделать нечто такое:
config.setLocale(loader.load("locale"));
и создать две реализации объекта loader, то дублирования получится избежать.

Чтобы это сделать, давайте введём функциональный интерфейс.
  1. @FunctionalInterface
  2. interface PropertyLoader {
  3.     String load(String key);
  4. }
Аннотация @FunctionalInterface проверяет на этапе компиляции, что в интерфейсе только один метод, в этом и суть функционального интерфейса.
Теперь мы можем создать две реализации этого интерфейса при помощи лямбда-выражений:
  1. PropertyLoader loader1 = key -> props.getProperty(key, res.getString(key));
  2. PropertyLoader loader2 = key -> res.getString(key);
  3. // или короче, через ссылку на метод:
  4. PropertyLoader loader3 = res::getString;

Следующий шаг — выносим заполнение параметров в новый метод:
  1. private void loadConfig(Config config, PropertyLoader loader) {
  2.     config.setLocale(loader.load("locale"));
  3.     config.setToken(loader.load("token"));
  4.     config.setMaxConnections(Integer.parseInt( loader.load("max-connections") ));
  5. }

А вызываем так:
  1. final Config config = new Config();
  2. final ResourceBundle res = ResourceBundle.getBundle(resourceName);
  3. final Path externalPath = Paths.get(System.getProperty("user.home"), ".config", "app.conf");
  4. try (InputStream extConfig = Files.newInputStream(externalPath)) {
  5.     final Properties props = new Properties();
  6.     props.load(extConfig);
  7.     loadConfig(config, key -> props.getProperty(key, res.getString(key)));
  8. } catch (IOException ex) {
  9.     loadConfig(config, res::getString);
  10. }

Полный код:
1 из 2Презентация
Было
  1. public class ConfigLoader {
  2.  
  3.     private final String resourceName;
  4.     private final Path externalPath;
  5.  
  6.     public ConfigLoader(String resourceName, Path externalPath) {
  7.         this.resourceName = resourceName;
  8.         this.externalPath = externalPath;
  9.     }
  10.  
  11.     public Config load() {
  12.         final Config config = new Config();
  13.         final ResourceBundle res = ResourceBundle.getBundle(resourceName);
  14.         try (InputStream extConfig = Files.newInputStream(externalPath)) {
  15.             final Properties props = new Properties();
  16.             props.load(extConfig);
  17.             config.setLocale(props.getProperty("locale", res.getString("locale")));
  18.             config.setToken(props.getProperty("token", res.getString("token")));
  19.             config.setMaxConnections(Integer.parseInt( props.getProperty("max-connections", res.getString("max-connections")) ));
  20.         } catch (IOException ex) {
  21.             config.setLocale(res.getString("locale"));
  22.             config.setToken(res.getString("token"));
  23.             config.setMaxConnections(Integer.parseInt( res.getString("max-connections") ));
  24.         }
  25.         return config;
  26.     }
  27. }
  • +7
  • views 6188