Повышение читаемости кода и функциональное программирование
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 |