계단식 리팩토링을 피하려면 어떻게해야합니까?


52

프로젝트가 있습니다. 이 프로젝트에서는 기능을 추가하기 위해 기능을 리팩터링하고 기능을 추가하기 위해 프로젝트를 리팩터링했습니다.

문제는 내가 끝났을 때 그것을 수용하기 위해 약간의 인터페이스 변경이 필요하다는 것이 밝혀졌습니다. 그래서 변경했습니다. 그런 다음 소비하는 클래스는 새로운 인터페이스 측면에서 현재 인터페이스로 구현할 수 없으므로 새로운 인터페이스도 필요합니다. 이제 3 개월 후 거의 무관 한 수많은 문제를 해결해야했으며 1 년 동안 로드맵 된 문제를 해결하려고하거나 문제가 컴파일되기 전에 어려움으로 해결되지 않는 것으로 보이는 문제를 찾고 있습니다. 다시.

앞으로 이런 종류의 계단식 리팩토링을 어떻게 피할 수 있습니까? 서로 너무 밀접하게 의존하는 이전 수업의 증상일까요?

간단한 편집 :이 경우 리 팩터 특징 이었습니다 . 리 팩터 특정 코드 조각의 확장 성을 높이고 일부 커플 링을 줄 였기 때문입니다. 이것은 외부 개발자가 더 많은 것을 할 수 있다는 것을 의미했습니다. 이것이 제가 원했던 기능이었습니다. 따라서 원래 리 팩터 자체가 기능적으로 변경되어서는 안됩니다.

5 일 전에 약속 한 더 큰 편집 :

이 리팩터링을 시작하기 전에 인터페이스가있는 시스템이 있었지만 구현시 dynamic_cast에는 가능한 모든 구현을 통해 간단하게 전달했습니다. 이것은 분명히 인터페이스에서 상속받을 수 없었으며 두 번째로 구현 액세스 권한이없는 사람은이 인터페이스를 구현하는 것이 불가능하다는 것을 의미했습니다. 그래서 나는이 문제를 해결하고 공공 소비를위한 인터페이스를 열어서 누구나 그것을 구현할 수 있고 인터페이스를 구현하는 것이 계약 전체를 개선해야한다고 결정했습니다.

내가 한 모든 장소를 찾아서 불 사고로 죽였을 때, 나는 특정한 문제로 판명 된 곳을 찾았습니다. 그것은 이미 구현되었지만 다른 곳에서 더 나은 모든 파생 클래스 및 복제 기능의 구현 세부 사항에 달려 있습니다. 대신 공용 인터페이스 측면에서 구현되어 해당 기능의 기존 구현을 재사용 할 수 있습니다. 제대로 작동하려면 특정 컨텍스트가 필요하다는 것을 알았습니다. 대략적으로 말하면, 이전 구현 호출은 다소 비슷했습니다.

for(auto&& a : as) {
     f(a);
}

그러나이 컨텍스트를 얻으려면 컨텍스트를 더 비슷한 것으로 변경해야했습니다.

std::vector<Context> contexts;
for(auto&& a : as)
    contexts.push_back(g(a));
do_thing_now_we_have_contexts();
for(auto&& con : contexts)
    f(con);

이는 이전에 일부로 사용되었던 모든 작업에 대해 f일부 g는 컨텍스트없이 작동 하는 새로운 기능 의 일부로 작성되어야하며 일부는 현재 지연된 일부로 작성되어야 함을 의미합니다 f. 그러나 모든 방법 f이 이러한 맥락을 필요로하거나 요구하는 것은 아닙니다. 그들 중 일부는 별도의 수단을 통해 얻은 뚜렷한 맥락을 필요로합니다. 따라서 f전화를 거는 모든 것 (대략 말하면 거의 모든 것 )에 대해, 필요한 경우, 필요한 상황, 어디에서 가져와야하는지, 그리고 오래된 것에서 f새로운 f것과 새로운 것으로 나누는 방법을 결정해야했습니다. g.

그리고 그것이 내가 지금있는 곳에서 끝나는 방식입니다. 내가 계속 한 유일한 이유는 어쨌든 다른 이유로이 리팩토링이 필요했기 때문입니다.


67
"기능을 추가하기 위해 프로젝트를 리팩토링했다"고 말할 때 정확히 무엇을 의미합니까? 리팩토링은 정의에 따라 프로그램의 동작을 변경하지 않으므로이 설명이 혼동됩니다.
Jules

5
@Jules : 엄밀히 말하면,이 기능은 다른 개발자가 특정 유형의 확장을 추가 할 수 있도록하는 것이 었습니다. 따라서이 기능은 리 팩터가되어 클래스 구조를 더욱 개방적으로 만들었습니다.
DeadMG

5
리팩토링에 관한 모든 책과 기사에서 이것이 논의되었다고 생각 했습니까? 소스 제어가 구출됩니다. A 단계를 수행하려면 B 단계를 먼저 수행 한 다음 A를 스크랩하고 B를 먼저 수행해야합니다.
rwong

4
@DeadMG :이 은 내가 처음으로 언급 한 책입니다 : "게임"픽업 스틱 "은 Mikado 방법에 대한 좋은 은유입니다."기술 부채 "(거의 모든 소프트웨어에 포함 된 레거시 문제)를 제거합니다. 시스템 — 구현하기 쉬운 규칙을 따름. 프로젝트를 무너 뜨리지 않고 중심 문제가 드러날 때까지 서로 얽힌 각 의존성을 신중하게 추출합니다. "
rwong

2
우리가 말하는 프로그래밍 언어를 명확히 할 수 있습니까? 모든 의견을 읽은 후에 IDE를 사용하는 대신 직접 도움을 요청한다는 결론에 도달했습니다. 따라서 실용적인 조언을 해줄 수 있는지 알고 싶습니다.
thepacker

답변:


69

마지막으로 예기치 않은 결과로 리팩토링을 시작하려고했는데 하루가 지나고 빌드 및 / 또는 테스트를 안정화 할 수 없었으므로 리팩토링 이전의 시점으로 코드베이스를 포기하고 되돌 렸습니다.

그런 다음 무엇이 잘못되었는지 분석하고 더 작은 단계로 리팩토링을 수행하는 더 나은 계획을 개발했습니다. 따라서 계단식 리팩토링을 피하기위한 조언은 다음과 같습니다. 중지 할시기를 알고 제어 할 수없는 상태로 두지 마십시오!

때로는 총알을 물고 하루 종일 일을 버려야합니다. 3 개월의 일을 버리는 것보다 훨씬 쉽습니다. 당신이 풀었던 날은 완전히 헛된 것이 아니며, 적어도 당신은 문제에 접근 하지 않는 방법을 배웠습니다 . 그리고 내 경험상 리팩토링에서 더 작은 단계를 만들 수있는 가능성 이 항상 있습니다.

참고 사항 : 당신은 당신이 당신이 3 개월의 전체 작업을 기꺼이 희생하고 새로운 (그리고 희망적으로 더 성공적인) 리팩토링 계획으로 다시 시작할 것인지 결정 해야하는 상황에있는 것 같습니다. 쉬운 결정이 아니라고 생각할 수 있지만, 빌드를 안정화하기 위해 3 개월이 더 걸릴 위험이 얼마나 높을 지, 또한 재 작성 중에 예상했던 모든 버그를 수정 하여 지난 3 개월 동안 수행 한 위험을 어느 정도 해결해야하는지 스스로에게 묻습니다. ? 나는 그것이 "리팩토링"이 아니라 당신이 실제로 한 일이라고 생각하기 때문에 "다시 쓰기"를 썼습니다. 프로젝트가 컴파일 된 마지막 개정판으로 돌아가서 실제 리팩토링 ( "재 작성"이 아닌)으로 다시 시작하여 현재 문제를 더 빨리 해결할 수있는 것은 아닙니다 .


53

서로 너무 밀접하게 의존하는 이전 수업의 증상일까요?

확실한. 무수히 많은 다른 변화를 일으키는 하나의 변화는 거의 커플 링의 정의입니다.

계단식 리 팩터를 피하려면 어떻게해야합니까?

최악의 코드베이스에서는 단일 변경 사항이 계속 캐스케이드되어 결국 거의 모든 것을 변경하게됩니다. 광범위한 커플 링이있는 리 팩터의 일부는 작업중인 부품을 분리하는 것입니다. 새 기능이이 코드에 영향을 미치는 곳뿐만 아니라 다른 모든 것이 해당 코드에 닿는 부분을 리팩터링해야합니다.

일반적으로 이는 이전 코드와 유사하게 작동하지만 새로운 구현 / 인터페이스를 사용하는 이전 코드를 작동하도록 일부 어댑터를 만드는 것을 의미합니다. 결국, 인터페이스 / 구현을 변경하고 커플 링을 남겨두면 아무것도 얻지 못하는 것입니다. 돼지의 립스틱입니다.


33
+1 리팩토링이 더 심할수록 리팩토링이 더 광범위하게 도달합니다. 사물의 본질입니다.
Paul Draper

4
그러나 실제로 리팩토링하는 경우 다른 코드는 변경 사항에 즉시 신경 쓸 필요가 없습니다. (물론 결국 다른 부분을 정리하고 싶을 것입니다. 그러나 즉시 필요한 것은 아닙니다.) 나머지 앱을 통해 "캐스케이드"되는 변경은 리팩토링보다 큽니다. 기본적으로 재 설계 또는 재 작성.
cHao

+1 어댑터는 먼저 변경하려는 코드를 분리하는 방법입니다.
winkbrace

17

리팩토링이 너무 야심 찬 것처럼 들립니다. 리팩토링은 작은 단계로 적용되어야하며, 각 단계는 30 분 (또는 최악의 경우 최대 하루)에 완료 될 수 있으며 프로젝트를 빌드 가능하게하고 모든 테스트는 여전히 통과합니다.

각 개별 변경 사항을 최소로 유지하면 리팩토링으로 인해 오랫동안 빌드가 중단 될 수 없습니다. 최악의 경우는 매개 변수를 널리 사용되는 인터페이스 (예 : 새 매개 변수 추가)의 메소드로 변경하는 것입니다. 그러나 그에 따른 결과적인 변화는 기계적입니다. 각 구현에서 매개 변수를 추가 (및 무시)하고 각 호출에서 기본값을 추가합니다. 수백 개의 참조가 있더라도 이러한 리팩토링을 수행하는 데 하루가 걸리지 않아야합니다.


4
그런 상황이 어떻게 생길지 모르겠습니다. 메소드 인터페이스의 합리적인 리팩토링을 위해서는 변경하기 전과 동일하게 호출 동작이 발생하도록 전달할 수있는 쉽게 결정되는 새로운 매개 변수 세트가 있어야합니다.
Jules

3
나는 그런 리팩토링을 수행하고 싶었던 상황에 가본 적이 없지만 그것이 매우 이상하게 들린다 고 말해야합니다. 인터페이스에서 기능을 제거했다는 말입니까? 그렇다면 어디로 갔습니까? 다른 인터페이스로? 아니면 다른 곳?
Jules

5
그런 다음 리팩토링하기 전에 제거 할 기능의 모든 사용법을 나중에 제거하는 대신 제거하는 것이 좋습니다. 이를 통해 작업하는 동안 코드 작성을 유지할 수 있습니다.
Jules

11
@DeadMG : 이상하게 들립니다. 더 이상 필요하지 않은 기능 하나를 제거하고 있습니다. 그러나 다른 한편으로, 당신은 "프로젝트가 완전히 기능하지 않게됩니다"라고 씁니다. 실제로 그 기능은 꼭 필요한 것 같습니다. 명확히하십시오.
Doc Brown

26
@DeadMG 이러한 경우 일반적으로 새 기능을 개발하고, 기능이 작동하는지 테스트하고, 새 인터페이스를 사용하기 위해 기존 코드를 전환 한 다음 , 기존의 불필요한 기능 제거합니다. 그렇게하면 일이 망가질 지점이 없어야합니다.
sapi

12

앞으로 이런 종류의 계단식 리 팩터를 어떻게 피할 수 있습니까?

희망적인 사고 디자인

목표는 새로운 기능에 대한 우수한 OO 설계 및 구현입니다. 리팩토링을 피하는 것도 목표입니다.

처음부터 시작하여 원하는 새로운 기능위한 디자인 만드십시오 . 잘하기 위해 시간을 내십시오.

그러나 여기서 핵심은 "기능 추가"입니다. 새로운 것들이 코드베이스의 현재 구조를 크게 무시하게하는 경향이 있습니다. 우리의 희망적인 사고 디자인은 독립적입니다. 그러나 다음 두 가지가 더 필요합니다.

  • 새로운 기능의 코드를 주입 ​​/ 구현하기 위해 필요한 심을 만들기에 충분한 리팩터링
    • 리팩토링에 대한 저항이 새로운 디자인을 주도해서는 안됩니다.
  • 새로운 기능과 기존 코덱을 행복하게 무시하는 API를 사용하여 클라이언트 용 클래스를 작성하십시오.
    • 개체, 데이터 및 결과를 앞뒤로 가져 오도록 음역합니다. 최소한의 지식 원칙은 저주받습니다. 우리는 기존 코드가하는 것보다 더 나쁜 일은하지 않을 것입니다.

휴리스틱, 학습 한 내용 등

리팩토링은 기존 메소드 호출에 기본 매개 변수를 추가하는 것만 큼 간단합니다. 또는 정적 클래스 메소드에 대한 단일 호출.

기존 클래스의 확장 방법을 사용하면 최소한의 위험으로 새로운 디자인의 품질을 유지할 수 있습니다.

"구조"가 전부입니다. 구조는 단일 책임 원칙의 실현입니다. 기능을 용이하게하는 디자인. 코드는 클래스 계층 구조에서 짧고 단순하게 유지됩니다. 테스트, 재 작업 및 레거시 코드 정글을 통한 해킹을 피하는 동안 새로운 디자인을위한 시간이 완성됩니다.

희망적인 사고 수업은 당면한 과제에 중점을 둡니다. 일반적으로 기존 클래스를 확장하는 것을 잊지 마십시오. 리 팩터 캐스케이드를 다시 유도하고 "더 무거운"클래스의 오버 헤드를 처리해야합니다.

기존 코드에서이 새로운 기능의 나머지를 제거하십시오. 여기서는 완벽하고 캡슐화 된 새로운 기능이 리팩토링을 피하는 것보다 중요합니다.


9

(멋진) 책에서 마이클 깃털에 의해 레거시 코드와 함께 효과적으로 작동 :

레거시 코드에서 종속성을 깨뜨릴 때 종종 미적 감각을 약간 중단해야합니다. 일부 의존성은 깨끗하게 깨집니다. 다른 것들은 디자인 관점에서 이상적이지 않은 것으로 보입니다. 수술의 절개 점과 같습니다. 작업 후에 코드에 흉터가 남을 수 있지만 그 아래의 모든 것이 더 좋아질 수 있습니다.

나중에 의존성을 깨뜨린 지점 주변의 코드를 덮을 수 있다면 그 흉터도 치료할 수 있습니다.


6

이 "사소한"변경은 소프트웨어를 완전히 다시 작성하는 것과 같은 양의 작업을 의미하는 자체 부과 규칙으로 상자에 넣은 것처럼 들립니다 (특히 주석의 토론에서).

해결책은 "그렇게하지 마십시오" 여야 합니다. 이것이 실제 프로젝트에서 일어나는 일입니다. 많은 오래된 API는 결과적으로 추악한 인터페이스 또는 버려진 (항상 null) 매개 변수 또는 완전히 다른 매개 변수 목록으로 DoThisThing ()과 동일한 DoThisThing2 ()라는 함수를 갖습니다. 다른 일반적인 트릭에는 많은 프레임 워크를 넘어서 밀수하기 위해 글로벌 또는 태그 포인터에 정보를 숨기는 것이 포함됩니다. (예를 들어, 오디오 버퍼의 절반이 4 바이트의 마법 값만 포함하는 프로젝트가 있는데, 그 이유는 라이브러리가 오디오 코덱을 호출하는 방식을 변경하는 것보다 훨씬 쉽기 때문입니다.)

특정 코드없이 구체적인 조언을하기는 어렵습니다.


3

자동화 된 테스트. TDD zealot 일 필요도없고 100 % 적용 범위도 필요하지 않지만, 자동화 된 테스트를 통해 자신있게 변경할 수 있습니다. 또한 매우 높은 커플 링 디자인을 가지고있는 것 같습니다. 소프트웨어 설계에서 이러한 종류의 문제를 해결하기 위해 특별히 고안된 SOLID 원리에 대해 읽어야합니다.

이 책들도 추천합니다.

  • 레거시 코드 , 깃털로 효과적으로 작업
  • 리팩토링 , 파울러
  • 테스트 , Freeman 및 Pryce의 지침에 따라 성장하는 객체 지향 소프트웨어
  • 깨끗한 코드 , 마틴

3
귀하의 질문은 "향후에이 [실패]를 어떻게 피할 수 있습니까?"입니다. 정답은 현재 CI와 테스트를 "가지고있는"경우에도 올바르게 적용하지 않는다는 것입니다. 컴파일을 "첫 번째 단위 테스트"로보고 있기 때문에 10 분 이상 지속 된 컴파일 오류가 없었으며, 그것이 깨질 때 수정했습니다. 테스트가 다음과 같이 통과하는 것을 볼 수 있어야하기 때문입니다. 코드를 더 연구하고 있습니다.
asthasr

6
많이 사용되는 인터페이스를 리팩터링하는 경우 심을 추가합니다. 이 심은 기본 설정을 처리하므로 레거시 호출이 계속 작동합니다. shim 뒤의 인터페이스에서 작업 한 다음 완료되면 shim 대신 인터페이스를 다시 사용하도록 클래스를 변경하기 시작합니다.
asthasr

5
빌드 실패에도 불구하고 리팩토링을 계속하는 것은 죽은 계산 과 유사합니다 . 최후 의 항해 기술 입니다 . 리팩토링에서 리팩토링 방향이 잘못되었을 가능성이 있으며 이미 그 표시가 나타납니다 (컴파일이 중지되는 순간 (예 : 대기 속도 표시기가없는 비행)). 결국 비행기는 레이더에서 떨어집니다. 다행히 리팩토링을 위해 블랙 박스 나 조사자가 필요하지 않습니다. 항상 "알려진 마지막 상태로 복원"할 수 있습니다.
rwong

4
@DeadMG : 당신은 "내 경우에는 이전의 전화는 더 이상 의미가 없다"고 썼지 만 당신의 질문 에는 "이를 수용하기 위한 작은 인터페이스 변경"이 있습니다. 솔직히이 두 문장 중 하나만 사실 일 수 있습니다. 그리고 문제 설명에서 인터페이스 변경이 사소한 것이 아니라는 것이 분명 합니다. 변경 사항을 이전 버전과 호환 가능하게 만드는 방법에 대해 더 열심히 생각해야합니다. 내 경험으로는 항상 가능하지만 먼저 좋은 계획을 세워야합니다.
Doc Brown

3
@DeadMG이 경우, 당신이하고있는 일 리팩토링이라고 부를 수 없다고 생각합니다. 이점은 기본적으로 디자인 변경 사항을 일련의 매우 간단한 단계로 적용하는 것입니다.
Jules

3

서로 너무 밀접하게 의존하는 이전 수업의 증상일까요?

아마 그렇습니다. 요구 사항이 충분히 변경되면 다소 훌륭하고 깨끗한 코드 기반으로 유사한 효과를 얻을 수 있지만

앞으로 이런 종류의 계단식 리팩토링을 어떻게 피할 수 있습니까?

레거시 코드 작업을 중단하는 것 외에도 두려워 할 수 없습니다. 그러나 며칠, 몇 주 또는 몇 달 동안 작동 코드 기반이없는 효과를 피하는 방법을 사용하는 것이 가능합니다.

이 방법의 이름은 "Mikado Method"이며 다음과 같이 작동합니다.

  1. 종이에 달성하고자하는 목표를 적어 라

  2. 그 방향으로 당신을 데려 가장 간단한 변경합니다.

  3. 컴파일러와 테스트 스위트를 사용하여 작동하는지 확인하십시오. 7 단계를 계속하면 4 단계를 계속하십시오.

  4. 종이에 현재의 변화를 적용하기 위해 변화해야 할 것들에 주목하십시오. 현재 작업에서 새 작업으로 화살표를 그립니다.

  5. 변경 사항 되돌리기 이것은 중요한 단계입니다. 처음에는 반 직관적이고 육체적으로 아프지 만, 간단한 것을 시도했기 때문에 실제로 그렇게 나쁘지는 않습니다.

  6. 나가는 오류 (알려진 종속성 없음)가없는 작업 중 하나를 선택하고 2로 돌아갑니다.

  7. 변경 사항을 커밋하고 종이에서 작업을 중단하고 나가는 오류가없는 작업 (알려진 종속성 없음)을 선택하고 2로 돌아갑니다.

이렇게하면 짧은 간격으로 작동하는 코드베이스를 갖게됩니다. 나머지 팀의 변경 사항을 병합 할 수도 있습니다. 그리고 당신은 당신이 아직도해야 할 일을 시각적으로 보여줍니다. 이것은 당신이 노력을 계속할 것인지 아니면 멈추어야하는지 결정하는 데 도움이됩니다.


2

리팩토링 은 코드 정리와는 다른 구조화 된 원칙입니다. 시작하기 전에 단위 테스트를 작성해야하며 각 단계는 기능을 변경하지 않아야하는 특정 변환으로 구성되어야합니다. 단위 테스트는 모든 변경 후에 통과해야합니다.

물론 리팩토링 프로세스 중에 파손을 일으킬 수있는 변경 사항을 자연스럽게 발견하게됩니다. 이 경우 새 프레임 워크를 사용하는 이전 인터페이스에 대한 호환성 shim을 구현하기 위해 최선을 다하십시오. 이론적으로 시스템은 여전히 ​​이전과 같이 작동하고 단위 테스트는 통과해야합니다. 호환성 shim을 더 이상 사용되지 않는 인터페이스로 표시하고보다 적절한 시간에 정리할 수 있습니다.


2

... 프로젝트를 리팩터링하여 기능을 추가했습니다.

@Jules가 말했듯이 리팩토링과 기능 추가는 매우 다른 두 가지입니다.

  • 리팩토링은 동작을 변경하지 않고 프로그램 구조를 변경하는 것입니다.
  • 반면에 기능을 추가하면 기능이 향상됩니다.

...하지만 실제로는 내부 작업을 변경하여 물건을 추가해야하지만 때로는 리팩토링보다는 수정이라고 부릅니다.

그것을 수용하기 위해 약간의 인터페이스를 변경해야했습니다.

그곳은 일이 더러워지는 곳입니다. 인터페이스는 구현 방식과 구현 방식을 분리하는 경계로 사용됩니다. 인터페이스를 터치하자마자 어느 쪽이든 (구현 또는 사용) 모든 것을 변경해야합니다. 이것은 당신이 경험 한대로 퍼질 수 있습니다.

그런 다음 소비하는 클래스는 새로운 인터페이스 측면에서 현재 인터페이스로 구현할 수 없으므로 새로운 인터페이스도 필요합니다.

하나의 인터페이스는 변경 소리가 필요하다는 것을 ... 다른 인터페이스로 확산하면 변경이 더 퍼짐을 의미합니다. 체인 아래로 흐르기 위해 어떤 형태의 입력 / 데이터가 필요한 것처럼 들립니다. 그 경우입니까?


당신의 대화는 매우 추상적이므로 이해하기 어렵습니다. 예가 도움이 될 것입니다. 일반적으로 인터페이스는 상당히 안정적이고 서로 독립적이어야하며 인터페이스 덕분에 나머지 부분을 손상시키지 않고 시스템의 일부를 수정할 수 있습니다.

... 실제로, 코드 수정 계단식 방지하는 가장 좋은 방법은 정확하게 있습니다 좋은 인터페이스를 제공합니다. ;)


-1

나는 당신이 기꺼이 물건을 기꺼이 지키지 않으면 보통 할 수 없다고 생각합니다. 그러나 귀하와 같은 상황에서는 팀에 알리고 더 건강한 개발을 계속하기 위해 리팩토링이 필요한 이유를 알려주는 것이 좋습니다. 나는 혼자서 물건을 고치지 않을 것입니다. 나는 스크럼 회의에서 그것에 대해 이야기하고 (당신이 가지고 있다고 가정) 다른 개발자들과 체계적으로 접근합니다.


1
이것은 이전의 9 가지 답변에서 제시되고 설명 된 포인트를 넘어서는 실질적인 것을 제공하지 않는 것 같습니다
gnat

@ gnat : 아마도 아닐 수도 있지만 응답을 단순화했습니다.
Tarik
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.