docs: Adds explanation for MVP design pattern (#2303)

This commit is contained in:
prasad-333
2022-11-13 12:49:39 +05:30
committed by GitHub
parent c097a5141b
commit 953f9a4e0c
+525 -3
View File
@@ -10,6 +10,531 @@ tags:
Apply a "Separation of Concerns" principle in a way that allows
developers to build and test user interfaces.
## Explanation
Real-world example
> Consider File selection application that allows to select a file from storage.
> File selection logic is completely separated from user interface implementation.
In plain words
> It separates the UI completely from service/domain layer into Presenter.
Wikipedia says
> Modelviewpresenter (MVP) is a derivation of the modelviewcontroller (MVC) architectural pattern,
> and is used mostly for building user interfaces.
**Programmatic example**
Let's understand a simple file selection application build in AWT/Swing.
`FileLoader` reads & loads contain of given file. It represents the model component of MVP.
```java
public class FileLoader implements Serializable {
/**
* Generated serial version UID.
*/
private static final long serialVersionUID = -4745803872902019069L;
private static final Logger LOGGER = LoggerFactory.getLogger(FileLoader.class);
/**
* Indicates if the file is loaded or not.
*/
private boolean loaded;
/**
* The name of the file that we want to load.
*/
private String fileName;
/**
* Loads the data of the file specified.
*/
public String loadData() {
var dataFileName = this.fileName;
try (var br = new BufferedReader(new FileReader(new File(dataFileName)))) {
var result = br.lines().collect(Collectors.joining("\n"));
this.loaded = true;
return result;
} catch (Exception e) {
LOGGER.error("File {} does not exist", dataFileName);
}
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;
}
/**
* Gets the path of the file to be loaded.
*
* @return fileName The path of the file to be loaded.
*/
public String getFileName() {
return this.fileName;
}
/**
* Returns true if the given file exists.
*
* @return True, if the file given exists, false otherwise.
*/
public boolean fileExists() {
return new File(this.fileName).exists();
}
/**
* Returns true if the given file is loaded.
*
* @return True, if the file is loaded, false otherwise.
*/
public boolean isLoaded() {
return this.loaded;
}
}
```
`FileSelectorView` interface represents the View component in the MVP pattern. It can be
implemented by either the GUI components, or by the Stub. This is how it eases the UI testing.
```java
public interface FileSelectorView extends Serializable {
/**
* Opens the view.
*/
void open();
/**
* Closes the view.
*/
void close();
/**
* Returns true if view is opened.
*
* @return True, if the view is opened, false otherwise.
*/
boolean isOpened();
/**
* Sets the presenter component, to the one given as parameter.
*
* @param presenter The new presenter component.
*/
void setPresenter(FileSelectorPresenter presenter);
/**
* Gets presenter component.
*
* @return The presenter Component.
*/
FileSelectorPresenter getPresenter();
/**
* Sets the file's name, to the value given as parameter.
*
* @param name The new name of the file.
*/
void setFileName(String name);
/**
* Gets the name of file.
*
* @return The name of the file.
*/
String getFileName();
/**
* Displays a message to the users.
*
* @param message The message to be displayed.
*/
void showMessage(String message);
/**
* Displays the data to the view.
*
* @param data The data to be written.
*/
void displayData(String data);
}
```
`FileSelectorJFrame` represents the GUI implementation of the View component in the MVP pattern.
```java
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 final JButton ok;
/**
* The cancel button.
*/
private final JButton cancel;
/**
* The text field for giving the name of the file that we want to open.
*/
private final JTextField input;
/**
* A text area that will keep the contents of the file opened.
*/
private final JTextArea area;
/**
* 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(EXIT_ON_CLOSE);
this.setLayout(null);
this.setBounds(100, 100, 500, 200);
/*
* Add the panel.
*/
var panel = new JPanel();
panel.setLayout(null);
this.add(panel);
panel.setBounds(0, 0, 500, 200);
panel.setBackground(Color.LIGHT_GRAY);
/*
* Add the info label.
*/
var info = new JLabel("File Name :");
panel.add(info);
info.setBounds(30, 10, 100, 30);
/*
* Add the contents label.
*/
var contents = new JLabel("File contents :");
panel.add(contents);
contents.setBounds(30, 100, 120, 30);
/*
* Add the text field.
*/
this.input = new JTextField(100);
panel.add(input);
this.input.setBounds(150, 15, 200, 20);
/*
* Add the text area.
*/
this.area = new JTextArea(100, 100);
var pane = new JScrollPane(area);
pane.setHorizontalScrollBarPolicy(HORIZONTAL_SCROLLBAR_AS_NEEDED);
pane.setVerticalScrollBarPolicy(VERTICAL_SCROLLBAR_AS_NEEDED);
panel.add(pane);
this.area.setEditable(false);
pane.setBounds(150, 100, 250, 80);
/*
* Add the OK button.
*/
this.ok = new JButton("OK");
panel.add(ok);
this.ok.setBounds(250, 50, 100, 25);
this.ok.addActionListener(this);
/*
* Add the cancel button.
*/
this.cancel = new JButton("Cancel");
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 (this.ok.equals(e.getSource())) {
this.fileName = this.input.getText();
presenter.fileNameChanged();
presenter.confirmed();
} else if (this.cancel.equals(e.getSource())) {
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);
}
}
```
`FileSelectorStub` is a stub that implements the View interface and it is useful when we want to test the reaction to
user events, such as mouse clicks etc.
```java
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;
}
/**
* Returns true, if the data were displayed.
*
* @return True if the data where displayed, false otherwise.
*/
public boolean dataDisplayed() {
return this.dataDisplayed;
}
}
```
`FileSelectorPresenter` represents the Presenter component in the MVP pattern.
It is responsible for reacting to the user's actions and update the View component.
```java
public class FileSelectorPresenter implements Serializable {
/**
* Generated serial version UID.
*/
private static final long serialVersionUID = 1210314339075855074L;
/**
* The View component that the presenter interacts with.
*/
private final 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 {@link FileLoader} object, to the value given as parameter.
*
* @param loader The new {@link 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());
}
/**
* Ok button handler.
*/
public void confirmed() {
if (loader.getFileName() == null || loader.getFileName().equals("")) {
view.showMessage("Please give the name of the file first!");
return;
}
if (loader.fileExists()) {
var 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();
}
}
```
Below code reflects how we wire-up the Presenter & the View and the Presenter & the Model.
```java
var loader = new FileLoader();
var frame = new FileSelectorJFrame();
var presenter = new FileSelectorPresenter(frame);
presenter.setLoader(loader);
presenter.start();
```
## Class diagram
![alt text](./etc/model-view-presenter_1.png "Model-View-Presenter")
@@ -20,6 +545,3 @@ situations
* When you want to improve the "Separation of Concerns" principle in presentation logic
* When a user interface development and testing is necessary.
## Real world examples
* [MVP4J](https://github.com/amineoualialami/mvp4j)