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 2020/08/31 06:15:39 UTC

[groovy] branch master updated: doco: add Observer pattern

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

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


The following commit(s) were added to refs/heads/master by this push:
     new f2b4a48  doco: add Observer pattern
f2b4a48 is described below

commit f2b4a48f07210fa2162f05509f94808bceff03c9
Author: Paul King <pa...@asert.com.au>
AuthorDate: Mon Aug 31 16:15:29 2020 +1000

    doco: add Observer pattern
---
 src/spec/doc/design-patterns-in-groovy.adoc        |   2 +
 src/spec/doc/fragment_design-pattern-observer.adoc | 105 ++++++++++++++
 src/spec/doc/fragment_design-pattern-visitor.adoc  |   2 +-
 src/spec/test/DesignPatternsTest.groovy            | 159 +++++++++++++++++++++
 4 files changed, 267 insertions(+), 1 deletion(-)

diff --git a/src/spec/doc/design-patterns-in-groovy.adoc b/src/spec/doc/design-patterns-in-groovy.adoc
index 77a828f..436e5d4 100644
--- a/src/spec/doc/design-patterns-in-groovy.adoc
+++ b/src/spec/doc/design-patterns-in-groovy.adoc
@@ -55,6 +55,8 @@ include::fragment_design-pattern-loan-my-resource.adoc[leveloffset=+2]
 
 include::fragment_design-pattern-null-object.adoc[leveloffset=+2]
 
+include::fragment_design-pattern-observer.adoc[leveloffset=+2]
+
 include::fragment_design-pattern-pimp-my-library.adoc[leveloffset=+2]
 
 include::fragment_design-pattern-proxy.adoc[leveloffset=+2]
diff --git a/src/spec/doc/fragment_design-pattern-observer.adoc b/src/spec/doc/fragment_design-pattern-observer.adoc
new file mode 100644
index 0000000..c265551
--- /dev/null
+++ b/src/spec/doc/fragment_design-pattern-observer.adoc
@@ -0,0 +1,105 @@
+//////////////////////////////////////////
+
+  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.
+
+//////////////////////////////////////////
+
+= Observer Pattern
+
+
+The https://en.wikipedia.org/wiki/Observer_pattern[Observer Pattern] allows one or more _observers_ to be notified
+about changes or events from a _subject_ object.
+
+[plantuml, ChainOfResponsibilityClasses, png]
+....
+skinparam ClassBorderColor<<Hidden>> Transparent
+skinparam ClassBackgroundColor<<Hidden>> Transparent
+skinparam ClassStereotypeFontColor<<Hidden>> Transparent
+skinparam ClassFontSize<<Hidden>> 24
+skinparam ClassFontStyle<<Hidden>> bold
+skinparam shadowing<<Hidden>> false
+hide <<Hidden>> circle
+class "..." as ConcreteHidden
+class ConcreteHidden <<Hidden>> {
+}
+class Observer {
+    +update()
+}
+class ConcreteObserver1 {
+    +update()
+}
+class ConcreteObserverN {
+    +update()
+}
+hide Observer fields
+class Subject {
+    -observerCollection
+    +registerObserver(observer)
+    +unregisterObserver(observer)
+    +notifyObservers()
+}
+Observer <|-- ConcreteObserver1
+Observer <|-[hidden]- ConcreteHidden
+Observer <|-- ConcreteObserverN
+Observer ---r---o Subject
+ConcreteObserver1 .r[hidden]. ConcreteHidden
+....
+
+== Example
+
+Here is a typical implementation of the classic pattern:
+
+[source,groovy]
+----
+include::../test/DesignPatternsTest.groovy[tags=observer_classic,indent=0]
+----
+
+Using Closures, we can avoid creating the concrete observer classes as shown below:
+
+[source,groovy]
+----
+include::../test/DesignPatternsTest.groovy[tags=observer_closures,indent=0]
+----
+
+As a variation for Groovy 3+, let's consider dropping the `Observer` interface and using lambdas as shown below:
+
+[source,groovy]
+----
+include::../test/DesignPatternsTest.groovy[tags=observer_lambdas,indent=0]
+----
+
+We are now calling the `accept` method from `Consumer` rather
+than the `update` method from `Observer`.
+
+== @Bindable and @Vetoable
+
+The JDK has some built-in classes which follow the observer pattern.
+The `java.util.Observer` and `java.util.Observable` classes are deprecated from JDK 9 due to various limitations.
+Instead, you are recommended to use various more powerful classes in the `java.beans` package such as `java.beans.PropertyChangeListener`.
+Luckily, Groovy has some built-in transforms (gapi:groovy.beans.Bindable[] and gapi:groovy.beans.Vetoable[])
+which support for some key classes from that package.
+
+[source,groovy]
+----
+include::../test/DesignPatternsTest.groovy[tags=observer_bindable,indent=0]
+----
+
+Here, methods like `addPropertyChangeListener` perform the same role as `registerObserver` in previous examples.
+There is a `firePropertyChange` method corresponding to `notifyAll`/`notifyObservers` in previous examples but Groovy adds that
+automatically here, so it isn't visible in the source code. There is also a `propertyChange` method that corresponds
+to the `update` method in previous examples, though again, that isn't visible here.
diff --git a/src/spec/doc/fragment_design-pattern-visitor.adoc b/src/spec/doc/fragment_design-pattern-visitor.adoc
index c427810..20697b2 100644
--- a/src/spec/doc/fragment_design-pattern-visitor.adoc
+++ b/src/spec/doc/fragment_design-pattern-visitor.adoc
@@ -146,7 +146,7 @@ include::../test/DesignPatternsTest.groovy[tags=visitor_advanced_example4,indent
 Looks like we saved a few lines of code here, but we made more.
 The `Visitable` nodes now do not refer to any `Visitor` class or interface.
 This is about the best level of separation you might expect here, but we can go further.
-Let us change the `Visitable` interface a little and let it return the children we want to visit next.
+Let's change the `Visitable` interface a little and let it return the children we want to visit next.
 This allows us a general iteration method.
 
 [source,groovy]
diff --git a/src/spec/test/DesignPatternsTest.groovy b/src/spec/test/DesignPatternsTest.groovy
index b8f29e3..276a6c0 100644
--- a/src/spec/test/DesignPatternsTest.groovy
+++ b/src/spec/test/DesignPatternsTest.groovy
@@ -1359,6 +1359,165 @@ class DesignPatternsTest extends CompilableTestSupport {
         '''
     }
 
+    void testObserverExample() {
+        assertScript '''
+            // tag::observer_classic[]
+            interface Observer {
+                void update(message)
+            }
+
+            class Subject {
+                private List observers = []
+                void register(observer) {
+                    observers << observer
+                }
+                void unregister(observer) {
+                    observers -= observer
+                }
+                void notifyAll(message) {
+                    observers.each{ it.update(message) }
+                }
+            }
+
+            class ConcreteObserver1 implements Observer {
+                def messages = []
+                void update(message) {
+                    messages << message
+                }
+            }
+
+            class ConcreteObserver2 implements Observer {
+                def messages = []
+                void update(message) {
+                    messages << message.toUpperCase()
+                }
+            }
+
+            def o1a = new ConcreteObserver1()
+            def o1b = new ConcreteObserver1()
+            def o2 = new ConcreteObserver2()
+            def observers = [o1a, o1b, o2]
+            new Subject().with {
+                register(o1a)
+                register(o2)
+                notifyAll('one')
+            }
+            new Subject().with {
+                register(o1b)
+                register(o2)
+                notifyAll('two')
+            }
+            def expected = [['one'], ['two'], ['ONE', 'TWO']]
+            assert observers*.messages == expected
+            // end::observer_classic[]
+        '''
+        assertScript '''
+            // tag::observer_closures[]
+            interface Observer {
+                void update(message)
+            }
+
+            class Subject {
+                private List observers = []
+                void register(Observer observer) {
+                    observers << observer
+                }
+                void unregister(observer) {
+                    observers -= observer
+                }
+                void notifyAll(message) {
+                    observers.each{ it.update(message) }
+                }
+            }
+
+            def messages1a = [], messages1b = [], messages2 = []
+            def o2 = { messages2 << it.toUpperCase() }
+            new Subject().with {
+                register{ messages1a << it }
+                register(o2)
+                notifyAll('one')
+            }
+            new Subject().with {
+                register{ messages1b << it }
+                register(o2)
+                notifyAll('two')
+            }
+            def expected = [['one'], ['two'], ['ONE', 'TWO']]
+            assert [messages1a, messages1b, messages2] == expected
+            // end::observer_closures[]
+        '''
+        assertScript '''
+            // tag::observer_lambdas[]
+            import java.util.function.Consumer
+
+            class Subject {
+                private List<Consumer> observers = []
+                void register(Consumer observer) {
+                    observers << observer
+                }
+                void unregister(observer) {
+                    observers -= observer
+                }
+                void notifyAll(message) {
+                    observers.each{ it.accept(message) }
+                }
+            }
+
+            def messages1a = [], messages1b = [], messages2 = []
+            def o2 = { messages2 << it.toUpperCase() }
+            new Subject().with {
+                register(s -> messages1a << s)
+                register(s -> messages2 << s.toUpperCase())
+                notifyAll('one')
+            }
+            new Subject().with {
+                register(s -> messages1b << s)
+                register(s -> messages2 << s.toUpperCase())
+                notifyAll('two')
+            }
+            def expected = [['one'], ['two'], ['ONE', 'TWO']]
+            assert [messages1a, messages1b, messages2] == expected
+            // end::observer_lambdas[]
+        '''
+        assertScript '''
+            // tag::observer_bindable[]
+            import groovy.beans.*
+            import java.beans.*
+
+            class PersonBean {
+                @Bindable String first
+                @Bindable String last
+                @Vetoable Integer age
+            }
+
+            def messages = [:].withDefault{[]}
+            new PersonBean().with {
+                addPropertyChangeListener{ PropertyChangeEvent ev ->
+                    messages[ev.propertyName] << "prop: $ev.newValue"
+                }
+                addVetoableChangeListener{ PropertyChangeEvent ev ->
+                    def name = ev.propertyName
+                    if (name == 'age' && ev.newValue > 40)
+                        throw new PropertyVetoException()
+                    messages[name] << "veto: $ev.newValue"
+                }
+                first = 'John'
+                age = 35
+                last = 'Smith'
+                first = 'Jane'
+                age = 42
+            }
+
+            def expected = [
+                first:['prop: John', 'prop: Jane'],
+                age:['veto: 35'],
+                last:['prop: Smith']
+            ]
+            assert messages == expected
+            // end::observer_bindable[]
+        '''
+    }
+
     void testPimpMyLibraryExample() {
         shouldCompile '''
             // tag::pimp_my_library_example[]