Skip to content

Commit d8b6886

Browse files
committed
Better extension pattern
1 parent b76c667 commit d8b6886

File tree

2 files changed

+58
-51
lines changed

2 files changed

+58
-51
lines changed

src/main/java/org/mybatis/dynamic/sql/SqlColumn.java

Lines changed: 45 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,30 @@
3030
*
3131
* <p>The class contains many attributes that are helpful for use in MyBatis and Spring runtime
3232
* environments, but the only required attributes are the name of the column and a reference to
33-
* the SqlTable the column is a part of.
33+
* the {@link SqlTable} the column is a part of.
3434
*
3535
* <p>The class can be extended if you wish to associate additional attributes with a column for your
36-
* own purposes. Extending the class involves the following activities:
36+
* own purposes. Extending the class is a bit more challenging than you might expect because you will need to
37+
* handle the covariant types for many methods in {@code SqlColumn}. Additionally, many methods in {@code SqlColumn}
38+
* create new instances of the class in keeping with the library's primary strategy of immutability. You will also
39+
* need to ensure that these methods create instances of your extended class, rather than the base {@code SqlColumn}
40+
* class. We have worked to keep this process as simple as possible.
41+
*
42+
* <p>Extending the class involves the following activities:
3743
* <ol>
3844
* <li>Create a class that extends {@link SqlColumn}</li>
3945
* <li>In your extended class, create a static builder class that extends {@link SqlColumn.AbstractBuilder}</li>
4046
* <li>Add your desired attributes to the class and the builder</li>
47+
* <li>In your extended class, override the {@link SqlColumn#copyBuilder()} method and return a new instance of
48+
* your builder with all attributes set. You should call the
49+
* {@link SqlColumn#populateBaseBuilder(AbstractBuilder)} method
50+
* to set the attributes from {@code SqlColumn}, then populate your extended attributes.
51+
* </li>
4152
* <li>You MUST override the following methods. These methods are used with regular operations in the library.
4253
* If you do not override these methods, it is likely that your extended attributes will be lost during
43-
* regular usage. For example, if a user calls the {@code as} method to apply an alias, the base
44-
* {@code SqlColumn} class will create a new instance of {@code SqlColumn}, NOT your extended class.
54+
* regular usage. For example, if you do not override the {@code as} method and a user calls the method to
55+
* apply an alias, then the base {@code SqlColumn} class would create a new instance of {@code SqlColumn}, NOT
56+
* your extended class.
4557
* <ul>
4658
* <li>{@link SqlColumn#as(String)}</li>
4759
* <li>{@link SqlColumn#asCamelCase()}</li>
@@ -63,10 +75,21 @@
6375
* </li>
6476
* </ol>
6577
*
66-
* <p>The test code for this library contains an example of a proper extension of this class. In most cases, the code
67-
* for the overriding methods can be simply copied from this class and then the return types can be changed to the
68-
* subclass's covariant type (this pre-supposes that you create a {@code copyBuilder} method that returns a new builder
69-
* populated with all current class attributes).
78+
* <p>For all overridden methods except {@code copyBuilder()}, the process is to call the superclass
79+
* method and cast the result properly. We provide a {@link SqlColumn#cast(SqlColumn)} method to aid with this
80+
* process. For example, overriding the {@code descending} method could look like this:
81+
*
82+
* <p>
83+
* <pre>
84+
* {@code
85+
* @Override
86+
* public MyExtendedColumn<T> descending() {
87+
* return cast(super.descending());
88+
* }
89+
* }
90+
* </pre>
91+
*
92+
* <p>The test code for this library contains an example of a proper extension of this class.
7093
*
7194
* @param <T> the Java type associated with the column
7295
*/
@@ -84,7 +107,7 @@ public class SqlColumn<T> implements BindableColumn<T>, SortSpecification {
84107
protected final @Nullable Class<T> javaType;
85108
protected final @Nullable String javaProperty;
86109

87-
protected SqlColumn(AbstractBuilder<T, ?> builder) {
110+
protected SqlColumn(AbstractBuilder<T, ?, ?> builder) {
88111
name = Objects.requireNonNull(builder.name);
89112
table = Objects.requireNonNull(builder.table);
90113
jdbcType = builder.jdbcType;
@@ -142,11 +165,7 @@ public Optional<String> javaProperty() {
142165
*/
143166
@Override
144167
public SqlColumn<T> descending() {
145-
return setDescending(copyBuilder()).build();
146-
}
147-
148-
protected <B extends AbstractBuilder<T, ?>> B setDescending(B builder) {
149-
return cast(builder.withDescendingPhrase(" DESC")); //$NON-NLS-1$
168+
return cast(copyBuilder().withDescendingPhrase(" DESC").build()); //$NON-NLS-1$
150169
}
151170

152171
/**
@@ -159,11 +178,7 @@ public SqlColumn<T> descending() {
159178
*/
160179
@Override
161180
public SqlColumn<T> as(String alias) {
162-
return setAlias(copyBuilder(), alias).build();
163-
}
164-
165-
protected <B extends AbstractBuilder<T, ?>> B setAlias(B builder, String alias) {
166-
return cast(builder.withAlias(alias));
181+
return cast(copyBuilder().withAlias(alias).build());
167182
}
168183

169184
/**
@@ -174,11 +189,7 @@ public SqlColumn<T> as(String alias) {
174189
* @return a new column that will be rendered with the specified table qualifier
175190
*/
176191
public SqlColumn<T> qualifiedWith(String tableQualifier) {
177-
return setTableQualifier(copyBuilder(), tableQualifier).build();
178-
}
179-
180-
protected <B extends AbstractBuilder<T, ?>> B setTableQualifier(B builder, String tableQualifier) {
181-
return cast(builder.withTableQualifier(tableQualifier));
192+
return cast(copyBuilder().withTableQualifier(tableQualifier).build());
182193
}
183194

184195
/**
@@ -193,11 +204,9 @@ public SqlColumn<T> qualifiedWith(String tableQualifier) {
193204
* @return a new column aliased with a camel case version of the column name
194205
*/
195206
public SqlColumn<T> asCamelCase() {
196-
return setCamelCaseAlias(copyBuilder()).build();
197-
}
198-
199-
protected <B extends AbstractBuilder<T, ?>> B setCamelCaseAlias(B builder) {
200-
return cast(builder.withAlias("\"" + StringUtilities.toCamelCase(name) + "\"")); //$NON-NLS-1$ //$NON-NLS-2$
207+
return cast(copyBuilder()
208+
.withAlias("\"" + StringUtilities.toCamelCase(name) + "\"") //$NON-NLS-1$ //$NON-NLS-2$
209+
.build());
201210
}
202211

203212
@Override
@@ -310,7 +319,7 @@ public <S> SqlColumn<S> withJavaProperty(String javaProperty) {
310319
return cast(copyBuilder().withJavaProperty(javaProperty).build());
311320
}
312321

313-
private Builder<T> copyBuilder() {
322+
protected AbstractBuilder<T, ?, ?> copyBuilder() {
314323
return populateBaseBuilder(new Builder<>());
315324
}
316325

@@ -319,11 +328,6 @@ protected <S extends SqlColumn<?>> S cast(SqlColumn<?> column) {
319328
return (S) column;
320329
}
321330

322-
@SuppressWarnings("unchecked")
323-
protected <B extends AbstractBuilder<?, ?>> B cast(AbstractBuilder<?, ?> builder) {
324-
return (B) builder;
325-
}
326-
327331
/**
328332
* This method will add all current attributes to the specified builder. It is useful when creating
329333
* new class instances that only change one attribute - we set all current attributes, then
@@ -334,7 +338,7 @@ protected <S extends SqlColumn<?>> S cast(SqlColumn<?> column) {
334338
* @return the populated builder
335339
*/
336340
@SuppressWarnings("unchecked")
337-
protected <B extends AbstractBuilder<T, ?>> B populateBaseBuilder(B builder) {
341+
protected <B extends AbstractBuilder<T, ?, ?>> B populateBaseBuilder(B builder) {
338342
return (B) builder
339343
.withName(this.name)
340344
.withTable(this.table)
@@ -362,7 +366,7 @@ public static <T> SqlColumn<T> of(String name, SqlTable table, JDBCType jdbcType
362366
.build();
363367
}
364368

365-
public static abstract class AbstractBuilder<T, B extends AbstractBuilder<T, B>> {
369+
public static abstract class AbstractBuilder<T, C extends SqlColumn<T>, B extends AbstractBuilder<T, C, B>> {
366370
protected @Nullable String name;
367371
protected @Nullable SqlTable table;
368372
protected @Nullable JDBCType jdbcType;
@@ -431,9 +435,12 @@ public B withJavaProperty(@Nullable String javaProperty) {
431435
}
432436

433437
protected abstract B getThis();
438+
439+
public abstract C build();
434440
}
435441

436-
public static class Builder<T> extends AbstractBuilder<T, Builder<T>> {
442+
public static class Builder<T> extends AbstractBuilder<T, SqlColumn<T>, Builder<T>> {
443+
@Override
437444
public SqlColumn<T> build() {
438445
return new SqlColumn<>(this);
439446
}

src/test/java/examples/simple/PrimaryKeyColumn.java

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,63 +33,63 @@ public boolean isPrimaryKeyColumn() {
3333

3434
@Override
3535
public PrimaryKeyColumn<T> descending() {
36-
return setDescending(copyBuilder()).build();
36+
return cast(super.descending());
3737
}
3838

3939
@Override
4040
public PrimaryKeyColumn<T> as(String alias) {
41-
return setAlias(copyBuilder(), alias).build();
41+
return cast(super.as(alias));
4242
}
4343

4444
@Override
4545
public PrimaryKeyColumn<T> qualifiedWith(String tableQualifier) {
46-
return setTableQualifier(copyBuilder(), tableQualifier).build();
46+
return cast(super.qualifiedWith(tableQualifier));
4747
}
4848

4949
@Override
5050
public PrimaryKeyColumn<T> asCamelCase() {
51-
return setCamelCaseAlias(copyBuilder()).build();
51+
return cast(super.asCamelCase());
5252
}
5353

5454
@Override
5555
public <S> PrimaryKeyColumn<S> withTypeHandler(String typeHandler) {
56-
return cast(copyBuilder().withTypeHandler(typeHandler).build());
56+
return cast(super.withTypeHandler(typeHandler));
5757
}
5858

5959
@Override
6060
public <S> PrimaryKeyColumn<S> withRenderingStrategy(RenderingStrategy renderingStrategy) {
61-
return cast(copyBuilder().withRenderingStrategy(renderingStrategy).build());
61+
return cast(super.withRenderingStrategy(renderingStrategy));
6262
}
6363

6464
@Override
65-
@SuppressWarnings("unchecked")
6665
public <S> PrimaryKeyColumn<S> withParameterTypeConverter(ParameterTypeConverter<S, ?> parameterTypeConverter) {
67-
return cast(copyBuilder().withParameterTypeConverter((ParameterTypeConverter<T, ?>) parameterTypeConverter).build());
66+
return cast(super.withParameterTypeConverter(parameterTypeConverter));
6867
}
6968

7069
@Override
71-
@SuppressWarnings("unchecked")
7270
public <S> PrimaryKeyColumn<S> withJavaType(Class<S> javaType) {
73-
return cast(copyBuilder().withJavaType((Class<T>) javaType).build());
71+
return cast(super.withJavaType(javaType));
7472
}
7573

7674
@Override
7775
public <S> PrimaryKeyColumn<S> withJavaProperty(String javaProperty) {
78-
return cast(copyBuilder().withJavaProperty(javaProperty).build());
76+
return cast(super.withJavaProperty(javaProperty));
7977
}
8078

81-
private Builder<T> copyBuilder() {
79+
@Override
80+
protected Builder<T> copyBuilder() {
8281
return populateBaseBuilder(new Builder<>()).isPrimaryKeyColumn(isPrimaryKeyColumn);
8382
}
8483

85-
public static class Builder<T> extends AbstractBuilder<T, Builder<T>> {
84+
public static class Builder<T> extends AbstractBuilder<T, PrimaryKeyColumn<T>, Builder<T>> {
8685
private boolean isPrimaryKeyColumn;
8786

8887
public Builder<T> isPrimaryKeyColumn(boolean isPrimaryKeyColumn) {
8988
this.isPrimaryKeyColumn = isPrimaryKeyColumn;
9089
return this;
9190
}
9291

92+
@Override
9393
public PrimaryKeyColumn<T> build() {
9494
return new PrimaryKeyColumn<>(this);
9595
}

0 commit comments

Comments
 (0)