This is the structure of the GoF design pattern series from the perspective of problems.
before
FooAPI fooAPI = new FooAPI(lat, lng);
FooPlace place = new FooPlace();
place.setAddress(fooAPI.getPostalCode() + " " + fooAPI.getAddress()); // 〒012-3456 Hoge City, Hoge Prefecture
place.setStation(
new StringJoiner(",")
.add(fooAPI.getStation1())
.add(fooAPI.getStation2())
.add(fooAPI.getStation3())
.toString()); //That station,Kakiku station,Sashisu Station
This is a case where the result of API is formatted and used. This is the only place where the FooAPI is used, and if the shaping method does not change in the future, this is fine, but considering maintainability, it is not a good design.
after :bulb: Adapter
public class FooAPIAdapter {
private final FooAPI fooAPI;
public FooAPIAdapter(double latitude, double longitude) {
this.fooAPI = new FooAPI(latitude, longitude);
}
public String getAddress() {
return fooAPI.getPostalCode() + fooAPI.getAddress();
}
public String getStation() {
return new StringJoiner(",")
.add(fooAPI.getStation1())
.add(fooAPI.getStation2())
.add(fooAPI.getStation3())
.toString();
}
}
FooAPIAdapter fooAPI = new FooAPIAdapter(lat, lng);
FooPlace place = new FooPlace();
place.setAddress(fooAPI.getAddress());
place.setStation(fooAPI.getStation());
We have prepared an Adapter to format the API response. If there is a change in the formatting method, you only need to modify this Adapter and it will not affect the user.
before
public class FooSorter {
private List<FooStudent> students = new List<>();
public void add(FooStudent student) {
students.add(student);
}
public void sort() {
students.sort(
Comparator.comparingInt(
student -> student.getJapaneseScore()
+ student.getMathScore()
+ student.getEnglishScore())
.reversed());
}
public List<FooStudent> getResult() {
return students;
}
}
FooSorter sorter = new FooSorter();
sorter.add(student1);
sorter.add(student2);
sorter.add(student3);
sorter.sort();
sorter.getResult();
The bad point is that the user must know the procedure of instance creation → add () → sort () → getResult ().
after :bulb: Facade
public class FooSorter {
private List<FooStudent> students = new List<>();
private FooSorter() {}
public static List<FooStudent> sort(FooStudent... students) {
for (FooStudent student : students)
add(student);
sort();
return getResult();
}
private void add(FooStudent student) {
students.add(student);
}
private void sort() {
students.sort(
Comparator.comparingInt(
student -> student.getJapaneseScore()
+ student.getMathScore()
+ student.getEnglishScore())
.reversed());
}
private List<FooStudent> getResult() {
return students;
}
}
FooSorter.sort(student1, student2, student3);
The user does not have to think about the order. At this scale, you can write in one line, so you don't have to separate the methods into Facade, but it's an example, so please take a good look. Facades are primarily useful for grouping complex processes that use multiple classes in sequence.
before
public class FooPosition {
private int x;
private int y;
// x,y accessor
public void moveAs(FooMove move) {
move.move(this);
}
}
public abstract class FooMove {
private final int addition;
public FooMove(int addition) {
this.addition = addition;
}
public abstract void move(FooPosition position);
}
public class FooMoveHorizontal extends FooMove {
public FooMoveHorizontal(int addition) {
super(addition);
}
@Override
public void move(FooPosition position) {
position.setX(position.getX() + addition);
}
}
public class FooMoveVertical extends FooMove {
public FooMoveVertical(int addition) {
super(addition);
}
@Override
public void move(FooPosition position) {
position.setY(position.getY() + addition);
}
}
FooMove moveHorizontal = new FooMoveHorizontal(x);
FooMove moveVertical = new FooMoveVertical(y);
FooPosition position = new FooPosition();
position.moveAs(moveHorizontal);
position.moveAs(moveVertical);
This is a simple and good design in itself, but suppose you want to create a class that handles movements in the x and y directions at the same time. (By the way, the design that divides the operation of the value of FooPosition into FooMove and accepts it with moveAs is the Visitor pattern)
after :bulb: Decorator
public class FooPosition {
private int x;
private int y;
// x,y accessor
public void moveAs(FooMove move) {
move.move(this);
}
}
public abstract class FooMove {
private final int addition;
private final FooMove move;
public FooMove(int addition) {
this.addition = addition;
}
public FooMove(int addition, FooMove move) {
this.addition = addition;
this.move = move;
}
public abstract void move(FooPosition position);
}
public class FooMoveHorizontal extends FooMove {
public FooMoveHorizontal(int addition) {
super(addition);
}
public FooMoveHorizontal(int addition, FooMove move) {
super(addition, move);
}
@Override
public void move(FooPosition position) {
if (move != null)
move.move(position);
position.setX(position.getX() + addition);
}
}
public class FooMoveVertical extends FooMove {
public FooMoveVertical(int addition) {
super(addition);
}
public FooMoveVertical(int addition, FooMove move) {
super(addition, move);
}
@Override
public void move(FooPosition position) {
if (move != null)
move.move(position);
position.setY(position.getY() + addition);
}
}
FooMove move = new FooMoveHorizontal(x, new FooMoveVertical(y));
FooPosition position = new FooPosition();
position.moveAs(move);
The movements of x and y have been summarized.
Also, with this design, you only need to use new FooMoveHorizontal (x, move)
to add more moves. (Call position.moveAs
only once)
At this scale, it is easier to understand if you create a class that handles x and y at the same time, but since it is an example (omitted)
before
public class FooTestUser extends FooUser {
@Override
public void foo1() {
//Normal User foo1
}
@Override
public void foo2() {
throw new RuntimeException("this operation is not permitted.");
}
@Override
public void foo3() {
throw new RuntimeException("this operation is not permitted.");
}
}
public class FooNormalUser extends FooUser {
@Override
public void foo1() {
//Normal User foo1
}
@Override
public void foo2() {
//Normal User foo2
}
@Override
public void foo3() {
throw new RuntimeException("this operation is not permitted.");
}
}
public class FooSuperUser extends FooUser {
@Override
public void foo1() {
//SuperUser foo1
}
@Override
public void foo2() {
//SuperUser foo2
}
@Override
public void foo3() {
//Processing of foo3
}
}
TestUser wants to limit its functionality so that only NormalUser foo1 can be executed. At this time, I want to manage that the implementation of foo1 of TestUser and NormalUser becomes a copy.
after :bulb: Proxy
public class FooTestUser extends FooUser {
private FooUser normalUser = new FooNormalUser();
@Override
public void foo1() {
normalUser.foo1();
}
@Override
public void foo2() {
throw new RuntimeException("this operation is not permitted.");
}
@Override
public void foo3() {
throw new RuntimeException("this operation is not permitted.");
}
}
//Normal User and Super User are the same as before
There is no longer a copy and paste of the implementation, and if the requirements of foo1 change, you only need to change the Normal User. However, since Java 8 has added a default implementation of interface, it may be better if you just want to share the implementation. (Same for Bridge pattern) If you don't have SuperUser, I think it's simpler to implement foo1 in the parent class (FooUser).
Recommended Posts