public abstract class MovieFinder{
private final MovieReader movieReader;
public MovieFinder(MovieReader movieReader) {
this.movieReader = Objects.requireNonNull(movieReader);
}
/**
* 저장된 영화 목록에서 감독으로 영화를 검색한다.
*
* @param directedBy 감독
* @return 검색된 영화 목록
*/
public List<Movie> directedBy(String directedBy) {
return movieReader.loadMovies()
.stream()
.filter(it -> it.getDirector().toLowerCase().contains(directedBy.toLowerCase()))
.collect(Collectors.toList());
}
/**
* 저장된 영화 목록에서 개봉년도로 영화를 검색한다.
*
* @param releasedYearBy
* @return 검색된 영화 목록
*/
public List<Movie> releasedYearBy(int releasedYearBy) {
return movieReader.loadMovies()
.stream()
.filter(it -> Objects.equals(it.getReleaseYear(), releasedYearBy))
.collect(Collectors.toList());
}
public abstract List<Movie> loadMovies();
}
MovieFinder 를 추상 클래스로 설정하고, loadMovies 메소드를 추상메소드로 등록한다.
public class CsvMoviesFinder extends MovieFinder {
/**
* 영화 메타데이터를 읽어 저장된 영화 목록을 불러온다.
*
* @return 불러온 영화 목록
*/
@Override
public List<Movie> loadMovies() {
try {
final InputStream content = getMetadataResource().getInputStream();
final Function<String, Movie> mapCsv = csv -> {
try {
// split with comma
String[] values = csv.split(",");
String title = values[0];
List<String> genres = Arrays.asList(values[1].split("\\|"));
String language = values[2].trim();
String country = values[3].trim();
int releaseYear = Integer.valueOf(values[4].trim());
String director = values[5].trim();
List<String> actors = Arrays.asList(values[6].split("\\|"));
URL imdbLink = new URL(values[7].trim());
String watchedDate = values[8];
return Movie.of(title, genres, language, country, releaseYear, director, actors, imdbLink, watchedDate);
} catch(IOException error) {
throw new ApplicationException("mapping csv to object failed.", error);
}
};
return new BufferedReader(new InputStreamReader(content, StandardCharsets.UTF_8))
.lines()
.skip(1)
.map(mapCsv)
.collect(Collectors.toList());
} catch(IOException error) {
throw new ApplicationException("failed to load movies data.", error);
}
}
}
CsvMoviesFinder 는 MovieFinder 를 상속받고 loadMovies 를 구현한다.
public class XmlMoviesReader extends MovieFinder {
@Override
public List<Movie> loadMovies() {
try {
final InputStream content = getMetadataResource().getInputStream();
final Source source = new StreamSource(content);
final MovieMetadata metadata = (MovieMetadata) unmarshaller.unmarshal(source);
return metadata.toMovies();
} catch (IOException error) {
throw new ApplicationException("failed to load movies data.", error);
}
}
}
XmlMoviesFinder 또한 MovieFinder 를 상속받고 loadMovies 를 구현한다.
MovieFinder movieFinder = new CsvMovieFinder();
List<Movie> result = movieFinder.directedBy("Michael");
assertEquals(3, result.size());
result = movieFinder.releasedYearBy(2015);
assertEquals(225, result.size());
위와 같이 상속을 적용할 수 있다.
이렇게 되면 부모 클래스에 기본적인 알고리즘의 흐름을 구현하고 중간에 필요한 처리를 자식 클래스에서 위임할 수 있다.
이러한 구조를 디자인 패턴에서는 템플릿 메소드 패턴이라고 한다.
하지만 상속보단 합성을 적용하는 것이 바람직하다. 위의 코드를 합성으로 바꿔보자.
합성 예시
public interface MovieReader {
List<Movie> loadMovies();
}
public class CsvMovieReader implements MovieReader {
/**
* 영화 메타데이터를 읽어 저장된 영화 목록을 불러온다.
*
* @return 불러온 영화 목록
*/
@Override
public List<Movie> loadMovies() {
try {
final InputStream content = getMetadataResource().getInputStream();
final Function<String, Movie> mapCsv = csv -> {
try {
// split with comma
String[] values = csv.split(",");
String title = values[0];
List<String> genres = Arrays.asList(values[1].split("\\|"));
String language = values[2].trim();
String country = values[3].trim();
int releaseYear = Integer.valueOf(values[4].trim());
String director = values[5].trim();
List<String> actors = Arrays.asList(values[6].split("\\|"));
URL imdbLink = new URL(values[7].trim());
String watchedDate = values[8];
return Movie.of(title, genres, language, country, releaseYear, director, actors, imdbLink, watchedDate);
} catch(IOException error) {
throw new ApplicationException("mapping csv to object failed.", error);
}
};
return new BufferedReader(new InputStreamReader(content, StandardCharsets.UTF_8))
.lines()
.skip(1)
.map(mapCsv)
.collect(Collectors.toList());
} catch(IOException error) {
throw new ApplicationException("failed to load movies data.", error);
}
}
}
public class XmlMovieReader implements MovieReader {
@Override
public List<Movie> loadMovies() {
try {
final InputStream content = getMetadataResource().getInputStream();
final Source source = new StreamSource(content);
final MovieMetadata metadata = (MovieMetadata) unmarshaller.unmarshal(source);
return metadata.toMovies();
} catch (IOException error) {
throw new ApplicationException("failed to load movies data.", error);
}
}
}
public class MovieFinder {
private MovieReader movieReader = new CsvMovieReader();
/**
* 저장된 영화 목록에서 감독으로 영화를 검색한다.
*
* @param directedBy 감독
* @return 검색된 영화 목록
*/
public List<Movie> directedBy(String directedBy) {
return movieReader.loadMovies()
.stream()
.filter(it -> it.getDirector().toLowerCase().contains(directedBy.toLowerCase()))
.collect(Collectors.toList());
}
/**
* 저장된 영화 목록에서 개봉년도로 영화를 검색한다.
*
* @param releasedYearBy
* @return 검색된 영화 목록
*/
public List<Movie> releasedYearBy(int releasedYearBy) {
return movieReader.loadMovies()
.stream()
.filter(it -> Objects.equals(it.getReleaseYear(), releasedYearBy))
.collect(Collectors.toList());
}
}