Большая часть вашего кода является частной, внутренней, частной и никогда не будет открыта для общественности. Если это так, вы можете расслабиться — вы можете реорганизовать все свои ошибки, в том числе те, которые влекут за собой серьезные изменения API.
Однако, если вы поддерживаете открытый API, это не так. Если вы поддерживаете общедоступные SPI ( интерфейсы сервис-провайдеров ), то все становится еще хуже.
H2 Trigger SPI
В недавнем вопросе о переполнении стека о том, как реализовать триггер базы данных H2 с помощью jOOQ , я org.h2.api.Trigger
снова столкнулся с SPI — простым и простым в реализации SPI, который реализует семантику триггера. Вот как работают триггеры в базе данных H2:
Используйте триггер
CREATE TRIGGER my_trigger
BEFORE UPDATE
ON my_table
FOR EACH ROW
CALL "com.example.MyTrigger"
Реализуйте триггер
public class MyTrigger implements Trigger {
@Override
public void init(
Connection conn,
String schemaName,
String triggerName,
String tableName,
boolean before,
int type
)
throws SQLException {}
@Override
public void fire(
Connection conn,
Object[] oldRow,
Object[] newRow
)
throws SQLException {
// Using jOOQ inside of the trigger, of course
DSL.using(conn)
.insertInto(LOG, LOG.FIELD1, LOG.FIELD2, ..)
.values(newRow[0], newRow[1], ..)
.execute();
}
@Override
public void close() throws SQLException {}
@Override
public void remove() throws SQLException {}
}
Весь SPI H2 Trigger на самом деле довольно элегантный, и обычно вам нужно всего лишь реализовать fire()
метод.
Итак, как же этот SPI не так?
Это неправильно очень тонко. Рассмотрим init()
метод. Она имеет boolean
флаг , чтобы указать , должен ли триггер срабатывает до того или после того, как инициирующее событие, то есть UPDATE
. Что если вдруг H2 также поддержит INSTEAD OF
триггеры? В идеале этот флаг должен быть заменен на enum
:
public enum TriggerTiming {
BEFORE,
AFTER,
INSTEAD_OF
}
Но мы не можем просто ввести этот новый enum
тип, потому что init()
метод не должен изменяться несовместимо, нарушая весь реализующий код! С Java 8 мы могли бы по крайней мере объявить перегрузку следующим образом:
default void init(
Connection conn,
String schemaName,
String triggerName,
String tableName,
TriggerTiming timing,
int type
)
throws SQLException {
// New feature isn't supported by default
if (timing == INSTEAD_OF)
throw new SQLFeatureNotSupportedException();
// Call through to old feature by default
init(conn, schemaName, triggerName,
tableName, timing == BEFORE, type);
}
Это позволило бы новым реализациям обрабатывать INSTEAD_OF
триггеры, тогда как старые реализации все еще будут работать. Но это кажется волосатым, не так ли?
Теперь представьте, что мы также поддерживаем ENABLE
/ DISABLE
предложения и хотим передать эти значения в init()
метод. Или, может быть, мы хотим справиться FOR EACH ROW
. В настоящее время нет способа сделать это с этим SPI. Таким образом, мы получим все больше и больше этих перегрузок, которые очень сложно реализовать. И, по сути, это уже произошло, как и то org.h2.tools.TriggerAdapter
, что избыточно (но немного отличается от) Trigger
.
Что будет лучшим подходом, тогда?
Идеальный подход для поставщика SPI заключается в предоставлении «объектов аргументов», например:
public interface Trigger {
default void init(InitArguments args)
throws SQLException {}
default void fire(FireArguments args)
throws SQLException {}
default void close(CloseArguments args)
throws SQLException {}
default void remove(RemoveArguments args)
throws SQLException {}
final class InitArguments {
public Connection connection() { ... }
public String schemaName() { ... }
public String triggerName() { ... }
public String tableName() { ... }
/** use #timing() instead */
@Deprecated
public boolean before() { ... }
public TriggerTiming timing() { ... }
public int type() { ... }
}
final class FireArguments {
public Connection connection() { ... }
public Object[] oldRow() { ... }
public Object[] newRow() { ... }
}
// These currently don't have any properties
final class CloseArguments {}
final class RemoveArguments {}
}
Как вы можете видеть в приведенном выше примере, Trigger.InitArguments
был успешно разработан с соответствующими предупреждениями об устаревании. Никакой клиентский код не был нарушен, и новая функциональность готова к использованию, если это необходимо. Кроме того, close()
и remove()
готовы к будущим изменениям, даже если нам пока не нужны никакие аргументы.
Издержки этого решения — не более одного выделения объекта на вызов метода, что не должно повредить слишком сильно.
Другой пример: пользовательский тип Hibernate
К сожалению, эта ошибка случается слишком часто. Другим ярким примером является сложный для реализации org.hibernate.usertype.UserType
SPI Hibernate :
public interface UserType {
int[] sqlTypes();
Class returnedClass();
boolean equals(Object x, Object y);
int hashCode(Object x);
Object nullSafeGet(
ResultSet rs,
String[] names,
SessionImplementor session,
Object owner
) throws SQLException;
void nullSafeSet(
PreparedStatement st,
Object value,
int index,
SessionImplementor session
) throws SQLException;
Object deepCopy(Object value);
boolean isMutable();
Serializable disassemble(Object value);
Object assemble(
Serializable cached,
Object owner
);
Object replace(
Object original,
Object target,
Object owner
);
}
SPI выглядит довольно сложно для реализации. Возможно, вы можете заставить что-то работать довольно быстро, но будете ли вы чувствовать себя непринужденно? Будете ли вы думать, что вы поняли это правильно? Несколько примеров:
- Не бывает ли случая, когда вам нужна
owner
ссылка также вnullSafeSet()
? - Что если ваш драйвер JDBC не поддерживает выборку значений по имени из
ResultSet
? - Что если вам нужно использовать свой тип пользователя
CallableStatement
для хранимой процедуры?
Другим важным аспектом таких SPI является способ, которым разработчики могут предоставлять значения обратно в платформу. Обычно плохая идея иметь не- void
методы в SPI, так как вы никогда не сможете снова изменить тип возврата метода. В идеале у вас должны быть типы аргументов, которые принимают «результаты». Многие из вышеперечисленных методов могут быть заменены одним configuration()
методом:
public interface UserType {
default void configure(ConfigureArgs args) {}
final class ConfigureArgs {
public void sqlTypes(int[] types) { ... }
public void returnedClass(Class<?> clazz) { ... }
public void mutable(boolean mutable) { ... }
}
// ...
}
Другой пример, SAX ContentHandler
Посмотрите на этот пример здесь:
public interface ContentHandler {
void setDocumentLocator (Locator locator);
void startDocument ();
void endDocument();
void startPrefixMapping (String prefix, String uri);
void endPrefixMapping (String prefix);
void startElement (String uri, String localName,
String qName, Attributes atts);
void endElement (String uri, String localName,
String qName);
void characters (char ch[], int start, int length);
void ignorableWhitespace (char ch[], int start, int length);
void processingInstruction (String target, String data);
void skippedEntity (String name);
}
Некоторые примеры недостатков этого SPI:
- Что если вам понадобятся атрибуты элемента на
endElement()
событии? Вы должны будете помнить их самостоятельно. - Что делать, если вы хотите узнать URI сопоставления префиксов на
endPrefixMapping()
мероприятии? Или на любом другом мероприятии?
Очевидно, что SAX был оптимизирован для скорости, и он был оптимизирован для скорости в то время, когда JIT и GC были еще слабыми. Тем не менее, реализация обработчика SAX не тривиальна. Частично это связано с трудностью реализации SPI.
Мы не знаем будущего
Как поставщики API или SPI, мы просто не знаем будущего. Прямо сейчас мы можем подумать, что определенного SPI достаточно, но мы сломаем его уже в следующем выпуске. Или мы не нарушаем его и не говорим нашим пользователям, что не можем реализовать эти новые функции.
Используя описанные выше приемы, мы можем продолжать развивать наш SPI без каких-либо серьезных изменений:
- Всегда передавайте точно один аргумент объекта в методы.
- Всегда возвращайся
void
. Позвольте разработчикам взаимодействовать с состоянием SPI через объект аргумента. - Используйте
default
методы Java 8 или предоставьте «пустую» реализацию по умолчанию.
Вам понравилось это чтение? Вы также можете наслаждаться: