diff --git a/binding-property/README.md b/binding-property/README.md new file mode 100644 index 000000000000..7babb2d6ca00 --- /dev/null +++ b/binding-property/README.md @@ -0,0 +1,61 @@ +# Binding Properties Pattern + +## Intent +Synchronize state between objects with automatic updates, enabling one-way or two-way data binding. + +## Explanation + +This pattern is used to ensure that changes in one component (like a data model) are reflected in another component (like a UI element) automatically, and vice versa. + +### Participants +1. **ObservableProperty**: Holds the value and notifies observers when it changes. +2. **Observer**: Defines the interface for objects that respond to observable property changes. +3. **TextView**: Implements the `Observer` interface and simulates a UI component. + +### Applicability +- Synchronizing state between multiple objects. +- Minimizing boilerplate code for updates. + +### Example Code +```java + +public class ObservableProperty { + private T value; + private final List> observers = new ArrayList<>(); + private boolean isUpdating = false; + + public ObservableProperty(T initialValue) { + this.value = initialValue; + } + + public T getValue() { + return value; + } + + public void setValue(T value) { + if (isUpdating || this.value == value || (this.value != null && this.value.equals(value))) { + return; + } + this.value = value; + notifyObservers(); + } + + public void addObserver(Observer observer) { + observers.add(observer); + observer.bind(this); + } + + public void removeObserver(Observer observer) { + observers.remove(observer); + observer.unbind(); + } + + private void notifyObservers() { + for (Observer observer : observers) { + isUpdating = true; + observer.update(value); + isUpdating = false; + } + } +} +// TextView reacts to the change diff --git a/binding-property/etc/binding.puml b/binding-property/etc/binding.puml new file mode 100644 index 000000000000..dbda794f9736 --- /dev/null +++ b/binding-property/etc/binding.puml @@ -0,0 +1,35 @@ +@startuml +title Two-Way Binding Properties Pattern - Class Diagram + +class ObservableProperty { + - value : T + - observers : List> + - isUpdating : boolean + + ObservableProperty(initialValue : T) + + getValue() : T + + setValue(value : T) + + addObserver(observer : Observer) + + removeObserver(observer : Observer) + - notifyObservers() +} + +interface Observer { + + update(newValue : T) + + bind(observableProperty : ObservableProperty) + + unbind() +} + +class TextView { + - text : String + - observableProperty : ObservableProperty + + update(newValue : String) + + bind(observableProperty : ObservableProperty) + + unbind() + + setText(newText : String) + + getText() : String +} + +ObservableProperty "1" o-- "*" Observer +Observer <|-- TextView + +@enduml diff --git a/binding-property/pom.xml b/binding-property/pom.xml new file mode 100644 index 000000000000..2b84b70060aa --- /dev/null +++ b/binding-property/pom.xml @@ -0,0 +1,67 @@ + + + + 4.0.0 + + com.iluwatar + java-design-patterns + 1.26.0-SNAPSHOT + + bridge + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.mockito + mockito-core + test + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + + + + com.iluwatar.binding.App + + + + + + + + + diff --git a/binding-property/src/main/java/com/iluwatar/binding/App.java b/binding-property/src/main/java/com/iluwatar/binding/App.java new file mode 100644 index 000000000000..d69c0727ac3e --- /dev/null +++ b/binding-property/src/main/java/com/iluwatar/binding/App.java @@ -0,0 +1,23 @@ +package com.iluwatar.binding; + +public class App { + public static void main(String[] args) { + ObservableProperty observableProperty = new ObservableProperty<>("Initial Value"); + TextView textView = new TextView(); + + // Bind TextView to ObservableProperty + observableProperty.addObserver(textView); + + // Update ObservableProperty + System.out.println("Setting value in ObservableProperty..."); + observableProperty.setValue("Hello, Design Patterns!"); + + // Simulate user input through TextView + System.out.println("User updates TextView..."); + textView.setText("User Input!"); + + // Set another value in ObservableProperty to observe two-way binding + System.out.println("Setting another value in ObservableProperty..."); + observableProperty.setValue("Two-Way Binding Works!"); + } +} diff --git a/binding-property/src/main/java/com/iluwatar/binding/ObservableProperty.java b/binding-property/src/main/java/com/iluwatar/binding/ObservableProperty.java new file mode 100644 index 000000000000..a809c88d50ad --- /dev/null +++ b/binding-property/src/main/java/com/iluwatar/binding/ObservableProperty.java @@ -0,0 +1,44 @@ +package com.iluwatar.binding; + +import java.util.ArrayList; +import java.util.List; + +public class ObservableProperty { + private T value; + private final List> observers = new ArrayList<>(); + private boolean isUpdating = false; + + public ObservableProperty(T initialValue) { + this.value = initialValue; + } + + public T getValue() { + return value; + } + + public void setValue(T value) { + if (isUpdating || this.value == value || (this.value != null && this.value.equals(value))) { + return; + } + this.value = value; + notifyObservers(); + } + + public void addObserver(Observer observer) { + observers.add(observer); + observer.bind(this); + } + + public void removeObserver(Observer observer) { + observers.remove(observer); + observer.unbind(); + } + + private void notifyObservers() { + for (Observer observer : observers) { + isUpdating = true; + observer.update(value); + isUpdating = false; + } + } +} diff --git a/binding-property/src/main/java/com/iluwatar/binding/Observer.java b/binding-property/src/main/java/com/iluwatar/binding/Observer.java new file mode 100644 index 000000000000..503bc19dea4d --- /dev/null +++ b/binding-property/src/main/java/com/iluwatar/binding/Observer.java @@ -0,0 +1,9 @@ +package com.iluwatar.binding; + +public interface Observer { + void update(T newValue); + + void bind(ObservableProperty observableProperty); + + void unbind(); +} diff --git a/binding-property/src/main/java/com/iluwatar/binding/TextView.java b/binding-property/src/main/java/com/iluwatar/binding/TextView.java new file mode 100644 index 000000000000..604fef3f8452 --- /dev/null +++ b/binding-property/src/main/java/com/iluwatar/binding/TextView.java @@ -0,0 +1,44 @@ +package com.iluwatar.binding; + +public class TextView implements Observer { + private String text; + private ObservableProperty boundProperty; + + @Override + public void update(String newValue) { + // Prevent unnecessary updates + if (text != null && text.equals(newValue)) { + return; + } + this.text = newValue; + System.out.println("TextView updated: " + text); + } + + @Override + public void bind(ObservableProperty observableProperty) { + // Unbind from the current property if already bound + if (this.boundProperty != null) { + this.boundProperty.removeObserver(this); + } + this.boundProperty = observableProperty; + } + + @Override + public void unbind() { + if (this.boundProperty != null) { + this.boundProperty.removeObserver(this); + this.boundProperty = null; + } + } + + public void setText(String newText) { + if (boundProperty != null) { + boundProperty.setValue(newText); + } + this.text = newText; + } + + public String getText() { + return text; + } +} diff --git a/binding-property/src/test/java/com/iluwatar/binding/AppTest.java b/binding-property/src/test/java/com/iluwatar/binding/AppTest.java new file mode 100644 index 000000000000..b81901330b76 --- /dev/null +++ b/binding-property/src/test/java/com/iluwatar/binding/AppTest.java @@ -0,0 +1,16 @@ +package com.iluwatar.binding; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +public class AppTest { + + @Test + public void testAppRunsWithoutExceptions() { + assertDoesNotThrow(() -> { + com.iluwatar.binding.App.main(new String[]{}); + }); + } +} + diff --git a/binding-property/src/test/java/com/iluwatar/binding/ObservablePropertyTest.java b/binding-property/src/test/java/com/iluwatar/binding/ObservablePropertyTest.java new file mode 100644 index 000000000000..cd43c79e25ea --- /dev/null +++ b/binding-property/src/test/java/com/iluwatar/binding/ObservablePropertyTest.java @@ -0,0 +1,41 @@ +package com.iluwatar.binding; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +public class ObservablePropertyTest { + + @Test + public void testNotifyObservers() { + ObservableProperty observableProperty = new ObservableProperty<>("Initial"); + TestObserver testObserver = new TestObserver(); + observableProperty.addObserver(testObserver); + + observableProperty.setValue("Updated"); + + assertEquals("Updated", testObserver.getReceivedValue()); + } + + private static class TestObserver implements Observer { + private String receivedValue; + + @Override + public void update(String newValue) { + receivedValue = newValue; + } + + @Override + public void bind(ObservableProperty observableProperty) { + // No binding logic needed for this test. + } + + @Override + public void unbind() { + // No unbinding logic needed for this test. + } + + public String getReceivedValue() { + return receivedValue; + } + } +} diff --git a/binding-property/src/test/java/com/iluwatar/binding/TextViewTest.java b/binding-property/src/test/java/com/iluwatar/binding/TextViewTest.java new file mode 100644 index 000000000000..fdcce02642be --- /dev/null +++ b/binding-property/src/test/java/com/iluwatar/binding/TextViewTest.java @@ -0,0 +1,35 @@ +package com.iluwatar.binding; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +public class TextViewTest { + + @Test + public void testTextViewUpdatesFromObservable() { + ObservableProperty observableProperty = new ObservableProperty<>("Initial"); + TextView textView = new TextView(); + + // Bind TextView to ObservableProperty + observableProperty.addObserver(textView); + + // Change observable value + observableProperty.setValue("New Value"); + + assertEquals("New Value", textView.getText()); + } + + @Test + public void testTextViewUpdatesObservable() { + ObservableProperty observableProperty = new ObservableProperty<>("Initial"); + TextView textView = new TextView(); + + // Bind TextView to ObservableProperty + observableProperty.addObserver(textView); + + // Simulate user updating TextView + textView.setText("User Input"); + + assertEquals("User Input", observableProperty.getValue()); + } +} diff --git a/pom.xml b/pom.xml index db06c59a9479..617b2cfb8843 100644 --- a/pom.xml +++ b/pom.xml @@ -66,6 +66,7 @@ prototype singleton adapter + binding-property bridge composite data-access-object