Большая часть вашего кода является частной, внутренней, частной и никогда не будет открыта для общественности. Если это так, вы можете расслабиться — вы можете реорганизовать все свои ошибки, в том числе те, которые влекут за собой серьезные изменения API.
Если вы используете публичный API, это не так. Если вы поддерживаете общедоступные SPI ( интерфейсы сервис-провайдеров ), то все становится еще хуже.
H2 Trigger SPI
В недавнем вопросе о переполнении стека о том, как реализовать триггер базы данных H2 с помощью jOOQ , я снова столкнулся с SPI org.h2.api.Trigger
— простым и простым в реализации SPI, который реализует семантику триггера. Вот как работают триггеры в базе данных H2:
Используйте триггер
1
2
3
4
5
|
CREATE TRIGGER my_trigger BEFORE UPDATE ON my_table FOR EACH ROW CALL "com.example.MyTrigger" |
Реализуйте триггер
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
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
:
1
2
3
4
5
|
public enum TriggerTiming { BEFORE, AFTER, INSTEAD_OF } |
Но мы не можем просто ввести этот новый тип enum
потому что метод init()
не должен изменяться несовместимым образом, нарушая весь реализующий код! С Java 8 мы могли бы по крайней мере объявить перегрузку следующим образом:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
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 заключается в предоставлении «объектов аргументов», например:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
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
К сожалению, эта ошибка случается слишком часто. Другим ярким примером является сложный для реализации SPI- org.hibernate.usertype.UserType
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
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 является способ, которым разработчики могут предоставлять значения обратно в платформу. Обычно плохая идея иметь не пустые методы в SPI, так как вы никогда не сможете снова изменить тип возврата метода. В идеале у вас должны быть типы аргументов, которые принимают «результаты». Многие из вышеперечисленных методов могут быть заменены одним методом configuration()
например так:
01
02
03
04
05
06
07
08
09
10
11
|
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
Посмотрите на этот пример здесь:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
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()
? Вы должны будете помнить их самостоятельно. - Что если вы хотите узнать
endPrefixMapping()
сопоставления префиксов вendPrefixMapping()
? Или на любом другом мероприятии?
Очевидно, что SAX был оптимизирован для скорости, и он был оптимизирован для скорости в то время, когда JIT и GC были еще слабыми. Тем не менее, реализация обработчика SAX не тривиальна. Частично это связано с трудностью реализации SPI.
Мы не знаем будущего
Как поставщики API или SPI, мы просто не знаем будущего. Прямо сейчас мы можем подумать, что определенного SPI достаточно, но мы сломаем его уже в следующем выпуске. Или мы не нарушаем его и не говорим нашим пользователям, что не можем реализовать эти новые функции.
Используя описанные выше приемы, мы можем продолжать развивать наш SPI без каких-либо серьезных изменений:
- Всегда передавайте точно один аргумент объекта в методы.
- Всегда возвращай
void
. Позвольте разработчикам взаимодействовать с состоянием SPI через объект аргумента. - Используйте
default
методы Java 8 или предоставьте «пустую» реализацию по умолчанию.
Ссылка: | Не допускайте этой ошибки при разработке SPI от нашего партнера JCG Лукаса Эдера в блоге JAVA, SQL и JOOQ . |