You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2022/01/09 09:53:44 UTC

[groovy] 03/03: documentation: finish builder TBDs

This is an automated email from the ASF dual-hosted git repository.

paulk pushed a commit to branch GROOVY_4_0_X
in repository https://gitbox.apache.org/repos/asf/groovy.git

commit fcd4addbb7bf4a4f4748a243d8540c993833b3d0
Author: Paul King <pa...@asert.com.au>
AuthorDate: Sun Jan 9 18:53:36 2022 +1000

    documentation: finish builder TBDs
---
 src/spec/doc/core-domain-specific-languages.adoc | 104 ++++++++++++++-
 src/spec/test/builder/BuilderSpecTest.groovy     | 153 +++++++++++++++++++++++
 2 files changed, 252 insertions(+), 5 deletions(-)

diff --git a/src/spec/doc/core-domain-specific-languages.adoc b/src/spec/doc/core-domain-specific-languages.adoc
index d251ab9..0a71590 100644
--- a/src/spec/doc/core-domain-specific-languages.adoc
+++ b/src/spec/doc/core-domain-specific-languages.adoc
@@ -1797,23 +1797,117 @@ include::../test/builder/FileTreeBuilderTest.groovy[tags=shorthand_syntax_assert
 
 === Creating a builder
 
-While Groovy has many builders built-in, this pattern is so common,
+While Groovy has many built-in builders, the builder pattern is so common,
 you will no doubt eventually come across a building requirement that
 hasn't been catered for by those built-in builders.
 The good news is that you can build your own.
 You can do everything from scratch by relying on Groovy's
 metaprogramming capabilities. Alternatively, the `BuilderSupport`
-and `FactoryBuilderSupport` classes are specially designed to make building
-builders easy.
+and `FactoryBuilderSupport` classes make designing your own
+builders much easier.
 
 ==== BuilderSupport
 
 One approach to building a builder is to subclass `BuilderSupport`.
+With this approach, the general idea is to override one or more of a number of
+_lifecycle_ methods including `setParent`, `nodeCompleted` and some or
+all of the `createNode` methods from the `BuilderSupport` abstract class.
 
-(TBD)
+As an example, suppose we want to create a builder of
+tracking athletic training programs. Each program is made
+up of a number of sets and each set has its own steps.
+A step might itself be a set of smaller steps.
+For each `set` or `step`, we might wish to record
+the `distance` required (or `time`), whether to `repeat`
+the steps a certain number of times, whether to take a `break`
+between each step and so forth.
+
+For the simplicity of this example, we'll capture the training
+programming using maps and lists. A set has a list of steps.
+Information like `repeat` count or `distance` will be tracked
+in a map of attributes for each step and set.
+
+The builder implementation is as follows:
+
+* Override a couple of the `createNode` methods.
+We'll create a map capturing the set name, an empty list of steps,
+and potentially some attributes.
+* Whenever we complete a node we'll add the node to the list of steps
+for the parent (if any).
+
+The code looks like this:
+
+[source,groovy]
+----
+include::../test/builder/BuilderSpecTest.groovy[tags=define_builder1,indent=0]
+----
+
+Next, we'll write a little helper method which recursively
+adds up the distances of all substeps, accounting for repeated steps
+as needed.
+
+[source,groovy]
+----
+include::../test/builder/BuilderSpecTest.groovy[tags=define_total_helper1,indent=0]
+----
+
+Finally, we can now use our builder and helper method to create a
+swimming training program and check its total distance:
+
+[source,groovy]
+----
+include::../test/builder/BuilderSpecTest.groovy[tags=use_builder1,indent=0]
+----
 
 ==== FactoryBuilderSupport
 
 A second approach to building a builder is to subclass `FactoryBuilderSupport`.
 
-(TBD)
+With this approach, the general idea is to override one or more of a number of
+_lifecycle_ methods including `resolveFactory`, `nodeCompleted` and `postInstantiate` methods from the `FactoryBuilderSupport` abstract class.
+
+We'll use the same example as for the previous `BuilderSupport` example;
+a builder of tracking athletic training programs.
+
+For this example, rather than capturing the training
+programming using maps and lists, we'll use some simple domain classes.
+
+The builder implementation is as follows:
+
+* Override the `resolveFactory` method to return a special
+factory which returns classes by capitalizing the names used in our mini DSL.
+* Whenever we complete a node we'll add the node to the list of steps
+for the parent (if any).
+
+The code, including the code for the special factory class, looks like this:
+
+[source,groovy]
+----
+include::../test/builder/BuilderSpecTest.groovy[tags=define_builder2,indent=0]
+----
+
+Rather than using lists and maps, we'll have some simple domain classes
+and related traits:
+
+[source,groovy]
+----
+include::../test/builder/BuilderSpecTest.groovy[tags=define_domain_classes2,indent=0]
+----
+
+Just like for the `BuilderSupport` example, it is useful to have a helper method
+to calculate the total distance covered during the training session.
+The implementation is very similar to our earlier example, but is adjusted
+to work well with our newly defined traits.
+
+[source,groovy]
+----
+include::../test/builder/BuilderSpecTest.groovy[tags=define_total_helper2,indent=0]
+----
+
+Finally, we can now use our new builder and helper methods to create a
+cycling training program and check its total distance:
+
+[source,groovy]
+----
+include::../test/builder/BuilderSpecTest.groovy[tags=use_builder2,indent=0]
+----
diff --git a/src/spec/test/builder/BuilderSpecTest.groovy b/src/spec/test/builder/BuilderSpecTest.groovy
new file mode 100644
index 0000000..cfef70f
--- /dev/null
+++ b/src/spec/test/builder/BuilderSpecTest.groovy
@@ -0,0 +1,153 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package builder
+
+import groovy.transform.CompileStatic
+import org.junit.Test
+import static groovy.test.GroovyAssert.assertScript
+
+@CompileStatic
+class BuilderSpecTest {
+    @Test
+    void testBuilderSupport() {
+        assertScript '''
+// tag::define_builder1[]
+class TrainingBuilder1 extends BuilderSupport {
+    protected createNode(name) {
+        [name: name, steps: []]
+    }
+
+    protected createNode(name, Map attributes) {
+        createNode(name) + attributes
+    }
+
+    void nodeCompleted(maybeParent, node) {
+        if (maybeParent) maybeParent.steps << node
+    }
+
+    // unused lifecycle methods
+    protected void setParent(parent, child) { }
+    protected createNode(name, Map attributes, value) { }
+    protected createNode(name, value) { }
+}
+// end::define_builder1[]
+
+// tag::define_total_helper1[]
+def total(map) {
+    if (map.distance) return map.distance
+    def repeat = map.repeat ?: 1
+    repeat * map.steps.sum{ total(it) }
+}
+// end::define_total_helper1[]
+
+// tag::use_builder1[]
+def training = new TrainingBuilder1()
+
+def monday = training.swimming {
+    warmup(repeat: 3) {
+        freestyle(distance: 50)
+        breaststroke(distance: 50)
+    }
+    endurance(repeat: 20) {
+        freestyle(distance: 50, break: 15)
+    }
+    warmdown {
+        kick(distance: 100)
+        choice(distance: 100)
+    }
+}
+
+assert 1500 == total(monday)
+// end::use_builder1[]
+'''
+    }
+
+    @Test
+    void testFactoryBuilderSupport() {
+        assertScript '''
+// tag::define_builder2[]
+import static org.apache.groovy.util.BeanUtils.capitalize
+
+class TrainingBuilder2 extends FactoryBuilderSupport {
+    def factory = new TrainingFactory(loader: getClass().classLoader)
+
+    protected Factory resolveFactory(name, Map attrs, value) {
+        factory
+    }
+
+    void nodeCompleted(maybeParent, node) {
+        if (maybeParent) maybeParent.steps << node
+    }
+}
+
+class TrainingFactory extends AbstractFactory {
+    ClassLoader loader
+    def newInstance(FactoryBuilderSupport fbs, name, value, Map attrs) {
+        def clazz = loader.loadClass(capitalize(name))
+        value ? clazz.newInstance(value: value) : clazz.newInstance()
+    }
+}
+// end::define_builder2[]
+
+// tag::define_domain_classes2[]
+trait HasDistance {
+    int distance
+}
+
+trait Container extends HasDistance {
+    List steps = []
+    int repeat
+}
+
+class Cycling implements Container { }
+
+class Interval implements Container { }
+
+class Sprint implements HasDistance {}
+
+class Tempo implements HasDistance {}
+// end::define_domain_classes2[]
+
+// tag::define_total_helper2[]
+def total(HasDistance c) {
+    c.distance
+}
+
+def total(Container c) {
+    if (c.distance) return c.distance
+    def repeat = c.repeat ?: 1
+    repeat * c.steps.sum{ total(it) }
+}
+// end::define_total_helper2[]
+
+// tag::use_builder2[]
+def training = new TrainingBuilder2()
+
+def tuesday = training.cycling {
+    interval(repeat: 5) {
+        sprint(distance: 400)
+        tempo(distance: 3600)
+    }
+}
+
+assert 20000 == total(tuesday)
+// end::use_builder2[]
+'''
+    }
+}