Как я пишу читаемый код

в 6:54, , рубрики: code, java, Программирование, Совершенный код, метки: ,

Всем привет.

В ответ на комментарии к прошлой статье, я пишу эту статью о том, что мне кажется читаемым кодом, и как я научился таковой писать.

Сразу скажу, что я не собираюсь перепечатывать рекомендации Макконнелла — У него написано пол-книги о методиках наименования методов, констант, переменных, классов, интерфейсов и прочего. Я опишу более общий подход к читаемому коду, который для себя выводил сам долгое время(что-то, конечно, было подсмотрено в коде у более опытных коллег). Рекомендации относятся к объектно-ориентированному коду в первую очередь.

image

Общий принцип

Простой код просто читать, непонятный код читать сложно(спасибо, кэп!). Путь к простоте — абстракции и ясность наименования. Таким образом, кроме понятных имен переменных и методов, нужно чтобы

в каждом структурном элементе сохранялся один уровень абстракции.

Это значит, что в каждом методе нужно выбрать один уровень абстракции и строго его соблюдать. Класс должен формировать законченную абстракцию. Пакет должен соблюдать законченную абстракцию. Библиотека… угадаете сами?

Подпринцип 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

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js