Как спроектировать хороший api и почему это так важно
DESCRIPTION
Перевод презентации "How to Design a Good API and Why it Matters" Joshua BlochTRANSCRIPT
How to Design a Good API and Why it Matters1
Как спроектировать хороший API и почему это так важно
Joshua Blochперевод coxx
How to Design a Good API and Why it Matters2
Почему проектирование API так важно?• API могут быть мощными активами компании
─ Клиенты вкладывают значительные средства в покупку, программирование, обучение
─ Стоимость прекращения использования API может быть слишком высока
─ Успешные публичные API «подсаживают» клиентов
• Также могут быть серьезным грузом для компании─ Плохой API может привести к бесконечному потоку
звонков в службу поддержки
─ Может препятствовать движению вперед
• Публичный API – навсегда. Есть только один шанс сделать все правильно.
How to Design a Good API and Why it Matters3
Почему проектирование API важно для Вас?
• Если Вы программируете – Вы уже проектировщик API─ Хороший код – модульный. Каждый модуль имеет
свой API.
• Полезные модули, как правило, используются повторно─ Как только у модуля появляются пользователи,
нельзя изменить его API по своему желанию─ Хорошие повторно используемые модули – это
активы компании
• Мышление в терминах API повышает качество кода
How to Design a Good API and Why it Matters4
Характеристики хорошего API
• Легко изучать
• Легко использовать, даже без документации documentation
• Трудно использовать неправильно
• Легко читать и поддерживать код, который использует его
• Достаточно мощный чтобы удовлетворять требованиям
• Легко развивать
• Соответствует аудитории
How to Design a Good API and Why it Matters5
Оглавление
I. Процесс проектирования API
II. Основные принципы
III. Проектирование классов
IV. Проектирование методов
V. Проектирование исключений
VI. Рефакторинг API
How to Design a Good API and Why it Matters6
I. Процесс проектирования API
How to Design a Good API and Why it Matters7
Соберите требования – со здоровой долей скептицизма
• Зачастую вам будут предлагать готовые решения (вместо требований)─ Могут существовать лучшие решения
• Ваша задача извлечь истинные требования ─ Должны быть в форме use-cases
• Может быть проще и полезнее решить более общую задачу
Что они говорят: “Нам нужны новые структуры данных и RPC с атрибутами версии 2”
Что они подразумевают: “Нам нужен новый формат данных, который бы позволил развивать набор атрибутов”
How to Design a Good API and Why it Matters8
Начните с краткой спецификации. Одна страница – это идеально.
• На этом этапе гибкость предпочтительнее завершенности
• Разошлите спецификацию стольким людям, скольким это возможно. ─ Прислушайтесь к их отзывам и примите это со
всей серьезностью
• Если Ваша спецификация будет краткой, её легче будет изменять
• Начинайте воплощать, когда обретете уверенность─ Здесь обязательно предполагается кодирование
How to Design a Good API and Why it Matters9
Sample Early API Draft (1)
// A strategy for retrying computation in the face of failure.public interface RetryPolicy { // Called after computation throws exception boolean isFailureRecoverable(Exception e);
// Called after isFailureRecoverable returns true. // Returns next delay in ns, or negative if no more retries. long nextDelay(long startTime, int numPreviousRetries);}
How to Design a Good API and Why it Matters10
Sample Early API Draft (2)
public class RetryPolicies { // Retrying decorators (wrappers) public static ExecutorService retryingExecutorService( ExecutorService es, RetryPolicy policy); public static Executor retryingExecutor( Executor e, RetryPolicy policy); public static <T> Callable<T> retryingCallable( Callable<T> computation, RetryPolicy policy); public static Runnable retryingRunnable( Runnable computation, RetryPolicy policy);
// Delay before nth retry is random number between 0 and 2^n public static RetryPolicy exponentialBackoff( long initialDelay, TimeUnit initialDelayUnit, long timeout, TimeUnit timeoutUnit, Class<? extends Exception>... recoverableExceptions);
public static RetryPolicy fixedDelay(long delay, TimeUnit delayUnit, long timeout, TimeUnit timeoutUnit, Class<? extends Exception>... recoverableExceptions);}
How to Design a Good API and Why it Matters11
Описывайте ваш API как можно раньше и чаще• Начинайте прежде чем Вы реализовали API
─ Это убережет Вас от создания от реализации, которую Вы потом выбросите
• Начинайте даже раньше, чем написали правильные спецификации─ Это убережет Вас от написания спецификаций, которые
Вы потом выбросите
• Продолжайте описывать API по ходу воплощения─ Это убережет Вас от неприятных сюрпризов
непосредственно перед развертыванием
• Код продолжает жить в примерах и модульных тестах─ Это наиболее важный код, который Вы когда-либо писали ─ Формируется основа для Design Fragments
[Fairbanks, Garlan, & Scherlis, OOPSLA ‘06, P. 75]
How to Design a Good API and Why it Matters12
Описание SPI – еще важнее
• Service Provider Interface (SPI)─ Интерфейс плагинов позволяет использовать
множество реализаций─ Пример: Java Cryptography Extension (JCE)
• Напишите несколько плагинов до релиза─ Если один, он скорее всего не будет совместим с
другими─ Если два, возможны проблемы с совместимостью─ Если три, все будет работать, как надо
• Will Tracz называет это “Правило трех”(Confessions of a Used Program Salesman, Addison-Wesley, 1995)
ПлохоХорошо
How to Design a Good API and Why it Matters13
Сохраняйте реалистичные ожидания
• Большинство проектировщиков API перегружены ограничениями─ Вы не можете угодить всем─ Старайтесь нравиться всем в равной степени
• Ожидайте, что совершите ошибки─ Несколько лет использования в реальном мире
выявят их─ Ожидайте, что будете развивать API
How to Design a Good API and Why it Matters14
II. Основные принципы
How to Design a Good API and Why it Matters15
API должен делать что-нибудь одно и делать это хорошо
• Функционал должно быть легко объясним─ Если трудно придумать название – это, как
правило, плохой знак─ Хорошие наименования являются движущей
силой─ С другой стороны, хорошие имена означают, что
вы на верном пути
Хорошо: Font, Set, PrivateKey, Lock, ThreadFactory, TimeUnit, Future
Плохо: DynAnyFactoryOperations, _BindingIteratorImplBase, ENCODING_CDR_ENCAPS, OMGVMCID
How to Design a Good API and Why it Matters16
API должен быть минимальным, но не меньше
• API должен удовлетворять требованиям
• Когда сомневаетесь – оставьте в покое─ Функционал, классы, методы, параметры и т.д.─ Вы всегда можете добавить, но Вы не
сможете убавить
• Концептуальная полнота важнее объема
• Ищите оптимальное соотношение возможностей и полноты (power-to-weight ratio)
How to Design a Good API and Why it Matters17
Реализация не должна влиять на API
• Детали реализации ─ Путают пользователей─ Ограничивают свободу для изменения реализации
• Поймите что такое «детали реализации»─ Не определяйте явно поведение методов
Например: не определяйте хэш-функции─ Все настраиваемые параметры – под подозрением
• Не давайте деталям реализации “утекать” в API─ Например: форматы хранения на диске и
форматы передачи по сети, исключения
How to Design a Good API and Why it Matters18
Минимизируйте доступность ВСЕГО
• Делайте классы и их члены максимально скрытыми
• Публичные классы не должны иметь публичных полей (за исключением констант)
• Максимизируйте сокрытие информации [Parnas]
• Минимизируйте связи
─ Это позволяет понимать, использовать, собирать, тестировать, отлаживать и оптимизировать модули независимо
How to Design a Good API and Why it Matters19
Имена имеют значение – API это маленький язык
• Названия должны быть самоочевидными─ Избегайте загадочных сокращений
• Стремитесь к согласованности─ Одно и то же слово должно означать одну и ту
же вещь по всему API─ (а в идеале, по всем API на платформе)
• Будьте последовательны – стремитесь к гармонии
• Если Вы сделали все правильно, код читается как проза
if (car.speed() > 2 * SPEED_LIMIT) speaker.generateAlert("Watch out for cops!");
How to Design a Good API and Why it Matters20
Документация имеет значение
Повторное использование – это нечто, что гораздо проще сказать, чем сделать. Чтобы достичь этого требуется не только хороший дизайн, но и очень хорошая документация. Даже когда мы видим хороший дизайн, что бывает не часто, мы не увидим повторно используемых компонентов без хорошей документации.
- D. L. Parnas, Software Aging. Proceedingsof the 16th International Conference onSoftware Engineering, 1994
How to Design a Good API and Why it Matters21
Документируйте скрупулёзно(document religiously)
• Документируйте каждый класс, интерфейс, метод, конструктор, параметр и исключение─ Класс: что представляет собой экземпляр─ Метод: контракт между методом и его клиентом
─ Входные условия (preconditions), выходные условия (postconditions), побочные эффекты
─ Параметр: укажите единицы измерения, формат, права доступа владельца
• Очень внимательно документируйте пространство состояний
• Нет оправдания недокументированным элементам API. Точка!
How to Design a Good API and Why it Matters22
Учитывайте, как принимаемые решения влияют на производительность• Плохие решения могут ограничить
производительность─ Использование изменяемых (mutable) типов
─ Использование конструктора вместо статической фабрики (static factory)
─ Использование типов реализации вместо интерфейсных типов
• Не делайте специальных оберток API (do not warp) для увеличения производительности─ Проблема с производительностью, лежащая в основе
будет исправлена, но головная боль останется с вами навсегда
─ Хороший дизайн обычно сопровождается хорошей производительностью
How to Design a Good API and Why it Matters23
Влияние решений по проектированию API на производительность реальна и постоянна• Component.getSize() возвращает Dimension
• Dimension – изменяемый тип (mutable)
• Каждый вызов getSize вынужден создавать Dimension
• Это приводит к миллионам бесполезных созданий объектов
• Альтернативный метод добавлен в 1.2, но старый клиентский код остается медленным
(пример взят из Java AWT)
How to Design a Good API and Why it Matters24
API должен мирно сосуществовать с платформой
• Делайте то, что принято (в платформе)─ Положитесь на стандартные методы
именования─ Избегайте устаревших параметров и
возвращаемых типов─ Подражайте шаблонам в базовом API
платформы и языка─ Используйте с выгодой полезные для API
особенности (API-friendly features): generics, varargs, enums, аргументы по умолчанию
• Знайте и избегайте ловушки и западни API─ Finalizers, public static final arrays
• Не используйте транслитерацию в API
How to Design a Good API and Why it Matters25
III. Проектирование классов
How to Design a Good API and Why it Matters26
Минимизируйте изменяемость (mutability)• Классы должны быть неизменяемы
(immutable) пока не появится достаточной причины сделать обратное─ Плюсы: простота, thread-safe, легкость
повторного использования─ Минусы: отдельные объекты для каждого
значения
• Если класс изменяемый, сохраняйте пространство состояний маленьким и четко определенным─ Проясните, когда какой метод допустимо
вызывать
Bad: Date, Calendar
Good: TimerTask
How to Design a Good API and Why it Matters27
Наследуйте классы только там где это имеет смысл
• Наследование подразумевает взаимозаменяемость─ Используйте наследование только если
существует отношение «is-a» (is every Foo a Bar?)
─ В противном случае используйте композицию
• Публичные классы не должны наследовать другие публичные классы для удобства реализации
Плохо: Properties extends Hashtable Stack extends Vector
Хорошо: Set extends Collection
How to Design a Good API and Why it Matters28
Проектируйте и документируйте с учетом возможного наследования или запретите его• Наследование нарушает инкапсуляцию
(Snyder, ‘86)─ Подклассы чувствительны к деталям реализации
суперкласса
• Если вы разрешаете наследование, документируйте само-использование(?) (self-use)─ Как методы используют друг друга?
• Консервативная политика: все конкретные классы – final.
Плохо: Множество конкретных классов в библиотеках J2SE
Хорошо: AbstractSet, AbstractMap
How to Design a Good API and Why it Matters29
IV. Проектирование методов
How to Design a Good API and Why it Matters30
Не заставляйте клиента делать то, что может сделать модуль
• Уменьшайте необходимость использования шаблонного кода─ Обычно делается через copy-and-paste─ Уродливый, раздражающий и предрасположенный к
ошибкам import org.w3c.dom.*; import java.io.*; import javax.xml.transform.*; import javax.xml.transform.dom.*; import javax.xml.transform.stream.*;
// DOM code to write an XML document to a specified output stream. static final void writeDoc(Document doc, OutputStream out)throws IOException{ try { Transformer t = TransformerFactory.newInstance().newTransformer(); t.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, doc.getDoctype().getSystemId()); t.transform(new DOMSource(doc), new StreamResult(out)); } catch(TransformerException e) { throw new AssertionError(e); // Can’t happen! } }
How to Design a Good API and Why it Matters31
Не нарушайте принцип «наименьшего изумления»
• Пользователь API не должен удивляться поведению─ Это стоит дополнительных усилий при реализации─ Это стоит даже снижения производительности
public class Thread implements Runnable { // Проверяет, была ли нить прервана. // Очищает статус interrupted у данной нити. public static boolean interrupted(); }
Вот особенно вопиющий пример того, чего не стоит делать.
How to Design a Good API and Why it Matters32
Падай быстро – сообщайте об ошибках как можно скорее
• Лучше всего во время компиляции – статическая типизация, generics.
• В время выполнения – лучше всего первый вызов ошибочного метода.─ Метод должен быть failure-atomic
// A Properties instance maps strings to strings public class Properties extends Hashtable { public Object put(Object key, Object value);
// Throws ClassCastException if this properties // contains any keys or values that are not strings public void save(OutputStream out, String comments); }
How to Design a Good API and Why it Matters33
Предоставьте программный доступ ко всем данным, доступным в виде строк• В противном случае клиентам придется
анализировать строки─ Мучительно для клиентов─ Хуже того, это де-факто превращает формат строк в
часть API
public class Throwable { public void printStackTrace(PrintStream s); public StackTraceElement[] getStackTrace(); // Since 1.4}
public final class StackTraceElement { public String getFileName(); public int getLineNumber(); public String getClassName(); public String getMethodName(); public boolean isNativeMethod();}
How to Design a Good API and Why it Matters34
Используйте перегрузку методов с осторожностью(Overload With Care)
• Избегайте неоднозначных перегрузок─ Multiple overloadings applicable to same actuals─ Консервативный подход: нет двух (методов или
функций) с одним и тем же числом аргументов
• Просто «потому что Вы можете» не означает, что «Вы должны»─ Часто лучше просто использовать другое имя
• Если Вам необходимо сделать неоднозначные перегрузки, обеспечьте одинаковое поведение для одинаковых аргументов
public TreeSet(Collection c); // Ignores orderpublic TreeSet(SortedSet s); // Respects order
How to Design a Good API and Why it Matters35
Используйте подходящие типы параметров и возвращаемых значений• Отдавайте предпочтение интерфейсным типам перед
классами для входных параметров─ Обеспечивает гибкость и улучшает производительность
• Используйте наиболее конкретный тип для аргументов─ Перемещает ошибки с времени выполнения на время
компиляции
• Не используйте строки если существует лучший тип─ Строки громоздки, медленны и часто приводят к ошибкам
• Не используйте плавающую точку для денежных значений─ Двоичная плавающая точка приводит к неточным
результатам!
• Используйте double (64 бита) вместо float (32 бита)─ Потеря точности существенна, потеря
производительности незначительна
How to Design a Good API and Why it Matters36
Используйте один и тот же порядок параметров во всех методах
• Особенно важно если типы параметров одинаковы
#include <string.h> char *strncpy(char *dst, char *src, size_t n);
void bcopy (void *src, void *dst, size_t n);
java.util.Collections – первый параметр всегда коллекция которая будет изменена или к которой делается запрос
java.util.concurrent – время всегда задается как long delay, TimeUnit unit
How to Design a Good API and Why it Matters37
Избегайте длинных списков параметров• Три или меньше параметров – это идеально
─ Сделайте больше и пользователю придется смотреть в документацию
• Длинные списки параметров с одинаковыми типами – вредны─ Программисты по ошибке сдвигают параметры─ Программа продолжает собираться, запускаться, но
работает неверно!
• Техники сокращения списка параметров─ Разбейте метод на части─ Создайте вспомогательный класс, содержащий
параметры
// Одинадцать параметров включая четыре последовательных intHWND CreateWindow(LPCTSTR lpClassName, LPCTSTR lpWindowName, DWORD dwStyle, int x, int y, int nWidth, int nHeight, HWND hWndParent, HMENU hMenu, HINSTANCE hInstance, LPVOID lpParam);
How to Design a Good API and Why it Matters38
Избегайте возвращать значения которые требуют особой обработки
• Возвращайте массив нулевой длины или пустое множество, а не null
package java.awt.image; public interface BufferedImageOp { // Returns the rendering hints for this operation, // or null if no hints have been set. public RenderingHints getRenderingHints(); }
How to Design a Good API and Why it Matters39
V. Проектирование исключений
How to Design a Good API and Why it Matters40
Выбрасывайте исключения только чтобы сигнализировать об исключительной ситуации
• Не заставляйте клиента использовать исключения для управления потоком выполнения
private byte[] a = new byte[BUF_SIZE]; void processBuffer (ByteBuffer buf) { try { while (true) { buf.get(a); processBytes(tmp, BUF_SIZE); } } catch (BufferUnderflowException e) { int remaining = buf.remaining(); buf.get(a, 0, remaining); processBytes(bufArray, remaining); } }
• С другой стороны, не «падайте» молча ThreadGroup.enumerate(Thread[] list)
How to Design a Good API and Why it Matters41
Отдавайте предпочтение Unchecked Exceptions• Checked – клиент должен предпринять меры
по устранению
• Unchecked – программная ошибка
• Чрезмерное использование checked exceptions приводит к шаблонному (copy-paste) программированию
try { Foo f = (Foo) super.clone(); ....} catch (CloneNotSupportedException e) { // This can't happen, since we’re Cloneable throw new AssertionError();}
How to Design a Good API and Why it Matters42
Включайте Failure-Capture информацию в исключения
• Предоставляет возможность для диагностики и восстановления
• Для unchecked exceptions достаточно сообщения
• Для checked exceptions предоставьте акцессор (метод, получающий текущее значение свойства)
How to Design a Good API and Why it Matters43
VI. Рефакторинг API
How to Design a Good API and Why it Matters44
1. Списковые операции над Vector-ом
public class Vector { public int indexOf(Object elem, int index); public int lastIndexOf(Object elem, int index); ...}
• Не очень мощно – поддерживается только поиск
• Трудно использовать без документации
How to Design a Good API and Why it Matters45
Sublist операции после рефакторинга
public interface List { List subList(int fromIndex, int toIndex); ...}
• Чрезвычайно мощно – поддерживаются все операции
• Использование интерфейса уменьшает концептуальный вес─ Высокое отношение мощность-вес
• Легко использовать без документации
How to Design a Good API and Why it Matters46
2. Thread-Local переменные(Thread-Local Variables)
// Broken - inappropriate use of String as capability. // Keys constitute a shared global namespace. public class ThreadLocal { private ThreadLocal() { } // Non-instantiable
// Sets current thread’s value for named variable. public static void set(String key, Object value);
// Returns current thread’s value for named variable. public static Object get(String key); }
How to Design a Good API and Why it Matters47
Thread-Local Variables Refactored (1)
public class ThreadLocal { private ThreadLocal() { } // Noninstantiable
public static class Key { Key() { } }
// Generates a unique, unforgeable key public static Key getKey() { return new Key(); }
public static void set(Key key, Object value); public static Object get(Key key); }
• Работает, но требует использования шаблонного кода
static ThreadLocal.Key serialNumberKey = ThreadLocal.getKey(); ThreadLocal.set(serialNumberKey, nextSerialNumber()); System.out.println(ThreadLocal.get(serialNumberKey));
How to Design a Good API and Why it Matters48
Thread-Local Variables Refactored (2)
public class ThreadLocal<T> { public ThreadLocal() { } public void set(T value); public T get();
}
• Устраняет беспорядок в API и клиентском коде
static ThreadLocal<Integer> serialNumber = new ThreadLocal<Integer>();
serialNumber.set(nextSerialNumber()); System.out.println(serialNumber.get());
How to Design a Good API and Why it Matters49
Заключение
• Проектирование API – это благородное и полезное дело─ Развивает программистов, конечных
пользователей, компании
• Это выступление охватывает некоторые хитрости этого ремесла─ Не будьте их рабами, но...─ не нарушайте их без особой на то причины
• Проектировать API трудно─ Это занятие не для одного человека─ Совершенство недостижимо, но Вы все равно
попробуйте
How to Design a Good API and Why it Matters50
Shameless Self-Promotion
“Bumper-Sticker API Design” - P. 506 in OOPSLA ‘06 Program
How to Design a Good API and Why it Matters51
Как спроектировать хороший API и почему это так важно
Joshua Bloch