'

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

Понравилась презентация – покажи это...





Слайд 0

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


Слайд 1

Почему проектирование API так важно? API могут быть мощными активами компании Клиенты вкладывают значительные средства в покупку, программирование, обучение Стоимость прекращения использования API может быть слишком высока Успешные публичные API «подсаживают» клиентов Также могут быть серьезным грузом для компании Плохой API может привести к бесконечному потоку звонков в службу поддержки Может препятствовать движению вперед Публичный API – навсегда. Есть только один шанс сделать все правильно.


Слайд 2

Почему проектирование API важно для Вас? Если Вы программируете – Вы уже проектировщик API Хороший код – модульный. Каждый модуль имеет свой API. Полезные модули, как правило, используются повторно Как только у модуля появляются пользователи, нельзя изменить его API по своему желанию Хорошие повторно используемые модули – это активы компании Мышление в терминах API повышает качество кода


Слайд 3

Характеристики хорошего API Легко изучать Легко использовать, даже без документации documentation Трудно использовать неправильно Легко читать и поддерживать код, который использует его Достаточно мощный чтобы удовлетворять требованиям Легко развивать Соответствует аудитории


Слайд 4

Оглавление I. Процесс проектирования API II. Основные принципы III. Проектирование классов IV. Проектирование методов V. Проектирование исключений VI. Рефакторинг API


Слайд 5

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


Слайд 6

Соберите требования – со здоровой долей скептицизма Зачастую вам будут предлагать готовые решения (вместо требований) Могут существовать лучшие решения Ваша задача извлечь истинные требования Должны быть в форме use-cases Может быть проще и полезнее решить более общую задачу Что они говорят: “Нам нужны новые структуры данных и RPC с атрибутами версии 2” Что они подразумевают: “Нам нужен новый формат данных, который бы позволил развивать набор атрибутов”


Слайд 7

Начните с краткой спецификации. Одна страница – это идеально. На этом этапе гибкость предпочтительнее завершенности Разошлите спецификацию стольким людям, скольким это возможно. Прислушайтесь к их отзывам и примите это со всей серьезностью Если Ваша спецификация будет краткой, её легче будет изменять Начинайте воплощать, когда обретете уверенность Здесь обязательно предполагается кодирование


Слайд 8

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


Слайд 9

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


Слайд 10

Описывайте ваш API как можно раньше и чаще Начинайте прежде чем Вы реализовали API Это убережет Вас от создания от реализации, которую Вы потом выбросите Начинайте даже раньше, чем написали правильные спецификации Это убережет Вас от написания спецификаций, которые Вы потом выбросите Продолжайте описывать API по ходу воплощения Это убережет Вас от неприятных сюрпризов непосредственно перед развертыванием Код продолжает жить в примерах и модульных тестах Это наиболее важный код, который Вы когда-либо писали Формируется основа для Design Fragments [Fairbanks, Garlan, & Scherlis, OOPSLA ‘06, P. 75]


Слайд 11

Описание SPI – еще важнее Service Provider Interface (SPI) Интерфейс плагинов позволяет использовать множество реализаций Пример: Java Cryptography Extension (JCE) Напишите несколько плагинов до релиза Если один, он скорее всего не будет совместим с другими Если два, возможны проблемы с совместимостью Если три, все будет работать, как надо Will Tracz называет это “Правило трех” (Confessions of a Used Program Salesman, Addison-Wesley, 1995) Плохо


Слайд 12

Сохраняйте реалистичные ожидания Большинство проектировщиков API перегружены ограничениями Вы не можете угодить всем Старайтесь нравиться всем в равной степени Ожидайте, что совершите ошибки Несколько лет использования в реальном мире выявят их Ожидайте, что будете развивать API


Слайд 13

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


Слайд 14

API должен делать что-нибудь одно и делать это хорошо Функционал должно быть легко объясним Если трудно придумать название – это, как правило, плохой знак Хорошие наименования являются движущей силой С другой стороны, хорошие имена означают, что вы на верном пути Хорошо: Font, Set, PrivateKey, Lock, ThreadFactory, TimeUnit, Future Плохо: DynAnyFactoryOperations, _BindingIteratorImplBase, ENCODING_CDR_ENCAPS, OMGVMCID


Слайд 15

API должен быть минимальным, но не меньше API должен удовлетворять требованиям Когда сомневаетесь – оставьте в покое Функционал, классы, методы, параметры и т.д. Вы всегда можете добавить, но Вы не сможете убавить Концептуальная полнота важнее объема Ищите оптимальное соотношение возможностей и полноты (power-to-weight ratio)


Слайд 16

Реализация не должна влиять на API Детали реализации Путают пользователей Ограничивают свободу для изменения реализации Поймите что такое «детали реализации» Не определяйте явно поведение методов Например: не определяйте хэш-функции Все настраиваемые параметры – под подозрением Не давайте деталям реализации “утекать” в API Например: форматы хранения на диске и форматы передачи по сети, исключения


Слайд 17

Минимизируйте доступность ВСЕГО Делайте классы и их члены максимально скрытыми Публичные классы не должны иметь публичных полей (за исключением констант) Максимизируйте сокрытие информации [Parnas] Минимизируйте связи Это позволяет понимать, использовать, собирать, тестировать, отлаживать и оптимизировать модули независимо


Слайд 18

Имена имеют значение – API это маленький язык Названия должны быть самоочевидными Избегайте загадочных сокращений Стремитесь к согласованности Одно и то же слово должно означать одну и ту же вещь по всему API (а в идеале, по всем API на платформе) Будьте последовательны – стремитесь к гармонии Если Вы сделали все правильно, код читается как проза if (car.speed() > 2 * SPEED_LIMIT) speaker.generateAlert("Watch out for cops!");


Слайд 19

Документация имеет значение Повторное использование – это нечто, что гораздо проще сказать, чем сделать. Чтобы достичь этого требуется не только хороший дизайн, но и очень хорошая документация. Даже когда мы видим хороший дизайн, что бывает не часто, мы не увидим повторно используемых компонентов без хорошей документации. - D. L. Parnas, Software Aging. Proceedings of the 16th International Conference on Software Engineering, 1994


Слайд 20

Документируйте скрупулёзно (document religiously) Документируйте каждый класс, интерфейс, метод, конструктор, параметр и исключение Класс: что представляет собой экземпляр Метод: контракт между методом и его клиентом Входные условия (preconditions), выходные условия (postconditions), побочные эффекты Параметр: укажите единицы измерения, формат, права доступа владельца Очень внимательно документируйте пространство состояний Нет оправдания недокументированным элементам API. Точка!


Слайд 21

Учитывайте, как принимаемые решения влияют на производительность Плохие решения могут ограничить производительность Использование изменяемых (mutable) типов Использование конструктора вместо статической фабрики (static factory) Использование типов реализации вместо интерфейсных типов Не делайте специальных оберток API (do not warp) для увеличения производительности Проблема с производительностью, лежащая в основе будет исправлена, но головная боль останется с вами навсегда Хороший дизайн обычно сопровождается хорошей производительностью


Слайд 22

Влияние решений по проектированию API на производительность реальна и постоянна Component.getSize() возвращает Dimension Dimension – изменяемый тип (mutable) Каждый вызов getSize вынужден создавать Dimension Это приводит к миллионам бесполезных созданий объектов Альтернативный метод добавлен в 1.2, но старый клиентский код остается медленным (пример взят из Java AWT)


Слайд 23

API должен мирно сосуществовать с платформой Делайте то, что принято (в платформе) Положитесь на стандартные методы именования Избегайте устаревших параметров и возвращаемых типов Подражайте шаблонам в базовом API платформы и языка Используйте с выгодой полезные для API особенности (API-friendly features): generics, varargs, enums, аргументы по умолчанию Знайте и избегайте ловушки и западни API Finalizers, public static final arrays Не используйте транслитерацию в API


Слайд 24

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


Слайд 25

Минимизируйте изменяемость (mutability) Классы должны быть неизменяемы (immutable) пока не появится достаточной причины сделать обратное Плюсы: простота, thread-safe, легкость повторного использования Минусы: отдельные объекты для каждого значения Если класс изменяемый, сохраняйте пространство состояний маленьким и четко определенным Проясните, когда какой метод допустимо вызывать Bad: Date, Calendar Good: TimerTask


Слайд 26

Наследуйте классы только там где это имеет смысл Наследование подразумевает взаимозаменяемость Используйте наследование только если существует отношение «is-a» (is every Foo a Bar?) В противном случае используйте композицию Публичные классы не должны наследовать другие публичные классы для удобства реализации Плохо: Properties extends Hashtable Stack extends Vector Хорошо: Set extends Collection


Слайд 27

Проектируйте и документируйте с учетом возможного наследования или запретите его Наследование нарушает инкапсуляцию (Snyder, ‘86) Подклассы чувствительны к деталям реализации суперкласса Если вы разрешаете наследование, документируйте само-использование(?) (self-use) Как методы используют друг друга? Консервативная политика: все конкретные классы – final. Плохо: Множество конкретных классов в библиотеках J2SE Хорошо: AbstractSet, AbstractMap


Слайд 28

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


Слайд 29

Не заставляйте клиента делать то, что может сделать модуль Уменьшайте необходимость использования шаблонного кода Обычно делается через 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! } }


Слайд 30

Не нарушайте принцип «наименьшего изумления» Пользователь API не должен удивляться поведению Это стоит дополнительных усилий при реализации Это стоит даже снижения производительности public class Thread implements Runnable { // Проверяет, была ли нить прервана. // Очищает статус interrupted у данной нити. public static boolean interrupted(); } Вот особенно вопиющий пример того, чего не стоит делать.


Слайд 31

Падай быстро – сообщайте об ошибках как можно скорее Лучше всего во время компиляции – статическая типизация, 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); }


Слайд 32

Предоставьте программный доступ ко всем данным, доступным в виде строк В противном случае клиентам придется анализировать строки Мучительно для клиентов Хуже того, это де-факто превращает формат строк в часть 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(); }


Слайд 33

Используйте перегрузку методов с осторожностью (Overload With Care) Избегайте неоднозначных перегрузок Multiple overloadings applicable to same actuals Консервативный подход: нет двух (методов или функций) с одним и тем же числом аргументов Просто «потому что Вы можете» не означает, что «Вы должны» Часто лучше просто использовать другое имя Если Вам необходимо сделать неоднозначные перегрузки, обеспечьте одинаковое поведение для одинаковых аргументов public TreeSet(Collection c); // Ignores order public TreeSet(SortedSet s); // Respects order


Слайд 34

Используйте подходящие типы параметров и возвращаемых значений Отдавайте предпочтение интерфейсным типам перед классами для входных параметров Обеспечивает гибкость и улучшает производительность Используйте наиболее конкретный тип для аргументов Перемещает ошибки с времени выполнения на время компиляции Не используйте строки если существует лучший тип Строки громоздки, медленны и часто приводят к ошибкам Не используйте плавающую точку для денежных значений Двоичная плавающая точка приводит к неточным результатам! Используйте double (64 бита) вместо float (32 бита) Потеря точности существенна, потеря производительности незначительна


Слайд 35

Используйте один и тот же порядок параметров во всех методах Особенно важно если типы параметров одинаковы #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


Слайд 36

Избегайте длинных списков параметров Три или меньше параметров – это идеально Сделайте больше и пользователю придется смотреть в документацию Длинные списки параметров с одинаковыми типами – вредны Программисты по ошибке сдвигают параметры Программа продолжает собираться, запускаться, но работает неверно! Техники сокращения списка параметров Разбейте метод на части Создайте вспомогательный класс, содержащий параметры // Одинадцать параметров включая четыре последовательных int HWND CreateWindow(LPCTSTR lpClassName, LPCTSTR lpWindowName, DWORD dwStyle, int x, int y, int nWidth, int nHeight, HWND hWndParent, HMENU hMenu, HINSTANCE hInstance, LPVOID lpParam);


Слайд 37

Избегайте возвращать значения которые требуют особой обработки Возвращайте массив нулевой длины или пустое множество, а не 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(); }


Слайд 38

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


Слайд 39

Выбрасывайте исключения только чтобы сигнализировать об исключительной ситуации Не заставляйте клиента использовать исключения для управления потоком выполнения 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)


Слайд 40

Отдавайте предпочтение 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(); }


Слайд 41

Включайте Failure-Capture информацию в исключения Предоставляет возможность для диагностики и восстановления Для unchecked exceptions достаточно сообщения Для checked exceptions предоставьте акцессор (метод, получающий текущее значение свойства)


Слайд 42

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


Слайд 43

1. Списковые операции над Vector-ом public class Vector { public int indexOf(Object elem, int index); public int lastIndexOf(Object elem, int index); ... } Не очень мощно – поддерживается только поиск Трудно использовать без документации


Слайд 44

Sublist операции после рефакторинга public interface List { List subList(int fromIndex, int toIndex); ... } Чрезвычайно мощно – поддерживаются все операции Использование интерфейса уменьшает концептуальный вес Высокое отношение мощность-вес Легко использовать без документации


Слайд 45

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


Слайд 46

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


Слайд 47

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


Слайд 48

Заключение Проектирование API – это благородное и полезное дело Развивает программистов, конечных пользователей, компании Это выступление охватывает некоторые хитрости этого ремесла Не будьте их рабами, но... не нарушайте их без особой на то причины Проектировать API трудно Это занятие не для одного человека Совершенство недостижимо, но Вы все равно попробуйте


Слайд 49

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


Слайд 50

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


×

HTML:





Ссылка: