mirror of
https://github.com/tiennm99/java-design-patterns.git
synced 2026-10-14 02:16:06 +00:00
docs: Adds explanation for MVP design pattern (#2303)
This commit is contained in:
1 parent
c097a5141b
commit
953f9a4e0c
1 file changed
+525
-3
@@ -10,6 +10,531 @@ tags:
|
|||||||
Apply a "Separation of Concerns" principle in a way that allows
|
Apply a "Separation of Concerns" principle in a way that allows
|
||||||
developers to build and test user interfaces.
|
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
|
||||||
|
|
||||||
|
> Model–view–presenter (MVP) is a derivation of the model–view–controller (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
|
## Class diagram
|
||||||

|

|
||||||
|
|
||||||
@@ -20,6 +545,3 @@ situations
|
|||||||
* When you want to improve the "Separation of Concerns" principle in presentation logic
|
* When you want to improve the "Separation of Concerns" principle in presentation logic
|
||||||
* When a user interface development and testing is necessary.
|
* When a user interface development and testing is necessary.
|
||||||
|
|
||||||
## Real world examples
|
|
||||||
|
|
||||||
* [MVP4J](https://github.com/amineoualialami/mvp4j)
|
|
||||||
Reference in new issue
Block a user