Repository navigation
Design Document
- LCS Algorithm
- MVC
- SOLID
LCS 알고리즘은 아래와 같은 클래스 인터페이스 하에 호출하도록 설계하였다.
public class LCS<T extends Comparable<T>> implements Diff.Algorithm<T> {
@Override
public List<Pair<Integer>> diff(List<T> l, List<T> r) { ... }
...
}LCS 클래스의 diff 메소드는 List 2개를 받아 비교하여 공통되는 순열의 리스트 인덱스 쌍을 반환하는 메소드이다. diff 메소드의 구현은 리스트의 요소가 공통되는 요소인지 확인할 수 있는 비교 메커니즘이 필요하다. 이를 위해 Comparable의 서브타입만 LCS 클래스를 생성할 수 있도록 제약을 걸어두었다.
따라서 이후에 텍스트를 String보다 복잡한 클래스를 사용하더라도 compareTo 메소드만 구현하면 LCS 클래스를 사용할 수 있다.
아래는 diff 메소드의 반환값에 대한 설명이다.
각 Pair<Integer>는 LCS 알고리즘 하에서 매칭되는 인덱스 쌍이다.
예를 들어 List<T> L, R이 T=Character 일 때 아래 처럼 들어왔다고 가정하면
L: ['a', 'b', 'c']
R: ['a', 'c', 'd']
이면 LCS는 ['a', 'c'] 이다.
LCS의 첫 요소인 'a'에 대해서는 Pair<Integer>는 (0, 0) 이다.
> L에서는 index 0 위치에 'a'가 있고, R에서도 index 0 위치에 'a'가 있기 때문이다.
LCS의 두번째 요소인 'c'에 대해서는 Pair<Integer>는 (2, 1) 이다.
> L에서는 index 2 위치에 'c'가 있고, R에서는 index 1 위치에 'c'가 있기 때문이다.
- 보이는 바와 같이 Controller과 EditPanel은 각각 Merge와 JavaFX에 종속적이지만 각각의 Model. View 부분이 Controller 부분에 종속적이지는 않음.
- 한 클래스는 하나의 책임만 가져야 한다.
- 이에 대한 설명은 아래 Designed to be testable by unit-test tools 부분의 설명과 일맥상통하므로 생략하겠다.
- 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀있어야 한다.
package SimpleMerge.util;
public class Pair<T> {
public final T first, second;
public Pair(T first, T second) {
this.first = first;
this.second = second;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Pair<?> pair = (Pair<?>) o;
if (first != null ? !first.equals(pair.first) : pair.first != null) return false;
return second != null ? second.equals(pair.second) : pair.second == null;
}
@Override
public int hashCode() {
int result = first != null ? first.hashCode() : 0;
result = 31 * result + (second != null ? second.hashCode() : 0);
return result;
}
}
- Generic으로 구현된 Pair는 모듈의 기능 확장이나 변경을 다른 클래스와 무관하게 진행 할 수 있다.
EditPanel class는 VBox를 상속하여 JavaFX의 GUI Component처럼 쓸 수 있다.
EditPanel.java
public class EditPanel extends VBox implements Initializable {
...
}애플리케이션의 메인 뷰를 정의하는 fxml 파일에 EditPanel을 GUI Component로 쓰고 있는 것을 확인할 수 있다.
main.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import SimpleMerge.control.EditPanel?>
<AnchorPane fx:id="root" prefHeight="400" prefWidth="800" xmlns="http://javafx.com/javafx/null" xmlns:fx="http://javafx.com/fxml/1" fx:controller="SimpleMerge.Controller">
...
<items>
<EditPanel fx:id="leftEditPanel"></EditPanel>
<EditPanel fx:id="rightEditPanel"></EditPanel>
</items>
...
</AnchorPane>- 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.
- 저장 및 불러오기 부분의 함수를 맡는 EditPanelEventListener와 FileChooser을 이용하는 함수를 맡는 FileSelector를 분리.
MVC 모델에서 Model-Controller 관계와, GUI Component 계층에서 상위 컴포넌트와 하위 컴포넌트 관계에서 DIP와 ISP 원칙이 적용되었다.
Text 비교 및 하이라이팅/병합 작업을 하는 Model인 Merger는 Controller에 이벤트를 직접 전달하지 않고 내부 인터페이스를 통해 전달한다. Merger는 3개 인터페이스를 받는다.
Merger.java
public class Merger<T extends Comparable<T>> {
...
public interface MergeEventListener {
void onStart();
void onEnd();
}
public interface UpdateBlockStyleEventListener {
void onUpdateBlockStyle(Block block, BlockState style);
}
public interface UpdateItemsEventListener<T> {
void onUpdateItems(Block block, List<T> items, boolean includeLastItem);
}
}main 컴포넌트가 EditPanel 컴포넌트를 하위 컴포넌트로 가지고 있다. 따라서 EditPanel은 main 컴포넌트에 의존하지 않고 인터페이스에 의존하여 main 컴포넌트에 이벤트를 전달해야 한다. 추가적으로 EditPanel은 파일을 Load 하기 위해 다른 컴포넌트에 의존하는데, 이 또한 인터페이스에 의존하도록 하였다.
EditPanel.java
public class EditPanel extends VBox implements Initializable {
...
private void emitLoad() {
if (eventListener == null)
return;
eventListener.onLoad();
}
private void emitEdit() {
if (eventListener == null)
return;
eventListener.onEdit();
}
private void emitSave() {
if (eventListener == null)
return;
eventListener.onSave();
}
private void emitTextChanged() {
if (eventListener == null)
return;
eventListener.onTextChanged();
}
...
public void setEventListener(EditPanelEventListener eventListener) {
this.eventListener = eventListener;
}
public void setFileSelector(FileSelector selector) {
this.selector = selector;
}
...
}EditPanelEventListener.java
public interface EditPanelEventListener {
void onLoad();
void onSave();
void onEdit();
void onTextChanged();
}FileSelector.java
public interface FileSelector {
File getFile();
File saveFile();
}- 각각의 컴포넌트나 기능들이 서로 의존적이지 않아 Mock이 테스트를 할 수 있도록 Interface로 분리.
- Merger, LCS 등 MVC에서 모델은 의존하는 구체 클래스가 없어 별도의 Mocking 없이도 유닛 테스트가 가능하다.
- MVC에서 Controller에 해당하는 EditPanel은 의존하는 컴포넌트를 인터페이스(FileSelector)에 의존하도록 만들어 Mocking하여 테스트되도록 하였다.