Всем привет.
В ответ на комментарии к прошлой статье, я пишу эту статью о том, что мне кажется читаемым кодом, и как я научился таковой писать.
Сразу скажу, что я не собираюсь перепечатывать рекомендации Макконнелла — У него написано пол-книги о методиках наименования методов, констант, переменных, классов, интерфейсов и прочего. Я опишу более общий подход к читаемому коду, который для себя выводил сам долгое время(что-то, конечно, было подсмотрено в коде у более опытных коллег). Рекомендации относятся к объектно-ориентированному коду в первую очередь.
Общий принцип
Простой код просто читать, непонятный код читать сложно(спасибо, кэп!). Путь к простоте — абстракции и ясность наименования. Таким образом, кроме понятных имен переменных и методов, нужно чтобы
в каждом структурном элементе сохранялся один уровень абстракции.
Это значит, что в каждом методе нужно выбрать один уровень абстракции и строго его соблюдать. Класс должен формировать законченную абстракцию. Пакет должен соблюдать законченную абстракцию. Библиотека… угадаете сами?
Подпринцип 1:
Пишите сначала не код, а комментарии, которые описывают, что делает код.
Подпринцип 2:
Пишите код так, как будто это сухой английский текст.
Читаемый код в методах
Итак, например, нам нужно написать код, который парсит rss-ленту от фликра. Этот класс я писал в рамках одного тестового задания. Полный код его можно найти тут. В нем есть пяток хороших с точки зрения читаемости методов, например этот:
/**
* Reading String from a stream.
*
* @param is stream to read
* @return parsed String
* @throws FlickrFeedException in case of I/O problems
*/
private static String convertStreamToString(InputStream is) throws FlickrFeedException {
if (is == null) {
throw new IllegalArgumentException("Stream for JSON can not be null");
}
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
StringBuilder sb = new StringBuilder();
String line;
try {
while ((line = reader.readLine()) != null) {
sb.append(line).append("n");
}
} catch (IOException e) {
Log.e(TAG, "Error reading input stream", e);
throw new FlickrFeedException("Failed to read input stream");
} finally {
close(is);
}
return sb.toString();
}
Метод знает лишь то, что работает с неким потоком, который нужно сконвертировать в строку. Единственное, что здесь немного выбивается из абстракции — это FlickrFeedException. Мы даем методу знать, что вообще говоря, он исполняется в контектсе работы с flickr. Но это допустимый компромисс — в случае, если метод будет нужен в другом месте, мы просто сменим исключение на более общее.
А вот метод(да, я знаю, что это конструктор) в том же классе похуже:
public JsonReader(String src) throws FlickrFeedException {
try {
String fixed = src.replaceFirst("jsonFlickrFeed\(", "");
JSONObject jsonObject = new JSONObject(fixed);
JSONArray array = jsonObject.getJSONArray("items");
final int itemsCount = array.length();
Log.i(TAG, "array size: " + itemsCount);
feed = new Feed();
// TODO : fill feed data
feed.setItems(new ArrayList<Entry>(itemsCount));
final List<Entry> items = feed.getItems();
for (int i = 0; i < itemsCount; i++) {
final JSONObject itemObject = (JSONObject) array.get(i);
Entry entry = new Entry();
entry.setAuthor(itemObject.getString("author"));
entry.setLink(itemObject.getString("link"));
entry.setAuthorId(itemObject.getString("author_id"));
entry.setDescription(itemObject.getString("description"));
entry.setMediaUrl(getMediaUrl(itemObject));
entry.setTaken(getDate(itemObject, "date_taken", FORMAT_TIMEZONE));
entry.setPublished(getDate(itemObject, "published", FORMAT));
items.add(entry);
}
} catch (JSONException e) {
throw new FlickrFeedException("Failed reading json stream", e);
}
}
Чем же он хуже предыдущего?
1. У него отсутствует javadoc, поясняющимй не что делает метод, а зачем.
2. Методу совершенно необязательно знать, под какими ключами и на каком уровне хранится интересующая нас информация
3. Методу абсолютно точно не нужно знать структуру Entry.
Итак, исправляем недостатки:
/**
* Reader constructor. Performs parsing of json-string to store {@link Feed} object inside
* for further access.
*
* @param src json-string
* @throws FlickrFeedException in case of any problems
*/
public JsonReader(String src) throws FlickrFeedException {
try {
JSONArray array = extractItemArray(src);
final int itemsCount = array.length();
Log.i(TAG, "array size: " + itemsCount);
feed = new Feed();
feed.setItems(new ArrayList<Entry>(itemsCount));
final List<Entry> items = feed.getItems();
for (int i = 0; i < itemsCount; i++) {
final JSONObject itemObject = (JSONObject) array.get(i);
Entry entry = readEntry();
items.add(entry);
}
} catch (JSONException e) {
throw new FlickrFeedException("Failed reading json stream", e);
}
}
Теперь уровень абстракции везде одинаков. Мы знаем, что мы как-то достаем из JSON-строки набор неких объектов, но структуру этой строки и вид этих объектов остается ниже абстракции. На самом деле, мы могли бы и весь цикл вынести в метод, но это уже необязательно — метод достаточно мал и читаем. Кстати, мы только что воспользовались простейшим рефакторингом, который поддерживается в современных java-ide по комбинации горячих клавиш. Если вы им не пользуетесь до сих пор — самое время начать.
Читаемый код классов
Классы — это уровень выше методов. Если в методах элементами, формирующими абстракцию, выступали операторы и вызовы методов, то в классе вашими элементами становятся видимые снаружи класса методы. Сделайте обзор публичных методов(ide вам в помощь) и оцените, насколько ясно предназначение класса.
Посмотрим на другой класс из того же проекта: ImageLoader.
Он хорош тем, что у него всего один метод и конструктор, видимые снаружи. Сразу ясно, что для загрузки картинок в ListView, его надо создать, передав собственно ListView, Handler, а затем вызывать методы addImage для загрузки очередной картинки. Класс формирует законченную абстракцию загрузчика картинок на UI.
Законченная абстракция на уровне public-методов — необходимое, но недостаточное условие. И опять же, пример похуже: здесь.
Чувствуете разницу? Публичных методов на порядок больше.
Если коротко, то это элемент представления, который умеет рисовать карту из загруженных тайлов. Ошибка читаемости здесь в том, что методы draw* необходимо вынести в отдельный класс-помощник, дабы оставить для View только инфраструктурную логику.
Читаемый код пакетов
Элементами абстракции на уровне пакетов выступают имена классов. Если их немного и их имена вместе формируют некую мысль, то мы достигли цели. Хороший пакет выглядит так:
В нем мало классов, у него ясное имя — работа с базой данных, предназначение классов тоже угадывается — базу нужно проинициализровать, а затем работать с каждой таблицей через класс-фасад к ней. Согласен, что термин «фасад» может быть не идеальным здесь.
Важным свойством организации пакета(как и любого другого структурного элемента) является удобство рефакторинга. Как только в пакете наберется большое количество классов для работы с таблицами, мы рассуем их по вложенным пакетам в соответствии с предназначением. Т.е. вот такого у нас быть не должно:
Классов много, предназначение весьма условное — какие-то утилиты, однородности классов в плане задачи не наблюдается. К чему нужно придти — смотри выше.
Еще несколько общих советов.
1. Кроме выноса методов в ваших любимых IDE есть функции выноса переменных, констант, членов класса и т.д. Пользуйтесь ими.
2. Используйте наследование с умом. Читаемый код — простой код. Наследование не всегда приводит к простому коду, если присутствует иерархия более чем из 3 классов. Подумайте об альтернативах, композиции, например.
3. Выносите интерфейсы. Иногда все таки встречается довольно большой объект, который нужно передать для работы в другой. Так вот, чтобы не отягощать знаниями второй объект, а предоставить только нужные ему функции, из первого можно вынести интерфейс из первого объекта только с той функциональностью, которая необходима второму.
На этом пока все, спасибо за внимание.
Автор: dzigoro