Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,22 @@
* When 생성한 지하철 노선을 삭제하면
* Then 해당 지하철 노선 정보는 삭제된다
* 인수 테스트의 결과가 다른 인수 테스트에 영향을 끼치지 않도록 인수 테스트를 서로 격리 시키세요.
* 인수 테스트의 재사용성과 가독성, 그리고 빠른 테스트 의도 파악을 위해 인수 테스트를 리팩터링 하세요.

## 🚀 3단계 - 구간 추가 기능
### 요구사항
#### 기능 요구사항
* 요구사항 설명에서 제공되는 요구사항을 기반으로 지하철 구간 추가 기능을 구현하세요.
* 요구사항을 정의한 인수 조건을 조출하세요.
* 인수 조건을 검증하는 인수 테스트를 작성하세요.
* 예외 케이스에 대한 검증도 포함하세요.

#### 프로그래밍 요구사항
* 인수 테스트 주도 개발 프로세스에 맞춰서 기능을 구현하세요.
* 요구사항 설명을 참고하여 인수 조건을 정의
* 인수 조건을 검증하는 인수 테스트 작성
* 인수 테스트를 충족하는 기능 구현
* 인수 조건은 인수 테스트 메서드 상단에 주석으로 작성하세요.
* 뼈대 코드의 인수 테스트를 참고
* 인수 테스트의 결과가 다른 인수 테스트에 영향을 끼치지 않도록 인수 테스트를 서로 격리 시키세요.
* 인수 테스트의 재사용성과 가독성, 그리고 빠른 테스트 의도 파악을 위해 인수 테스트를 리팩터링 하세요.
15 changes: 13 additions & 2 deletions src/main/java/nextstep/subway/application/LineService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

import nextstep.subway.domain.Line;
import nextstep.subway.domain.LineRepository;
import nextstep.subway.domain.Section;
import nextstep.subway.domain.Station;
import nextstep.subway.dto.LineRequest;
import nextstep.subway.dto.LineResponse;
import nextstep.subway.dto.LineUpdateRequest;
import nextstep.subway.dto.SectionRequest;
import nextstep.subway.error.exception.LineNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -29,9 +31,9 @@ public LineService(LineRepository lineRepository, StationService stationService)
public LineResponse saveLine(LineRequest lineRequest) {
Station upStation = stationService.findStationEntityById(lineRequest.getUpStationId());
Station downStation = stationService.findStationEntityById(lineRequest.getDownStationId());
Line line = lineRepository.save(new Line(lineRequest.getName(), lineRequest.getColor()));
line.initSections(new Section(upStation, downStation, lineRequest.getDistance(), line));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line의 생성과 initSections를 분리해주신 이유가 있을까요?


Line line = lineRepository.save(new Line(lineRequest.getName(), lineRequest.getColor(),
upStation, downStation, lineRequest.getDistance()));
return new LineResponse(line);
}

Expand Down Expand Up @@ -62,4 +64,13 @@ public LineResponse updateLineById(Long lineId, LineUpdateRequest lineUpdateRequ
line.update(lineUpdateRequest);
return new LineResponse(line);
}

@Transactional
public void addSection(Long lineId, SectionRequest sectionRequest) {
Line line = getLineEntity(lineId);
Station upStation = stationService.findStationEntityById(sectionRequest.getUpStationId());
Station downStation = stationService.findStationEntityById(sectionRequest.getDownStationId());
Section section = new Section(upStation, downStation, sectionRequest.getDistance(), line);
line.addSection(section);
}
}
41 changes: 15 additions & 26 deletions src/main/java/nextstep/subway/domain/Line.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,50 +16,39 @@ public class Line extends BaseEntity {
@Column(unique = true)
private String color;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "up_station_id", nullable = false)
private Station upStation;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "down_station_id", nullable = false)
private Station downStation;

@Column(name = "distance", nullable = false)
private int distance;
@Embedded
private Sections sections;

public Line() {
}

public Line(String name, String color, Station upStation, Station downStation, int distance) {
public Line(String name, String color) {
this.name = name;
this.color = color;
this.upStation = upStation;
this.downStation = downStation;
this.distance = distance;
}

public Long getId() {
return id;
public void initSections(Section section) {
sections = new Sections(this, section);
}

public String getName() {
return name;
public void addSection(Section section) {
sections.add(section);
}

public String getColor() {
return color;
public Sections getSections() {
return sections;
}

public Station getUpStation() {
return upStation;
public Long getId() {
return id;
}

public Station getDownStation() {
return downStation;
public String getName() {
return name;
}

public int getDistance() {
return distance;
public String getColor() {
return color;
}

public void update(LineUpdateRequest lineUpdateRequest) {
Expand Down
116 changes: 116 additions & 0 deletions src/main/java/nextstep/subway/domain/Section.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package nextstep.subway.domain;

import javax.persistence.*;

@Entity
public class Section extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "up_station_id", foreignKey = @ForeignKey(name = "fk_section_to_up_station"))
private Station upStation;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "down_station_id", foreignKey = @ForeignKey(name = "fk_section_to_down_station"))
private Station downStation;

@Column(name = "distance")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@Column(name = "distance")
@Column

변수명과 동일하다면 name을 따로 지정하지 않아도 됩니다 ㅎㅎ

private int distance;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "line_id", nullable = false)
private Line line;

public Section() {
}

public Section(Station upStation, Station downStation, int distance, Line line) {
this.upStation = upStation;
this.downStation = downStation;
this.distance = distance;
this.line = line;
}

public Station getUpStation() {
return upStation;
}

public Station getDownStation() {
return downStation;
}

public void arrangeFirstSection(Section newSection) {
this.downStation = newSection.upStation;
}

public void arrangeEndSection(Section newSection) {
this.upStation = newSection.getDownStation();
}

public void arrangeInterSection(Section newSection) {
this.upStation = newSection.downStation;
this.distance -= newSection.distance;
}

public boolean canAddFirstSection(Section newSection) {
if (isNotValidNewSection(newSection)) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (isNotValidNewSection(newSection)) {
if (newSection.isNotValidNewSection()) {

두 방식은 어떠한 차이가 있을까요?

return false;
}

if (!isFirstSection()) {
return false;
}

return newSection.downStation.getId().equals(this.downStation.getId());
}

public boolean canAddEndSection(Section newSection) {
if (isNotValidNewSection(newSection)) {
return false;
}

if (!isEndSection()) {
return false;
}

return newSection.upStation.getId().equals(this.upStation.getId());
}

public boolean canAddInterSection(Section section) {
if (!isInterSection()) {
return false;
}

// upStation이 같아야 inter section의 후보가 될 수 있다.
return this.upStation.getId().equals(section.upStation.getId());
}

private boolean isFirstSection() {
return this.upStation == null && this.downStation != null;
}

private boolean isEndSection() {
return this.upStation != null && this.downStation == null;
}

private boolean isInterSection() {
return !isFirstSection() && !isEndSection();
}

public boolean isSameSection(Section newSection) {
return isInterSection()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isSameSection이라는 메서드명으로 봤을 때 isInterSection을 검증하는 것은 오해의 소지가 될 것 같은데 어떻게 생각하시나요?

&& this.upStation.getId().equals(newSection.getUpStation().getId())
&& this.downStation.getId().equals(newSection.getDownStation().getId());
}

private boolean isNotValidNewSection(Section newSection) {
// newSection의 up, down station은 모두 null이 아니어야 한다.
return newSection.getUpStation() == null || newSection.getDownStation() == null;
}

public boolean isShorterThen(Section newSection) {
return this.distance < newSection.distance;
}
}
128 changes: 128 additions & 0 deletions src/main/java/nextstep/subway/domain/Sections.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package nextstep.subway.domain;

import nextstep.subway.error.exception.SectionInvalidException;

import javax.persistence.CascadeType;
import javax.persistence.Embeddable;
import javax.persistence.OneToMany;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

@Embeddable
public class Sections {
@OneToMany(mappedBy = "line", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Section> sections;

protected Sections() {
}

public Sections(Line line, Section section) {
this.sections = new ArrayList<>();
sections.add(new Section(null, section.getUpStation(), 0, line));
sections.add(new Section(section.getDownStation(), null, 0, line));
sections.add(section);
}

public void add(Section newSection) {
checkAlreadyExist(newSection);
if (addIfFirstSection(newSection)) {
return ;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return ;
return;

공백을 추가해주신 이유가 있을까요?

}
if (addIfEndSection(newSection)) {
return ;
}
addIfIntersection(newSection);
}

private boolean addIfFirstSection(Section newSection) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 메서드는 변경과 조회의 역할 두 가지 모두 하고 있는데요.
변경의 메서드와 조회의 메서드를 분리하여 유지보수성을 높여보면 어떨까요?
관련 키워드로는 CQS(Command Query Separation) 입니다 :)

Section firstSection = getFirstSection()
.orElseThrow(() -> new SectionInvalidException("first section이 없는 라인입니다."));
if (firstSection.canAddFirstSection(newSection)) {
firstSection.arrangeFirstSection(newSection);
sections.add(newSection);
return true;
}
return false;
}

private boolean addIfEndSection(Section newSection) {
List<Section> sectionsInOrder = getSectionsInOrder();
Section endSection = sectionsInOrder.get(sectionsInOrder.size() - 1);
if (endSection.canAddEndSection(newSection)) {
endSection.arrangeEndSection(newSection);
sections.add(newSection);
return true;
}
return false;
}

private void addIfIntersection(Section newSection) {
Section upperSection = getTargetUpperSection(newSection).orElseThrow(()
-> new SectionInvalidException("상행역과 하행역 둘 다 하나도 포함되어 있지 않습니다."));
if (upperSection.isShorterThen(newSection)) {
throw new SectionInvalidException("distance가 기존 구간의 보다 큰 길이 입니다.");
}
sections.add(newSection);
upperSection.arrangeInterSection(newSection);
}

private Optional<Section> getTargetUpperSection(Section newSection) {
return sections.stream()
.filter(s -> s.canAddInterSection(newSection))
.findFirst();
}

public List<Station> getStationsInOrder() {
// section을 downStation을 기준으로 변경
return getSectionsInOrder().stream()
.filter(s -> s.getDownStation() != null)
.map(Section::getDownStation)
.collect(Collectors.toList());
}

public List<Section> getSectionsInOrder() {
// 출발지점 찾기
Optional<Section> preSection = getFirstSection();

// section 에 있는 station 순서대로 찾기
List<Section> result = new ArrayList<>();
while (preSection.isPresent()) {
Section section = preSection.get();
result.add(section);
preSection = sections.stream()
.filter(candidate -> isNextSection(section, candidate))
.findFirst();
}
return result;
}

private Optional<Section> getFirstSection() {
return sections.stream()
.filter(s -> s.getUpStation() == null)
.findFirst();
}

private boolean isNextSection(Section section, Section candidateSection) {
if (section.getDownStation() == null) {
return false;
}

if (candidateSection.getUpStation() == null) {
return false;
}

return candidateSection.getUpStation().getId().equals(section.getDownStation().getId());
}

private void checkAlreadyExist(Section section) {
if (hasSection(section)) {
throw new SectionInvalidException("이미 존재하는 section 입니다.");
}
}

private boolean hasSection(Section newSection) {
return sections.stream().anyMatch(s -> s.isSameSection(newSection));
}
}
8 changes: 4 additions & 4 deletions src/main/java/nextstep/subway/dto/LineResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class LineResponse {
private Long id;
Expand All @@ -18,10 +19,9 @@ public LineResponse(Line saved) {
this.id = saved.getId();
this.name = saved.getName();
this.color = saved.getColor();
this.stations = Arrays.asList(
new StationResponse(saved.getUpStation()),
new StationResponse(saved.getDownStation())
);
this.stations = saved.getSections().getStationsInOrder().stream()
.map(StationResponse::new)
.collect(Collectors.toList());
}

public Long getId() {
Expand Down
Loading