Как спроектировать хороший api и почему это так важно

51
How to Design a Good API and Why it Matters 1 Как спроектировать хороший API и почему это так важно Joshua Bloch перевод coxx

Upload: bubon-makabra

Post on 28-Nov-2014

3.458 views

Category:

Technology


1 download

DESCRIPTION

Перевод презентации "How to Design a Good API and Why it Matters" Joshua Bloch

TRANSCRIPT

Page 1: Как спроектировать хороший API и почему это так важно

How to Design a Good API and Why it Matters1

Как спроектировать хороший API и почему это так важно

Joshua Blochперевод coxx

Page 2: Как спроектировать хороший API и почему это так важно

How to Design a Good API and Why it Matters2

Почему проектирование API так важно?• API могут быть мощными активами компании

─ Клиенты вкладывают значительные средства в покупку, программирование, обучение

─ Стоимость прекращения использования API может быть слишком высока

─ Успешные публичные API «подсаживают» клиентов

• Также могут быть серьезным грузом для компании─ Плохой API может привести к бесконечному потоку

звонков в службу поддержки

─ Может препятствовать движению вперед

• Публичный API – навсегда. Есть только один шанс сделать все правильно.

Page 3: Как спроектировать хороший API и почему это так важно

How to Design a Good API and Why it Matters3

Почему проектирование API важно для Вас?

• Если Вы программируете – Вы уже проектировщик API─ Хороший код – модульный. Каждый модуль имеет

свой API.

• Полезные модули, как правило, используются повторно─ Как только у модуля появляются пользователи,

нельзя изменить его API по своему желанию─ Хорошие повторно используемые модули – это

активы компании

• Мышление в терминах API повышает качество кода

Page 4: Как спроектировать хороший API и почему это так важно

How to Design a Good API and Why it Matters4

Характеристики хорошего API

• Легко изучать

• Легко использовать, даже без документации documentation

• Трудно использовать неправильно

• Легко читать и поддерживать код, который использует его

• Достаточно мощный чтобы удовлетворять требованиям

• Легко развивать

• Соответствует аудитории

Page 5: Как спроектировать хороший API и почему это так важно

How to Design a Good API and Why it Matters5

Оглавление

I. Процесс проектирования API

II. Основные принципы

III. Проектирование классов

IV. Проектирование методов

V. Проектирование исключений

VI. Рефакторинг API

Page 6: Как спроектировать хороший API и почему это так важно

How to Design a Good API and Why it Matters6

I. Процесс проектирования API

Page 7: Как спроектировать хороший API и почему это так важно

How to Design a Good API and Why it Matters7

Соберите требования – со здоровой долей скептицизма

• Зачастую вам будут предлагать готовые решения (вместо требований)─ Могут существовать лучшие решения

• Ваша задача извлечь истинные требования ─ Должны быть в форме use-cases

• Может быть проще и полезнее решить более общую задачу

Что они говорят: “Нам нужны новые структуры данных и RPC с атрибутами версии 2”

Что они подразумевают: “Нам нужен новый формат данных, который бы позволил развивать набор атрибутов”

Page 8: Как спроектировать хороший API и почему это так важно

How to Design a Good API and Why it Matters8

Начните с краткой спецификации. Одна страница – это идеально.

• На этом этапе гибкость предпочтительнее завершенности

• Разошлите спецификацию стольким людям, скольким это возможно. ─ Прислушайтесь к их отзывам и примите это со

всей серьезностью

• Если Ваша спецификация будет краткой, её легче будет изменять

• Начинайте воплощать, когда обретете уверенность─ Здесь обязательно предполагается кодирование

Page 9: Как спроектировать хороший API и почему это так важно

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);}

Page 10: Как спроектировать хороший API и почему это так важно

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);}

Page 11: Как спроектировать хороший API и почему это так важно

How to Design a Good API and Why it Matters11

Описывайте ваш API как можно раньше и чаще• Начинайте прежде чем Вы реализовали API

─ Это убережет Вас от создания от реализации, которую Вы потом выбросите

• Начинайте даже раньше, чем написали правильные спецификации─ Это убережет Вас от написания спецификаций, которые

Вы потом выбросите

• Продолжайте описывать API по ходу воплощения─ Это убережет Вас от неприятных сюрпризов

непосредственно перед развертыванием

• Код продолжает жить в примерах и модульных тестах─ Это наиболее важный код, который Вы когда-либо писали ─ Формируется основа для Design Fragments

[Fairbanks, Garlan, & Scherlis, OOPSLA ‘06, P. 75]

Page 12: Как спроектировать хороший API и почему это так важно

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)

ПлохоХорошо

Page 13: Как спроектировать хороший API и почему это так важно

How to Design a Good API and Why it Matters13

Сохраняйте реалистичные ожидания

• Большинство проектировщиков API перегружены ограничениями─ Вы не можете угодить всем─ Старайтесь нравиться всем в равной степени

• Ожидайте, что совершите ошибки─ Несколько лет использования в реальном мире

выявят их─ Ожидайте, что будете развивать API

Page 14: Как спроектировать хороший API и почему это так важно

How to Design a Good API and Why it Matters14

II. Основные принципы

Page 15: Как спроектировать хороший API и почему это так важно

How to Design a Good API and Why it Matters15

API должен делать что-нибудь одно и делать это хорошо

• Функционал должно быть легко объясним─ Если трудно придумать название – это, как

правило, плохой знак─ Хорошие наименования являются движущей

силой─ С другой стороны, хорошие имена означают, что

вы на верном пути

Хорошо: Font, Set, PrivateKey, Lock, ThreadFactory, TimeUnit, Future

Плохо: DynAnyFactoryOperations, _BindingIteratorImplBase, ENCODING_CDR_ENCAPS, OMGVMCID

Page 16: Как спроектировать хороший API и почему это так важно

How to Design a Good API and Why it Matters16

API должен быть минимальным, но не меньше

• API должен удовлетворять требованиям

• Когда сомневаетесь – оставьте в покое─ Функционал, классы, методы, параметры и т.д.─ Вы всегда можете добавить, но Вы не

сможете убавить

• Концептуальная полнота важнее объема

• Ищите оптимальное соотношение возможностей и полноты (power-to-weight ratio)

Page 17: Как спроектировать хороший API и почему это так важно

How to Design a Good API and Why it Matters17

Реализация не должна влиять на API

• Детали реализации ─ Путают пользователей─ Ограничивают свободу для изменения реализации

• Поймите что такое «детали реализации»─ Не определяйте явно поведение методов

Например: не определяйте хэш-функции─ Все настраиваемые параметры – под подозрением

• Не давайте деталям реализации “утекать” в API─ Например: форматы хранения на диске и

форматы передачи по сети, исключения

Page 18: Как спроектировать хороший API и почему это так важно

How to Design a Good API and Why it Matters18

Минимизируйте доступность ВСЕГО

• Делайте классы и их члены максимально скрытыми

• Публичные классы не должны иметь публичных полей (за исключением констант)

• Максимизируйте сокрытие информации [Parnas]

• Минимизируйте связи

─ Это позволяет понимать, использовать, собирать, тестировать, отлаживать и оптимизировать модули независимо

Page 19: Как спроектировать хороший API и почему это так важно

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!");

Page 20: Как спроектировать хороший API и почему это так важно

How to Design a Good API and Why it Matters20

Документация имеет значение

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

- D. L. Parnas, Software Aging. Proceedingsof the 16th International Conference onSoftware Engineering, 1994

Page 21: Как спроектировать хороший API и почему это так важно

How to Design a Good API and Why it Matters21

Документируйте скрупулёзно(document religiously)

• Документируйте каждый класс, интерфейс, метод, конструктор, параметр и исключение─ Класс: что представляет собой экземпляр─ Метод: контракт между методом и его клиентом

─ Входные условия (preconditions), выходные условия (postconditions), побочные эффекты

─ Параметр: укажите единицы измерения, формат, права доступа владельца

• Очень внимательно документируйте пространство состояний

• Нет оправдания недокументированным элементам API. Точка!

Page 22: Как спроектировать хороший API и почему это так важно

How to Design a Good API and Why it Matters22

Учитывайте, как принимаемые решения влияют на производительность• Плохие решения могут ограничить

производительность─ Использование изменяемых (mutable) типов

─ Использование конструктора вместо статической фабрики (static factory)

─ Использование типов реализации вместо интерфейсных типов

• Не делайте специальных оберток API (do not warp) для увеличения производительности─ Проблема с производительностью, лежащая в основе

будет исправлена, но головная боль останется с вами навсегда

─ Хороший дизайн обычно сопровождается хорошей производительностью

Page 23: Как спроектировать хороший API и почему это так важно

How to Design a Good API and Why it Matters23

Влияние решений по проектированию API на производительность реальна и постоянна• Component.getSize() возвращает Dimension

• Dimension – изменяемый тип (mutable)

• Каждый вызов getSize вынужден создавать Dimension

• Это приводит к миллионам бесполезных созданий объектов

• Альтернативный метод добавлен в 1.2, но старый клиентский код остается медленным

(пример взят из Java AWT)

Page 24: Как спроектировать хороший API и почему это так важно

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

Page 25: Как спроектировать хороший API и почему это так важно

How to Design a Good API and Why it Matters25

III. Проектирование классов

Page 26: Как спроектировать хороший API и почему это так важно

How to Design a Good API and Why it Matters26

Минимизируйте изменяемость (mutability)• Классы должны быть неизменяемы

(immutable) пока не появится достаточной причины сделать обратное─ Плюсы: простота, thread-safe, легкость

повторного использования─ Минусы: отдельные объекты для каждого

значения

• Если класс изменяемый, сохраняйте пространство состояний маленьким и четко определенным─ Проясните, когда какой метод допустимо

вызывать

Bad: Date, Calendar

Good: TimerTask

Page 27: Как спроектировать хороший API и почему это так важно

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

Page 28: Как спроектировать хороший API и почему это так важно

How to Design a Good API and Why it Matters28

Проектируйте и документируйте с учетом возможного наследования или запретите его• Наследование нарушает инкапсуляцию

(Snyder, ‘86)─ Подклассы чувствительны к деталям реализации

суперкласса

• Если вы разрешаете наследование, документируйте само-использование(?) (self-use)─ Как методы используют друг друга?

• Консервативная политика: все конкретные классы – final.

Плохо: Множество конкретных классов в библиотеках J2SE

Хорошо: AbstractSet, AbstractMap

Page 29: Как спроектировать хороший API и почему это так важно

How to Design a Good API and Why it Matters29

IV. Проектирование методов

Page 30: Как спроектировать хороший API и почему это так важно

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! } }

Page 31: Как спроектировать хороший API и почему это так важно

How to Design a Good API and Why it Matters31

Не нарушайте принцип «наименьшего изумления»

• Пользователь API не должен удивляться поведению─ Это стоит дополнительных усилий при реализации─ Это стоит даже снижения производительности

public class Thread implements Runnable { // Проверяет, была ли нить прервана. // Очищает статус interrupted у данной нити. public static boolean interrupted(); }

Вот особенно вопиющий пример того, чего не стоит делать.

Page 32: Как спроектировать хороший API и почему это так важно

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); }

Page 33: Как спроектировать хороший API и почему это так важно

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();}

Page 34: Как спроектировать хороший API и почему это так важно

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

Page 35: Как спроектировать хороший API и почему это так важно

How to Design a Good API and Why it Matters35

Используйте подходящие типы параметров и возвращаемых значений• Отдавайте предпочтение интерфейсным типам перед

классами для входных параметров─ Обеспечивает гибкость и улучшает производительность

• Используйте наиболее конкретный тип для аргументов─ Перемещает ошибки с времени выполнения на время

компиляции

• Не используйте строки если существует лучший тип─ Строки громоздки, медленны и часто приводят к ошибкам

• Не используйте плавающую точку для денежных значений─ Двоичная плавающая точка приводит к неточным

результатам!

• Используйте double (64 бита) вместо float (32 бита)─ Потеря точности существенна, потеря

производительности незначительна

Page 36: Как спроектировать хороший API и почему это так важно

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

Page 37: Как спроектировать хороший API и почему это так важно

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);

Page 38: Как спроектировать хороший API и почему это так важно

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(); }

Page 39: Как спроектировать хороший API и почему это так важно

How to Design a Good API and Why it Matters39

V. Проектирование исключений

Page 40: Как спроектировать хороший API и почему это так важно

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)

Page 41: Как спроектировать хороший API и почему это так важно

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();}

Page 42: Как спроектировать хороший API и почему это так важно

How to Design a Good API and Why it Matters42

Включайте Failure-Capture информацию в исключения

• Предоставляет возможность для диагностики и восстановления

• Для unchecked exceptions достаточно сообщения

• Для checked exceptions предоставьте акцессор (метод, получающий текущее значение свойства)

Page 43: Как спроектировать хороший API и почему это так важно

How to Design a Good API and Why it Matters43

VI. Рефакторинг API

Page 44: Как спроектировать хороший 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); ...}

• Не очень мощно – поддерживается только поиск

• Трудно использовать без документации

Page 45: Как спроектировать хороший API и почему это так важно

How to Design a Good API and Why it Matters45

Sublist операции после рефакторинга

public interface List { List subList(int fromIndex, int toIndex); ...}

• Чрезвычайно мощно – поддерживаются все операции

• Использование интерфейса уменьшает концептуальный вес─ Высокое отношение мощность-вес

• Легко использовать без документации

Page 46: Как спроектировать хороший API и почему это так важно

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); }

Page 47: Как спроектировать хороший API и почему это так важно

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));

Page 48: Как спроектировать хороший API и почему это так важно

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());

Page 49: Как спроектировать хороший API и почему это так важно

How to Design a Good API and Why it Matters49

Заключение

• Проектирование API – это благородное и полезное дело─ Развивает программистов, конечных

пользователей, компании

• Это выступление охватывает некоторые хитрости этого ремесла─ Не будьте их рабами, но...─ не нарушайте их без особой на то причины

• Проектировать API трудно─ Это занятие не для одного человека─ Совершенство недостижимо, но Вы все равно

попробуйте

Page 50: Как спроектировать хороший API и почему это так важно

How to Design a Good API and Why it Matters50

Shameless Self-Promotion

“Bumper-Sticker API Design” - P. 506 in OOPSLA ‘06 Program

Page 51: Как спроектировать хороший API и почему это так важно

How to Design a Good API and Why it Matters51

Как спроектировать хороший API и почему это так важно

Joshua Bloch