diff --git a/README.md b/README.md index f47252648..8463ec21d 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,15 @@ * a snapshot of an object's state must be saved so that it can be restored to that state later, and * a direct interface to obtaining the state would expose implementation details and break the object's encapsulation +##Model-View-Presenter +**Intent:** Apply a "Separation of Concerns" principle in a way that allows developers to build and test user interfaces. + +![alt text](https://github.com/pitsios-s/java-design-patterns/blob/master/model-view-presenter/etc/model-view-presenter.jpg "Model-View-Presenter") + +**Applicability:** Use the Model-View-Presenter in any of the following situations +* when you want to improve the "Separation of Concerns" principle in presentation logic +* when a user interface development and testing is necessary. + ##Observer **Intent:** Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. diff --git a/model-view-presenter/etc/data/test.txt b/model-view-presenter/etc/data/test.txt new file mode 100644 index 000000000..997ae361a --- /dev/null +++ b/model-view-presenter/etc/data/test.txt @@ -0,0 +1,2 @@ +Test line 1 +Test line 2 \ No newline at end of file diff --git a/model-view-presenter/etc/model-view-presenter.jpg b/model-view-presenter/etc/model-view-presenter.jpg new file mode 100644 index 000000000..a51c914d0 Binary files /dev/null and b/model-view-presenter/etc/model-view-presenter.jpg differ diff --git a/model-view-presenter/pom.xml b/model-view-presenter/pom.xml new file mode 100644 index 000000000..ef6a43260 --- /dev/null +++ b/model-view-presenter/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + + com.iluwatar + java-design-patterns + 1.0-SNAPSHOT + + com.iluwatar + model-view-presenter + 1.0-SNAPSHOT + model-view-presenter + http://maven.apache.org + + + junit + junit + 4.11 + test + + + net.java.dev.swing-layout + swing-layout + 1.0.2 + + + diff --git a/model-view-presenter/src/main/java/com/iluwatar/FileLoader.java b/model-view-presenter/src/main/java/com/iluwatar/FileLoader.java new file mode 100644 index 000000000..91c45ecca --- /dev/null +++ b/model-view-presenter/src/main/java/com/iluwatar/FileLoader.java @@ -0,0 +1,80 @@ +package com.iluwatar; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; + +/** + * Every instance of this class represents the Model component + * in the Model-View-Presenter architectural pattern. + * + * It is responsible for reading and loading the contents of a given file. + */ +public class FileLoader { + + /** + * Indicates if the file is loaded or not. + */ + private boolean loaded = false; + + /** + * The name of the file that we want to load. + */ + private String fileName; + + /** + * Loads the data of the file specified. + */ + public String loadData() { + try { + BufferedReader br = new BufferedReader(new FileReader(new File(this.fileName))); + String text = ""; + String line = ""; + + while( (line = br.readLine()) != null ) { + text += line + "\n"; + } + + this.loaded = true; + br.close(); + + return text; + } + + catch(Exception e) { + e.printStackTrace(); + } + + return null; + } + + /** + * Sets the path of the file to be loaded, to the given value. + * + * @param fileName The path of the file to be loaded. + */ + public void setFileName(String fileName) { + this.fileName = fileName; + } + + /** + * @return fileName The path of the file to be loaded. + */ + public String getFileName() { + return this.fileName; + } + + /** + * @return True, if the file given exists, false otherwise. + */ + public boolean fileExists() { + return new File(this.fileName).exists(); + } + + /** + * @return True, if the file is loaded, false otherwise. + */ + public boolean isLoaded() { + return this.loaded; + } +} diff --git a/model-view-presenter/src/main/java/com/iluwatar/FileSelectorJFrame.java b/model-view-presenter/src/main/java/com/iluwatar/FileSelectorJFrame.java new file mode 100644 index 000000000..231128ca2 --- /dev/null +++ b/model-view-presenter/src/main/java/com/iluwatar/FileSelectorJFrame.java @@ -0,0 +1,200 @@ +package com.iluwatar; + +import java.awt.Color; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.JTextField; + +/** + * This class is the GUI implementation of the View component + * In the Model-View-Presenter pattern. + */ +public class FileSelectorJFrame extends JFrame implements FileSelectorView, ActionListener { + + /** + * Default serial version ID. + */ + private static final long serialVersionUID = 1L; + + /** + * The "OK" button for loading the file. + */ + private JButton OK; + + /** + * The cancel button. + */ + private JButton cancel; + + /** + * The information label. + */ + private JLabel info; + + /** + * The contents label. + */ + private JLabel contents; + + /** + * The text field for giving the name of the file + * that we want to open. + */ + private JTextField input; + + /** + * A text area that will keep the contents of the file opened. + */ + private JTextArea area; + + /** + * The panel that will hold our widgets. + */ + private JPanel panel; + + /** + * The Presenter component that the frame will interact with + */ + private FileSelectorPresenter presenter; + + /** + * The name of the file that we want to read it's contents. + */ + private String fileName; + + /** + * Constructor. + */ + public FileSelectorJFrame() { + super("File Loader"); + this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + this.setLayout(null); + this.setBounds(100, 100, 500, 200); + + /* + * Add the panel. + */ + this.panel = new JPanel(); + panel.setLayout(null); + this.add(panel); + panel.setBounds(0, 0, 500, 200); + panel.setBackground(Color.LIGHT_GRAY); + + /* + * Add the info label. + */ + this.info = new JLabel("File Name :"); + this.panel.add(info); + info.setBounds(30, 10, 100, 30); + + /* + * Add the contents label. + */ + this.contents = new JLabel("File contents :"); + this.panel.add(contents); + this.contents.setBounds(30, 100, 120, 30); + + /* + * Add the text field. + */ + this.input = new JTextField(100); + this.panel.add(input); + this.input.setBounds(150, 15, 200, 20); + + /* + * Add the text area. + */ + this.area = new JTextArea(100, 100); + JScrollPane pane = new JScrollPane(area); + pane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); + pane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); + this.panel.add(pane); + this.area.setEditable(false); + pane.setBounds(150, 100, 250, 80); + + /* + * Add the OK button. + */ + this.OK = new JButton("OK"); + this.panel.add(OK); + this.OK.setBounds(250, 50, 100, 25); + this.OK.addActionListener(this); + + /* + * Add the cancel button. + */ + this.cancel = new JButton("Cancel"); + this.panel.add(this.cancel); + this.cancel.setBounds(380, 50, 100, 25); + this.cancel.addActionListener(this); + + this.presenter = null; + this.fileName = null; + } + + @Override + public void actionPerformed(ActionEvent e) { + if(e.getSource() == this.OK) { + this.fileName = this.input.getText(); + presenter.fileNameChanged(); + presenter.confirmed(); + } + + else if(e.getSource() == this.cancel) { + presenter.cancelled(); + } + } + + @Override + public void open() { + this.setVisible(true); + } + + @Override + public void close() { + this.dispose(); + } + + @Override + public boolean isOpened() { + return this.isVisible(); + } + + @Override + public void setPresenter(FileSelectorPresenter presenter) { + this.presenter = presenter; + } + + @Override + public FileSelectorPresenter getPresenter() { + return this.presenter; + } + + @Override + public void setFileName(String name) { + this.fileName = name; + } + + @Override + public String getFileName() { + return this.fileName; + } + + @Override + public void showMessage(String message) { + JOptionPane.showMessageDialog(null, message); + } + + @Override + public void displayData(String data) { + this.area.setText(data); + } +} diff --git a/model-view-presenter/src/main/java/com/iluwatar/FileSelectorPresenter.java b/model-view-presenter/src/main/java/com/iluwatar/FileSelectorPresenter.java new file mode 100644 index 000000000..3d1a22010 --- /dev/null +++ b/model-view-presenter/src/main/java/com/iluwatar/FileSelectorPresenter.java @@ -0,0 +1,76 @@ +package com.iluwatar; + +/** + * Every instance of this class represents the Presenter component + * in the Model-View-Presenter architectural pattern. + * + * It is responsible for reacting to the user's actions and update the View component. + */ +public class FileSelectorPresenter { + + /** + * The View component that the presenter interacts with. + */ + private FileSelectorView view; + + /** + * The Model component that the presenter interacts with. + */ + private FileLoader loader; + + /** + * Constructor + * + * @param view The view component that the presenter will interact with. + */ + public FileSelectorPresenter(FileSelectorView view) { + this.view = view; + } + + /** + * Sets the FileLoader object, to the value given as parameter. + * + * @param loader The new FileLoader object(the Model component). + */ + public void setLoader(FileLoader loader) { + this.loader = loader; + } + + /** + * Starts the presenter. + */ + public void start() { + view.setPresenter(this); + view.open(); + } + + /** + * An "event" that fires when the name of the file to be loaded changes. + */ + public void fileNameChanged() { + loader.setFileName(view.getFileName()); + } + + public void confirmed() { + if(loader.getFileName() == null || loader.getFileName().equals("")) { + view.showMessage("Please give the name of the file first!"); + return; + } + + if(loader.fileExists()) { + String data = loader.loadData(); + view.displayData(data); + } + + else { + view.showMessage("The file specified does not exist."); + } + } + + /** + * Cancels the file loading process. + */ + public void cancelled() { + view.close(); + } +} diff --git a/model-view-presenter/src/main/java/com/iluwatar/FileSelectorStub.java b/model-view-presenter/src/main/java/com/iluwatar/FileSelectorStub.java new file mode 100644 index 000000000..a4b6cabf8 --- /dev/null +++ b/model-view-presenter/src/main/java/com/iluwatar/FileSelectorStub.java @@ -0,0 +1,109 @@ +package com.iluwatar; + +/** + * Every instance of this class represents the Stub component in + * the Model-View-Presenter architectural pattern. + * + * The stub implements the View interface and it is useful when + * we want the test the reaction to user events, such as mouse clicks. + * + * Since we can not test the GUI directly, the MVP pattern provides + * this functionality through the View's dummy implementation, the Stub. + */ +public class FileSelectorStub implements FileSelectorView { + + /** + * Indicates whether or not the view is opened. + */ + private boolean opened; + + /** + * The presenter Component. + */ + private FileSelectorPresenter presenter; + + /** + * The current name of the file. + */ + private String name; + + /** + * Indicates the number of messages that were "displayed" to the user. + */ + private int numOfMessageSent; + + /** + * Indicates if the data of the file where displayed or not. + */ + private boolean dataDisplayed; + + /** + * Constructor + */ + public FileSelectorStub() { + this.opened = false; + this.presenter = null; + this.name = ""; + this.numOfMessageSent = 0; + this.dataDisplayed = false; + } + + @Override + public void open() { + this.opened = true; + } + + @Override + public void setPresenter(FileSelectorPresenter presenter) { + this.presenter = presenter; + } + + @Override + public boolean isOpened() { + return this.opened; + } + + @Override + public FileSelectorPresenter getPresenter() { + return this.presenter; + } + + @Override + public String getFileName() { + return this.name; + } + + @Override + public void setFileName(String name) { + this.name = name; + } + + @Override + public void showMessage(String message) { + this.numOfMessageSent++; + } + + @Override + public void close() { + this.opened = false; + } + + @Override + public void displayData(String data) { + this.dataDisplayed = true; + } + + /** + * Returns the number of messages that were displayed to the user. + */ + public int getMessagesSent() { + return this.numOfMessageSent; + } + + /** + * @return True if the data where displayed, false otherwise. + */ + public boolean dataDisplayed() { + return this.dataDisplayed; + } +} diff --git a/model-view-presenter/src/main/java/com/iluwatar/FileSelectorView.java b/model-view-presenter/src/main/java/com/iluwatar/FileSelectorView.java new file mode 100644 index 000000000..deb1bd841 --- /dev/null +++ b/model-view-presenter/src/main/java/com/iluwatar/FileSelectorView.java @@ -0,0 +1,62 @@ +package com.iluwatar; + +/** + * This interface represents the View component in the + * Model-View-Presenter pattern. It can be implemented + * by either the GUI components, or by the Stub. + */ +public interface FileSelectorView { + + /** + * Opens the view. + */ + public void open(); + + /** + * Closes the view. + */ + public void close(); + + /** + * @return True, if the view is opened, false otherwise. + */ + public boolean isOpened(); + + /** + * Sets the presenter component, to the one given as parameter. + * + * @param presenter The new presenter component. + */ + public void setPresenter(FileSelectorPresenter presenter); + + /** + * @return The presenter Component. + */ + public FileSelectorPresenter getPresenter(); + + /** + * Sets the file's name, to the value given as parameter. + * + * @param name The new name of the file. + */ + public void setFileName(String name); + + /** + * @return The name of the file. + */ + public String getFileName(); + + /** + * Displays a message to the users. + * + * @param message The message to be displayed. + */ + public void showMessage(String message); + + /** + * Displays the data to the view. + * + * @param data The data to be written. + */ + public void displayData(String data); +} diff --git a/model-view-presenter/src/main/java/com/iluwatar/MainApp.java b/model-view-presenter/src/main/java/com/iluwatar/MainApp.java new file mode 100644 index 000000000..cd342d539 --- /dev/null +++ b/model-view-presenter/src/main/java/com/iluwatar/MainApp.java @@ -0,0 +1,12 @@ +package com.iluwatar; + +public class MainApp { + + public static void main(String[] args) { + FileLoader loader = new FileLoader(); + FileSelectorJFrame jFrame = new FileSelectorJFrame(); + FileSelectorPresenter presenter = new FileSelectorPresenter(jFrame); + presenter.setLoader(loader); + presenter.start(); + } +} diff --git a/model-view-presenter/src/test/java/com/iluwatar/FileSelectorPresenterTest.java b/model-view-presenter/src/test/java/com/iluwatar/FileSelectorPresenterTest.java new file mode 100644 index 000000000..507cad251 --- /dev/null +++ b/model-view-presenter/src/test/java/com/iluwatar/FileSelectorPresenterTest.java @@ -0,0 +1,122 @@ +package com.iluwatar; + +import static org.junit.Assert.*; + +import org.junit.Before; +import org.junit.Test; + +/** + * This test case is responsible for testing our application + * by taking advantage of the Model-View-Controller architectural pattern. + */ +public class FileSelectorPresenterTest { + + /** + * The Presenter component. + */ + private FileSelectorPresenter presenter; + + /** + * The View component, implemented this time as a Stub!!! + */ + private FileSelectorStub stub; + + /** + * The Model component. + */ + private FileLoader loader; + + + /** + * Initializes the components of the test case. + */ + @Before + public void setUp() { + this.stub = new FileSelectorStub(); + this.loader = new FileLoader(); + presenter = new FileSelectorPresenter(this.stub); + presenter.setLoader(loader); + } + + /** + * Tests if the Presenter was successfully connected with the View. + */ + @Test + public void wiring() { + presenter.start(); + + assertNotNull(stub.getPresenter()); + assertTrue(stub.isOpened()); + } + + /** + * Tests if the name of the file changes. + */ + @Test + public void updateFileNameToLoader() { + String EXPECTED_FILE = "Stamatis"; + stub.setFileName(EXPECTED_FILE); + + presenter.start(); + presenter.fileNameChanged(); + + assertEquals(EXPECTED_FILE, loader.getFileName()); + } + + /** + * Tests if we receive a confirmation when we attempt to open a file + * that it's name is null or an empty string. + */ + @Test + public void fileConfirmationWhenNameIsNull() { + stub.setFileName(null); + + presenter.start(); + presenter.fileNameChanged(); + presenter.confirmed(); + + assertFalse(loader.isLoaded()); + assertEquals(1, stub.getMessagesSent()); + } + + /** + * Tests if we receive a confirmation when we attempt to open a file + * that it doesn't exist. + */ + @Test + public void fileConfirmationWhenFileDoesNotExist() { + stub.setFileName("RandomName.txt"); + + presenter.start(); + presenter.fileNameChanged(); + presenter.confirmed(); + + assertFalse(loader.isLoaded()); + assertEquals(1, stub.getMessagesSent()); + } + + /** + * Tests if we can open the file, when it exists. + */ + @Test + public void fileConfirmationWhenFileExists() { + stub.setFileName("etc/data/test.txt"); + presenter.start(); + presenter.fileNameChanged(); + presenter.confirmed(); + + assertTrue(loader.isLoaded()); + assertTrue(stub.dataDisplayed()); + } + + /** + * Tests if the view closes after cancellation. + */ + @Test + public void cancellation() { + presenter.start(); + presenter.cancelled(); + + assertFalse(stub.isOpened()); + } +} diff --git a/pom.xml b/pom.xml index 4da45cbe6..6f43e6cc7 100644 --- a/pom.xml +++ b/pom.xml @@ -36,6 +36,7 @@ iterator mediator memento + model-view-presenter observer state strategy @@ -59,4 +60,4 @@ - \ No newline at end of file +