feat: implemented the bloC design pattern issue #2374 (#3128)

* added bloC design pattern

* added bloC design pattern

* added Readme file

* fixed checkstyle warnings

* added tests for the ui

* fixed a test in MainTest file

* separating ui from main file and adding more tests

* added pom.xml plugins and properties and fixed readme.md

* fixed renaming problem and added context to main

* chsnged state class to record

* syncing changes for conflicts

* Revert "fixed conflicts"

* restored files

* renamed readme file and abstracted pom file
This commit is contained in:
darkhyper24
2025-01-16 16:56:53 +02:00
committed by GitHub
parent 3a1a71405b
commit 21115f4568
14 changed files with 733 additions and 0 deletions
@@ -0,0 +1,80 @@
package com.iluwatar.bloc;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* The Bloc class is responsible for managing the current state and notifying registered listeners
* whenever the state changes. It implements the ListenerManager interface, allowing listeners
* to be added, removed, and notified of state changes.
*/
public class Bloc implements ListenerManager<State> {
private State currentState;
private final List<StateListener<State>> listeners = new ArrayList<>();
/**
* Constructs a new Bloc instance with an initial state of value 0.
*/
public Bloc() {
this.currentState = new State(0);
}
/**
* Adds a listener to receive state change notifications.
*
* @param listener the listener to add
*/
@Override
public void addListener(StateListener<State> listener) {
listeners.add(listener);
listener.onStateChange(currentState);
}
/**
* Removes a listener from receiving state change notifications.
*
* @param listener the listener to remove
*/
@Override
public void removeListener(StateListener<State> listener) {
listeners.remove(listener);
}
/**
* Returns an unmodifiable list of all registered listeners.
*
* @return an unmodifiable list of listeners
*/
@Override
public List<StateListener<State>> getListeners() {
return Collections.unmodifiableList(listeners);
}
/**
* Emits a new state and notifies all registered listeners of the change.
*
* @param newState the new state to emit
*/
private void emitState(State newState) {
currentState = newState;
for (StateListener<State> listener : listeners) {
listener.onStateChange(currentState);
}
}
/**
* Increments the current state value by 1 and notifies listeners of the change.
*/
public void increment() {
emitState(new State(currentState.value() + 1));
}
/**
* Decrements the current state value by 1 and notifies listeners of the change.
*/
public void decrement() {
emitState(new State(currentState.value() - 1));
}
}
@@ -0,0 +1,64 @@
package com.iluwatar.bloc;
import java.awt.BorderLayout;
import java.awt.Font;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingConstants;
import javax.swing.WindowConstants;
/**
* The BlocUI class handles the creation and management of the UI components.
*/
public class BlocUi {
/**
* Creates and shows the UI.
*/
public void createAndShowUi() {
// Create a Bloc instance to manage the state
final Bloc bloc = new Bloc();
// setting up a frame window with a title
JFrame frame = new JFrame("BloC example");
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.setSize(400, 300);
// label to display the counter value
JLabel counterLabel = new JLabel("Counter: 0", SwingConstants.CENTER);
counterLabel.setFont(new Font("Arial", Font.BOLD, 20));
// buttons for increment, decrement, and toggling listener
JButton decrementButton = new JButton("Decrement");
JButton toggleListenerButton = new JButton("Disable Listener");
JButton incrementButton = new JButton("Increment");
frame.setLayout(new BorderLayout());
frame.add(counterLabel, BorderLayout.CENTER);
frame.add(incrementButton, BorderLayout.NORTH);
frame.add(decrementButton, BorderLayout.SOUTH);
frame.add(toggleListenerButton, BorderLayout.EAST);
// making a state listener to update the counter label when the state changes
StateListener<State> stateListener = state -> counterLabel.setText("Counter: " + state.value());
// adding the listener to the Bloc instance
bloc.addListener(stateListener);
toggleListenerButton.addActionListener(e -> {
if (bloc.getListeners().contains(stateListener)) {
bloc.removeListener(stateListener);
toggleListenerButton.setText("Enable Listener");
} else {
bloc.addListener(stateListener);
toggleListenerButton.setText("Disable Listener");
}
});
incrementButton.addActionListener(e -> bloc.increment());
decrementButton.addActionListener(e -> bloc.decrement());
frame.setVisible(true);
}
}
@@ -0,0 +1,32 @@
package com.iluwatar.bloc;
import java.util.List;
/**
* Interface for managing listeners for state changes.
*
* @param <T> The type of state to be handled by the listeners.
*/
public interface ListenerManager<T> {
/**
* Adds a listener that will be notified of state changes.
*
* @param listener the listener to be added
*/
void addListener(StateListener<T> listener);
/**
* Removes a listener so that it no longer receives state change notifications.
*
* @param listener the listener to be removed
*/
void removeListener(StateListener<T> listener);
/**
* Returns a list of all listeners currently registered for state changes.
*
* @return a list of registered listeners
*/
List<StateListener<T>> getListeners();
}
@@ -0,0 +1,28 @@
package com.iluwatar.bloc;
/**
* The BLoC (Business Logic Component) pattern is a software design pattern primarily used
* in Flutter applications. It facilitates the separation of business logic from UI code,
* making the application more modular, testable, and scalable. The BLoC pattern uses streams
* to manage the flow of data and state changes, allowing widgets to react to new states as
* they arrive.
* In the BLoC pattern, the application is divided into three key components:
* - Input streams: Represent user interactions or external events fed into the BLoC.
* - Business logic: Processes the input and determines the resulting state or actions.
* - Output streams: Emit the updated state for the UI to consume.
* The BLoC pattern is especially useful in reactive programming scenarios and aligns well with the declarative nature of Flutter.
* By using this pattern, developers can ensure a clear separation of concerns, enhance reusability, and maintain consistent state management throughout the application.
*/
public class Main {
/**
* The entry point of the application. Initializes the GUI.
*
* @param args command-line arguments (not used in this example)
*/
public static void main(String[] args) {
BlocUi blocUi = new BlocUi();
blocUi.createAndShowUi();
}
}
@@ -0,0 +1,8 @@
package com.iluwatar.bloc;
/**
* The {@code State} class represents a state with an integer value.
* This class encapsulates the value and provides methods to retrieve it.
*/
public record State(int value) {
}
@@ -0,0 +1,17 @@
package com.iluwatar.bloc;
/**
* The {@code StateListener} interface defines the contract for listening to state changes.
* Implementations of this interface should handle state changes and define actions to take when the state changes.
*
* @param <T> the type of state that this listener will handle
*/
public interface StateListener<T> {
/**
* This method is called when the state has changed.
*
* @param state the updated state
*/
void onStateChange(T state);
}
@@ -0,0 +1,56 @@
package com.iluwatar.bloc;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.*;
class BlocTest {
private Bloc bloc;
private AtomicInteger stateValue;
@BeforeEach
void setUp() {
bloc = new Bloc();
stateValue = new AtomicInteger(0);
}
@Test
void initialState() {
assertTrue(bloc.getListeners().isEmpty(), "No listeners should be present initially.");
}
@Test
void IncrementUpdateState() {
bloc.addListener(state -> stateValue.set(state.value()));
bloc.increment();
assertEquals(1, stateValue.get(), "State should increment to 1");
}
@Test
void DecrementUpdateState() {
bloc.addListener(state -> stateValue.set(state.value()));
bloc.decrement();
assertEquals(-1, stateValue.get(), "State should decrement to -1");
}
@Test
void addingListener() {
bloc.addListener(state -> {});
assertEquals(1, bloc.getListeners().size(), "Listener count should be 1.");
}
@Test
void removingListener() {
StateListener<State> listener = state -> {};
bloc.addListener(listener);
bloc.removeListener(listener);
assertTrue(bloc.getListeners().isEmpty(), "Listener count should be 0 after removal.");
}
@Test
void multipleListeners() {
AtomicInteger secondValue = new AtomicInteger();
bloc.addListener(state -> stateValue.set(state.value()));
bloc.addListener(state -> secondValue.set(state.value()));
bloc.increment();
assertEquals(1, stateValue.get(), "First listener should receive state 1.");
assertEquals(1, secondValue.get(), "Second listener should receive state 1.");
}
}
@@ -0,0 +1,98 @@
package com.iluwatar.bloc;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import javax.swing.*;
import java.awt.*;
import static org.junit.Assert.assertEquals;
public class BlocUiTest {
private JFrame frame;
private JLabel counterLabel;
private JButton incrementButton;
private JButton decrementButton;
private JButton toggleListenerButton;
private Bloc bloc;
private StateListener<State> stateListener;
@Before
public void setUp() {
bloc = new Bloc(); // Re-initialize the Bloc for each test
frame = new JFrame("BloC example");
frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
frame.setSize(400, 300);
counterLabel = new JLabel("Counter: 0", SwingConstants.CENTER);
counterLabel.setFont(new Font("Arial", Font.BOLD, 20));
incrementButton = new JButton("Increment");
decrementButton = new JButton("Decrement");
toggleListenerButton = new JButton("Disable Listener");
frame.setLayout(new BorderLayout());
frame.add(counterLabel, BorderLayout.CENTER);
frame.add(incrementButton, BorderLayout.NORTH);
frame.add(decrementButton, BorderLayout.SOUTH);
frame.add(toggleListenerButton, BorderLayout.EAST);
stateListener = state -> counterLabel.setText("Counter: " + state.value());
bloc.addListener(stateListener);
incrementButton.addActionListener(e -> bloc.increment());
decrementButton.addActionListener(e -> bloc.decrement());
toggleListenerButton.addActionListener(e -> {
if (bloc.getListeners().contains(stateListener)) {
bloc.removeListener(stateListener);
toggleListenerButton.setText("Enable Listener");
} else {
bloc.addListener(stateListener);
toggleListenerButton.setText("Disable Listener");
}
});
frame.setVisible(true);
}
@After
public void tearDown() {
frame.dispose();
bloc = new Bloc(); // Reset Bloc state after each test to avoid state carryover
}
@Test
public void testIncrementButton() {
simulateButtonClick(incrementButton);
assertEquals("Counter: 1", counterLabel.getText());
}
@Test
public void testDecrementButton() {
simulateButtonClick(decrementButton);
assertEquals("Counter: -1", counterLabel.getText());
}
@Test
public void testToggleListenerButton() {
// Disable listener
simulateButtonClick(toggleListenerButton);
simulateButtonClick(incrementButton);
assertEquals("Counter: 0", counterLabel.getText()); // Listener is disabled
// Enable listener
simulateButtonClick(toggleListenerButton);
simulateButtonClick(incrementButton);
assertEquals("Counter: 2", counterLabel.getText()); // Listener is re-enabled
}
private void simulateButtonClick(JButton button) {
for (var listener : button.getActionListeners()) {
listener.actionPerformed(null);
}
}
}
@@ -0,0 +1,20 @@
package com.iluwatar.bloc;
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.mockStatic;
class MainTest {
@Test
void testMain() {
try (var mockedBlocUi = mockStatic(BlocUi.class)) {
// Call the main method
Main.main(new String[]{});
// Verify that createAndShowUi was called
mockedBlocUi.verify(() -> new BlocUi().createAndShowUi());
}
}
}