menu

Повышение читаемости кода и функциональное программирование Часть 1

Повышение читаемости кода и функциональное программирование

Автор: Евгений Кирпичев aka jkff

DISCLAIMER:

Большая часть данной статьи не имеет отношения к собственно функциональному программированию (далее – ФП). В основном будут рассмотрены способы повышения читаемости некоторых часто встречающихся паттернов, особенно часто встречающихся при использовании функционального стиля, и без которых об ФП не может быть и речи. О приемах собственно ФП будет сказано совсем немного, ближе к концу статьи.

Вот некоторые из недостатков Java, с которыми мы будем бороться:

  • "kingdom of nouns" (традиция считать все на свете существительным и считать, что вычислить сумму двух чисел можно, только создав экземпляр ЧисленногоСумматора и любезно послав ему сообщение). Этот недостаток порождает громоздкий код, переполненный лишними factory- и builder-классами, скрывающий логику работы программы – кажется, что код, собственно говоря, ничего не делает – только создает бесконечные FooFactory, BarProcessor и GazonkProvider'ы.
  • Отсутствие встроенного синтаксиса для конструирования и деконструирования (разбора на составные части) данных – пар и кортежей, списков и т.п. Этот недостаток приводит к загромождению программы кодом, создающим данные – последовательностями List.add() или Map.put(), и – что менее заметно, но более вредно – закрывает дорогу к обильному использованию составных данных в коде и в API – например, разработчик библиотеки для работы с регулярными выражениями может воздержаться от добавления полезного оптимизированного метода «выполнить сразу несколько поисков-и-замен» с аргументом «список пар шаблон/замена», поскольку вызов этого метода потребует громоздкого кода для создания такого списка.
  • Плохой синтаксис generic-ов, отсутствие полноценных вывода типов и полиморфизма. Этот недостаток делает нечитаемыми сколько-нибудь сложные параметризованные типы и выражения с ними, а также часто заставляет писать дублирующийся код (сложность выражения «List<Pair<String,Integer>> list = new ArrayList<Pair<String,Integer>>» гораздо выше реальной сложности сущности «список пар из строки и числа»; отсутствие вывода типов для конструкторов – самое яркое проявление этого недостатка).

В качестве орудий пригодятся такие возможности Java 5, как import static, varargs – и немного пороха в пороховницах.

Приемы будут даны в виде серии сравнений «типичный код vs. хороший код» на коротких примерах из разных областей. Одна из целей статьи – показать, что эти приемы применимы в любой области.

Итак, приступим.

Увеличиваем читаемость данных

Совсем плохо:

private static final Set<String> INTERESTING_TAGS = 
 new HashSet<String>();
Map<String,Integer> WEIGHTS = new HashMap<String,Integer>

static {
 INTERESTING_TAGS.add("A");
 INTERESTING_TAGS.add("FORM");
 INTERESTING_TAGS.add("INPUT");
 INTERESTING_TAGS.add("SCRIPT");
 INTERESTING_TAGS.add("OBJECT");

 WEIGHTS.put("bad", -2);
 WEIGHTS.put("poor", -1);
 WEIGHTS.put("average", 0);
 WEIGHTS.put("nice", 1);
 WEIGHTS.put("outstanding", 3);
}

Плохо:

private static final Set<String> INTERESTING_TAGS = 
 new HashSet<String>(Arrays.asList(new String[] 
 {
 "A","FORM","INPUT","SCRIPT","OBJECT"
 }));


for(int coinValue : new int[] {1, 2, 5, 10, 20, 50, 100}) 
{
 ...
}

Хорошо:

public class CollectionUtils 
{
 public static T[] ar(T... ts) {return ts;}

 public static Set<T> set(T... ts) 
 {
 return new HashSet<T>(Arrays.asList(ts));
 }

 public static Map<K,V> zipMap(K[] keys, V[] values) {...}
}

import static CollectionUtils.set;
import static CollectionUtils.ar;

private static final Set<String> INTERESTING_TAGS = 
 set("A","FORM","INPUT","SCRIPT","OBJECT");

for(int coinValue : ar(1, 2, 5, 10, 20, 50, 100)) {
 ...
}

Map<String,Integer> WEIGHTS = zipMap(
 ar("bad", "poor", "average", "nice", "outstanding"),
 ar(-2, -1, 0, 1, 3));

Казалось бы, как просто! Обыкновенная абстракция конструкторов данных, вторая глава классической библии программирования «Структура и интерпретация компьютерных программ» Абельсона и Сассмана.

Особенно ярко полезность упрощенного синтаксиса для создания данных проявляется в юнит-тестах, когда есть необходимость сконструировать для программы множество входных примеров.

В этом случае очень часто оправдано даже кодирование структур в строке и написание маленького парсера. Например:

assertTrue(GraphAnalyzer.isConnected(graph("1->2 2->3 3->1")));

Легко представить себе, как выглядела бы эта строчка без абстракции конструктора – последовательность конструкторов, объявлений переменных и вызовов вида .addVertex() и .addEdge().

Отсутствие объявляемых временных переменных, засоряющих область видимости – одно из основных преимуществ этих приемов.

Увеличиваем читаемость комбинаторов

Плохо:

Filter f = new AndFilter(first, second);

Хорошо:

public abstract class Filters 
{
 public static Filter and(Filter a, Filter b) {return new AndFilter(a,b);}
}

import static Filters.*;

Filter f = and(first,second);

Еще лучше:

public abstract class Filter 
{
 Filter and(Filter other) {return Filters.and(this,other);}
}
Filter f = first.and(second).and(third);

Совсем хорошо:

public abstract class Filters 
{
 public static Filter ALWAYS_TRUE = new AlwaysTrue();

 public static Filter and(Filter... filters) 
 {
 Filter res = ALWAYS_TRUE;
 for(Filter f : filters) res = res.and(f);
 return res;
 }
}

Filter f = and(first, second, third);

Эта серия выглядит парадоксально – размер кода увеличивается от плохого кода к хорошему. Однако существенно лишь то, что клиентский код становится все более читаемым – все «лишнее» переносится в библиотечный код, раздуваясь в размерах, но становясь и более общим.

От клиентского кода требуется мгновенная читаемость, от библиотечного – читаемость при необходимости.

Еще один пример:

Плохо:

enum StringComparisonKind {EXACT, REGEX, GLOB}
enum StringPosition {ANYWHERE, WHOLE_STRING, STARTS_WITH, ENDS_WITH}
public class StringCondition {
 ...

 public StringCondition(
 String pattern, StringComparisonKind comparisonKind, 
 StringPosition position) {...}
}

conditions.add(new StringCondition(
 "foo", StringComparisonKind.REGEX, StringPosition.ANYWHERE))

Хорошо:

import static StringComparisonKind.*;
import static StringPosition.*;
public class StringConditions {
 public static regexWhole(String regex) {
 return new StringCondition(regex, REGEX, WHOLE_STRING);
 }
 public static regexAnywhere(String regex) {
 return new StringCondition(regex, REGEX, ANYWHERE);
 }
 public static exactWhole(String pattern) {
 return new StringCondition(pattern, EXACT, WHOLE_STRING);
 }
 ...
}


import static StringConditions.*;
conditions.add(regexAnywhere("foo"));

Абстракция, абстракция и еще раз абстракция. Удивительно, насколько ее обычно недооценивают.

Увеличиваем читаемость анонимных классов

Анонимные классы в Java – бледная замена замыканиям и анонимным функциям из функциональных языков, но при этом они, по крайней мере, обладают такой же мощностью и полезностью. Поэтому особенно остра необходимость увеличить их читаемость. Для этого стоит выносить их в константы или хотя бы локальные переменные:

Плохо:

List<Order> orders = CollectionUtils.flatten(CollectionUtils.map(
 customers, new Function<Customer, List<Order>>() 
{
 public List<Order> apply(Customer customer) 
 {
 return customer.getOrders();
 }
}));

Хорошо:

import static CollectionUtils.*;

private static final Function<Customer, List<Order>> GET_ORDERS = 
 new Function<Customer, List<Order>>() 
{
 public List<Order> apply(Customer customer) 
 {
 return customer.getOrders();
 }
};

List<Order> orders = flatten(map(customers, GET_ORDERS));

(к сожалению, судя по всему, не существует способа избавиться от дублирования аргументов generic-ов).

По сути, это еще один пример переноса сложности и нечитаемости в библиотечный код – однако различие библиотечного и клиентского кода в данном случае более зыбкое.

Увеличиваем читаемость имен классов

Плохо:

CustomerProcessor taxes = new ComputeTaxesCustomerProcessor();

Эти суффиксы не дают вообще ничего. Если бы у Java не было пакетов (package) или статической типизации, то это было бы оправдано, чтобы не засорять глобальный namespace или случайно не перепутать один And с другим. Но они есть, и суффиксы не нужны – так же, как, например, венгерская нотация.

Хорошо:

CustomerProcessor taxes = new ComputeTaxes();


Source: http://www.rsdn.ru/mag/main.htm
Category: Java | Added by: tsvetkov (27.01.2009)
Views: 1488 | Rating: 0.0/0