11000 라인 C ++ 소스 파일에 대해 어떻게해야합니까?


229

그래서 우리는 프로젝트 에이 거대한 (11000 줄이 큽니까?) mainmodule.cpp 소스 파일을 가지고 있으며 그것을 만질 때마다 나는 울고 있습니다.

이 파일은 매우 중앙에 있고 크기가 커지면서 점점 더 많은 코드를 축적하고 실제로 축소하기 시작하는 좋은 방법을 생각할 수 없습니다.

이 파일은 제품의 여러 (> 10) 유지 관리 버전에서 사용되고 적극적으로 변경되므로 리팩토링하기가 실제로 어렵습니다. "간단하게"파일을 세 개의 파일로 분할하면 유지 보수 버전의 변경 사항을 병합하는 것은 악몽이 될 것입니다. 또한 길고 풍부한 기록으로 파일을 분할하면 SCC기록의 오래된 변경 사항을 추적하고 확인 하는 것이 갑자기 훨씬 어려워집니다.

파일은 기본적으로 프로그램의 "메인 클래스"(메인 내부 작업 디스패치 및 조정)를 포함하므로 기능이 추가 될 때마다이 파일과 그 파일이 커질 때마다 영향을줍니다. :-(

이 상황에서 무엇을 하시겠습니까? SCC워크 플로우 를 방해하지 않고 새로운 기능을 별도의 소스 파일로 옮기는 방법에 대한 아이디어가 있습니까?

(도구에 대한 참고 사항 : 우리는 함께 C ++를 사용하여 Visual Studio, 우리는 사용 AccuRev으로 SCC하지만 난의 종류가 생각하는 SCC정말 여기에 문제가되지 않습니다, 우리가 사용하는 Araxis Merge실제 비교 및 파일의 병합을 할)


15
@BoltClock : 실제로 Vim은 꽤 빨리 열 것입니다.
ereOn

58
69305 라인과 계산. 내 동료가 자신의 코드를 대부분 덤프하는 응용 프로그램 파일입니다. 여기에 게시하지 마십시오. 회사에이 사실을보고 할 사람이 없습니다.
Agnel Kurian

204
나는 그것을 얻지 못한다. "그 일을 그만두십시오"라는 의견이 어떻게 많은 찬사를 받습니까? 어떤 사람들은 모든 프로젝트가 처음부터 작성되거나 100 % 민첩한 TDD를 사용하는 동화 나라에 사는 것처럼 보입니다.
Stefan

39
@ 스테판 : 비슷한 코드베이스에 직면했을 때 정확히 그랬습니다. 나는 10 년 된 코드베이스에서 crud를 다루는 데 95 %의 시간을 소비 하고 실제로 코드를 작성하는 데 5 %를 소비하는 것을 좋아하지 않았습니다 . 실제로 시스템의 일부 측면을 테스트하는 것은 불가능 했습니다 (그리고 단위 테스트를 의미하지는 않습니다. 실제로 작동 하는지 확인하기 위해 코드실행 한다는 의미입니다 ). 나는 6 개월의 시험 기간을 지속하지 못했고, 전투를 잃어 버리고 견딜 수없는 코드를 작성하는 데 지쳤다.
이진 걱정

50
파일 분할의 히스토리 추적 측면과 관련하여 : 버전 제어 시스템의 copy 명령을 사용하여 파일을 여러 번 분할하려는 경우 여러 번 복사 한 다음 원하지 않는 각 사본에서 모든 코드를 제거하십시오. 그 파일에. 분할 된 각 파일이 분할을 통해 이력을 다시 추적 할 수 있기 때문에 전체 기록이 유지됩니다 (파일의 대부분의 내용을 크게 삭제하는 것처럼 보입니다).
rmeador

답변:


86
  1. 파일에서 비교적 안정적이며 (빠르게 변경되지 않고 분기마다 크게 다르지 않은) 일부 코드를 찾아 독립적 인 단위로 사용할 수 있습니다. 이것을 자신의 파일로 옮기고 그 문제를 위해 모든 지점의 자체 클래스로 옮기십시오. 안정적이기 때문에 한 지점에서 다른 지점으로 변경 사항을 병합 할 때 원래 만들어진 파일과 다른 파일에 적용해야하는 "많은"어색한 병합이 발생하지 않습니다. 반복.

  2. 파일에서 기본적으로 적은 수의 가지에만 적용되고 독립적으로 작동하는 코드를 찾으십시오. 분기 수가 적기 때문에 빠르게 변경되는지 여부는 중요하지 않습니다. 이것을 자신의 클래스와 파일로 옮기십시오. 반복.

그래서 우리는 모든 곳에서 동일한 코드와 특정 브랜치에 특정한 코드를 제거했습니다.

이렇게하면 잘못 관리 된 코드의 핵이 남습니다. 어디서나 필요하지만 모든 브랜치마다 다릅니다 (및 / 또는 일부 브랜치가 다른 브랜치 뒤에서 실행되도록 지속적으로 변경 됨) 분기 간 병합에 실패했습니다. 그만해. 각 브랜치에서 파일 이름을 바꾸어 파일을 영구적으로 브랜치하십시오. 더 이상 "메인"이 아니며 "구성 X의 메인"입니다. 병합을 통해 여러 분기에 동일한 변경 사항을 적용하는 기능을 잃어 버릴 수 있지만 이는 병합이 제대로 작동하지 않는 코드의 핵심입니다. 어쨌든 충돌을 처리하기 위해 병합을 수동으로 관리 해야하는 경우 각 지점에 독립적으로 수동으로 적용하는 것은 손실이 없습니다.

예를 들어 git의 병합 기능이 사용중인 병합 도구보다 낫기 때문에 SCC의 종류가 중요하지 않다고 말하는 것이 잘못이라고 생각합니다. 따라서 SCC마다 다른 시점에 "병합이 어렵다"는 핵심 문제가 발생합니다. 그러나 SCC를 변경할 수 없을 가능성이 높으므로 문제는 관련이 없습니다.


병합에 관해서는 : GIT를 보았고 SVN을 보았고 Perforce를 보았습니다. 어디에서 본 적도 AccuRev + Araxis를 능가하는 곳은 아무것도 없습니다. :-) (GIT가이 작업을 수행 할 수 있지만 [ stackoverflow.com/questions/1728922/… ] 및 AccuRev는 할 수 없지만 모든 사람이 병합 또는 이력 분석의 일부인지 스스로 결정해야합니다.
Martin Ba

충분히 공평 할 것입니다-아마도 가장 좋은 도구를 이미 가지고있을 것입니다. Git의 브랜치 X의 파일 A에서 발생한 변경 사항을 브랜치 Y의 파일 B로 병합하는 기능은 브랜치 된 파일을보다 쉽게 ​​분할 할 수 있도록해야하지만 아마도 사용하는 시스템에는 장점이 있습니다. 어쨌든, 나는 당신이 git로 전환 할 것을 제안하지 않고 SCC가 여기에서 차이를 만든다고 말하면서도 할인 될 수 있다는 것에 동의합니다 :-)
Steve Jessop

129

앞으로 30000 LOC 파일을 얻을 때와 같이 병합하는 것은 큰 악몽이 아닙니다. 그래서:

  1. 해당 파일에 더 많은 코드를 추가하지 마십시오.
  2. 나눠.

리팩토링 프로세스 중에 코딩을 중단 할 수 없다면 적어도 더 많은 코드를 추가하지 않고도이 큰 파일 잠시 그대로 둘 수 있습니다. 하나의 "주 클래스"가 포함되어 있기 때문에 상속 할 수 있으며 상속 된 클래스를 유지할 수 있습니다. es) 여러 개의 작고 잘 설계된 파일에서 오버로드 된 기능이있는 경우


@ Martin : 다행히도 파일을 여기에 붙여 넣지 않았으므로 구조에 대해 전혀 모릅니다. 그러나 일반적인 아이디어는 논리적 부분으로 나누는 것입니다. 이러한 논리 부분에는 "주 클래스"의 함수 그룹이 포함되거나 여러 보조 클래스로 분할 될 수 있습니다.
Kirill V. Lyadvinsky

3
10 개의 유지 보수 버전과 많은 활성 개발자의 경우 파일을 충분히 오래 동결시킬 수 없습니다.
Kobi

9
@Martin, 트릭을 수행하는 두 가지 GOF 패턴 , mainmodule.cpp의 기능을 매핑 하는 단일 Facade 가 있습니다 (또는 아래 권장 사항). 각각 함수에 매핑되는 Command 클래스 모음을 만듭니다. mainmodule.app의 기능. (내 답변에 이것을 확장했습니다.)
ocodo

2
그렇습니다. 어떤 시점에서 코드 추가를 중단해야하거나 결국 30k, 40k, 50k, kaboom 메인 모듈이 잘못되었습니다. :-)
Chris

67

많은 코드 냄새가 나는 것처럼 들립니다. 우선 모든 주요 계급이 개방 / 폐쇄 원칙 을 위반 한 것으로 보입니다 . 또한 너무 많은 책임을 처리하는 것처럼 들린다 . 이로 인해 코드가 필요한 것보다 부서지기 쉽다고 가정합니다.

리팩토링 후 추적성에 대한 귀하의 우려를 이해할 수 있지만,이 클래스는 유지 관리 및 향상이 다소 어려우며 변경 사항이 있으면 부작용이 발생할 수 있습니다. 나는 이것들의 비용이 클래스 리팩토링 비용보다 크다고 가정한다.

어쨌든 코드 냄새는 시간이 지남에 따라 악화되기 때문에 적어도 어느 시점에서 이들의 비용은 리팩토링 비용을 능가합니다. 당신의 설명에서 나는 당신이 팁 포인트를 지났다고 가정합니다.

리팩토링은 작은 단계로 수행해야합니다. 가능한 경우 리팩토링 하기 전에 자동 테스트를 추가하여 현재 동작을 확인하십시오 . 그런 다음 격리 된 기능의 작은 영역을 선택하고 책임을 위임하기 위해이를 유형으로 추출하십시오.

어쨌든 주요 프로젝트처럼 들리므로 행운을 빕니다 :)


18
그것은 많은 냄새가납니다 : 그것은 Blob 안티 패턴이 집에있는 것처럼 냄새가납니다 ... en.wikipedia.org/wiki/God_object . 그가 가장 좋아하는 음식은 스파게티 코드입니다 : en.wikipedia.org/wiki/Spaghetti_code :-)
jdehaan

@jdehaan : 나는 :) 그것에 대해 외교적으로 노력했다
브라이언 라스무센

+1 나도 테스트를하지 않고 작성한 복잡한 코드조차 감히 다루지 않는다.
Danny Thomas

49

그런 문제에 대해 내가 상상 한 유일한 해결책은 다음과 같습니다. 설명 된 방법에 의한 실제 이득은 진화의 진보성입니다. 여기에는 혁명이 없습니다. 그렇지 않으면 매우 빨리 곤경에 처하게됩니다.

원래 메인 클래스 위에 새로운 cpp 클래스를 삽입하십시오. 현재로서는 기본적으로 모든 호출을 현재 기본 클래스로 리디렉션하지만이 새 클래스의 API를 최대한 명확하고 간결하게 만드는 것을 목표로합니다.

이 작업이 완료되면 새로운 클래스에 새로운 기능을 추가 할 수 있습니다.

기존 기능에 관해서는 새로운 클래스에서 충분히 안정적으로 점진적으로 이동해야합니다. 이 코드 조각에 대한 SCC 도움말이 손실되지만 그에 대해 할 수있는 일은 많지 않습니다. 올바른 타이밍을 선택하십시오.

나는 이것이 도움이 될 수 있기를 바라지 만 이것이 완벽하지 않다는 것을 알고 있으며 프로세스는 귀하의 요구에 맞게 조정되어야합니다!

추가 정보

Git은 한 파일에서 다른 파일로 코드 조각을 따를 수있는 SCC입니다. 나는 그것에 대해 좋은 소식을 들었으므로 점차적으로 작업을 진행하는 동안 도움이 될 수 있습니다.

Git은 올바르게 이해하면 코드 파일 조각을 나타내는 Blob이라는 개념을 중심으로 구성됩니다. 이 조각들을 다른 파일로 옮기면 Git은 수정하더라도 찾을 수 있습니다. 아래 의견에서 언급 한 Linus Torvalds의 비디오 외에는 이것에 대해 분명한 것을 찾을 수 없었습니다.


GIT가 어떻게 수행하는지 / GIT 로 어떻게 수행하는지대한 참조 가 가장 환영받을 것입니다.
Martin Ba

@Martin Git이 자동으로 수행합니다.
Matthew

4
@Martin : Git은 자동으로 파일을 추적하지 않기 때문에 콘텐츠를 추적합니다. 실제로 git에서는 "하나의 파일 히스토리를 얻는"것이 더 어렵다.
Arafangion

1
@Martin youtube.com/watch?v=4XpnKHJAok8 은 Torvalds가 git에 대해 이야기하는 대화입니다. 그는 나중에 대화에서 언급합니다.
Matthew

6
@Martin,이 질문을보십시오 : stackoverflow.com/questions/1728922/…
Benjol

30

공자는 말한다 : "구멍에서 빠져 나가는 첫 단계는 구멍을 파는 것을 멈추는 것이다."


25

다양한 기능 세트와 "사용자 정의"를 촉진하는 영업 관리자가있는 10 명의 고객이 있습니까? 나는 이전에 그런 제품을 작업했습니다. 본질적으로 같은 문제가있었습니다.

당신은 거대한 파일을 갖는 것은 문제가 있다는 것을 알고 있지만, 더 많은 문제는 "현재"로 유지해야하는 10 가지 버전입니다. 그것은 다중 유지 보수입니다. SCC는이를 쉽게 만들 수 있지만 제대로 만들 수는 없습니다.

파일을 여러 부분으로 나누기 전에 모든 코드를 한 번에보고 모양을 만들 수 있도록 열 가지를 서로 다시 동기화해야합니다. 한 번에 하나의 브랜치를 수행하여 동일한 기본 코드 파일에 대해 두 브랜치를 테스트 할 수 있습니다. 사용자 지정 동작을 적용하려면 #ifdef 및 friends를 사용할 수 있지만 정의 된 상수에 대해 일반적인 if / else를 사용하는 것이 좋습니다. 이런 식으로 컴파일러는 모든 유형을 확인하고 어쨌든 "죽은"객체 코드를 제거합니다. (데드 코드에 대한 경고를 끄고 싶을 수도 있습니다.)

모든 브랜치가 내재적으로 공유하는 해당 파일 버전이 하나만 있으면 전통적인 리팩토링 방법을 시작하는 것이 더 쉽습니다.

#ifdef는 영향을받는 코드가 다른 브랜치 별 사용자 정의 컨텍스트에서만 이해되는 섹션에 주로 적합합니다. 이것들은 동일한 브랜치 병합 체계에 대한 기회를 제공하지만, 헛소리하지는 않을 것이라고 주장 할 수 있습니다. 한 번에 하나의 거대한 프로젝트입니다.

단기적으로는 파일이 커지는 것처럼 보입니다. 괜찮습니다. 당신이하고있는 일은 함께해야 할 것들을 모으는 것입니다. 그 후에는 버전에 관계없이 명확하게 동일한 영역이 보이기 시작합니다. 이들은 단독으로 남겨 두거나 마음대로 리팩토링 할 수 있습니다. 다른 영역은 버전에 따라 분명히 다릅니다. 이 경우 여러 가지 옵션이 있습니다. 한 가지 방법은 차이점을 버전 별 전략 개체에 위임하는 것입니다. 다른 하나는 공통 추상 클래스에서 클라이언트 버전을 파생시키는 것입니다. 그러나 다른 브랜치에서 10 개의 "팁"을 개발하는 한 이러한 변형은 가능하지 않습니다.


2
소프트웨어의 한 버전을 사용하는 것이 목표라는 데 동의하지만 구성 파일 (런타임)을 사용하고 시간 맞춤화를 컴파일하지 않는 것이 좋습니다.
Esben Skov Pedersen

또는 각 고객의 빌드에 대한 "구성 클래스"도 있습니다.
tc.

컴파일 타임 또는 런타임 구성은 기능적으로 관련이 없지만 가능성을 제한하고 싶지는 않습니다. 컴파일 타임 구성은 클라이언트가 구성 파일을 해킹하여 추가 기능을 활성화 할 수 없다는 장점이 있습니다. 배포 가능한 "텍스트 개체"코드 대신 모든 구성을 소스 트리에 저장하기 때문입니다. 단점은 런타임에 AlternateHardAndSoftLayers를 선호한다는 것입니다.
Ian

22

이것이 문제를 해결하는지는 모르겠지만 파일의 내용을 서로 독립적으로 작은 파일로 마이그레이션하는 것입니다. 내가 얻는 것은 약 10 가지 버전의 소프트웨어가 떠 다니고 있기 때문에 문제를 일으키지 않고 모두 지원해야한다는 것입니다.

우선 이것이 쉬운 방법 은 없으며 몇 분 동안 브레인 스토밍으로 스스로를 해결할 것입니다. 파일에 링크 된 기능은 모두 응용 프로그램에 매우 중요하며이를 잘라내어 다른 파일로 마이그레이션해도 문제가 해결되지 않습니다.

나는 당신에게 다음과 같은 옵션 만 있다고 생각합니다.

  1. 마이그레이션하거나 가지고있는 것을 그대로 유지하십시오. 작업을 종료하고 디자인이 좋은 심각한 소프트웨어 작업을 시작하십시오. 크래쉬 나 두 번 살아남을 수있을만큼 충분한 자금으로 장시간 프로젝트를 수행하는 경우 극단적 인 프로그래밍이 항상 최상의 솔루션은 아닙니다.

  2. 파일이 분할 된 후 파일을보기 좋게 레이아웃하는 방법을 알아 봅니다. 필요한 파일을 작성하여 애플리케이션에 통합하십시오. 추가 매개 변수를 취하도록 함수의 이름을 바꾸거나 과부하하십시오 (단순한 부울입니까?). 코드 작업을해야하는 경우 작업해야하는 함수를 새 파일로 마이그레이션하고 이전 함수의 함수 호출을 새 함수에 매핑하십시오. 메인 파일을 이런 식으로 유지해야하며 아웃소싱 된시기 등을 정확히 알고있는 특정 기능에 관한 변경 사항을 계속 볼 수 있어야합니다.

  3. 워크 플로가 과대 평가되어 심각한 비즈니스를 수행하려면 애플리케이션의 일부를 다시 작성해야한다는 좋은 케이크를 동료들에게 납득 시키십시오.


19

정확히이 문제는 "레거시 코드로 효과적으로 작업하기"( http://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052 ) 장의 장에서 처리됩니다 .


informit.com/store/product.aspx?isbn=0131177052를 사용하면이 책의 목차 (및 2 개의 샘플 장)를 볼 수 있습니다. 20 장은 얼마나 걸립니까? (어떻게 유용 할 수 있는지 느껴보십시오.)
Martin Ba

17
20 장의 길이는 10,000 줄이지 만 저자는 그것을 소화 가능한 덩어리로 나누는 방법을 연구하고 있습니다 ... 8)
Tony Delroy

1
약 23 페이지이지만 14 이미지가 있습니다. 나는 당신이 그것을 얻어야한다고 생각합니다, 당신은 imho를 어떻게 해야할지 결정하려고 훨씬 자신감을 느낄 것입니다.
Emile Vrijdags

문제에 대한 훌륭한 책이지만 그것이 제안하는 권장 사항 (및이 스레드의 다른 권장 사항)은 모두 공통 요구 사항을 공유합니다. 모든 분기에 대해이 파일을 리팩터링하려면이 작업을 수행 할 수있는 유일한 방법은 모든 분기에 대해 파일을 작성하고 초기 구조 변경을 수행하십시오. 그 주위에 방법이 없습니다. 이 책에서는 중복 메소드를 작성하고 호출을 위임하여 자동 리팩토링 지원없이 하위 클래스를 안전하게 추출하기위한 반복적 인 접근 방법에 대해 설명하지만 파일을 수정할 수없는 경우이 모든 것이 무의미합니다.
Dan Bryant

2
@Martin,이 책은 훌륭하지만 테스트, 리팩터링, 테스트주기에 상당히 의존하고 있습니다. 나는 비슷한 상황에 있었고이 책은 내가 찾은 가장 도움이되었습니다. 당신이 가진 못생긴 문제에 대한 좋은 제안이 있습니다. 그러나 그림에 어떤 종류의 테스트 하네스를 얻을 수 없다면 세계의 모든 리팩토링 제안이 도움이되지 않습니다.

14

mainmodule.cpp의 API 포인트에 매핑되는 명령 클래스 세트를 만드는 것이 가장 좋습니다 .

일단 배치되면 명령 클래스를 통해 이러한 API 포인트에 액세스하기 위해 기존 코드베이스를 리팩터링해야합니다. 완료되면 각 명령의 구현을 새 클래스 구조로 리팩터링 할 수 있습니다.

물론 단일 클래스의 11 KLOC를 사용하면 코드가 매우 결합되어 있고 취하기 쉽지만 개별 명령 클래스를 작성하면 다른 프록시 / 외관 전략보다 훨씬 도움이됩니다.

나는 과제를 부러워하지 않지만 시간이 지남에 따라이 문제가 해결되지 않으면 악화 될 것입니다.

최신 정보

Command 패턴이 Facade보다 선호되는 것이 좋습니다.

(상대적으로) 모 놀리 식 외관에 대해 많은 다른 명령 클래스를 유지 관리 / 구성하는 것이 좋습니다. 단일 Facade를 11 KLOC 파일에 매핑하는 것은 아마도 몇 개의 다른 그룹으로 나뉘어 야 할 것입니다.

왜 이러한 파사드 그룹을 알아 내려고 귀찮게합니까? 명령 패턴을 사용하면 이러한 작은 클래스를 유기적으로 그룹화하고 구성 할 수 있으므로 훨씬 더 유연합니다.

물론 두 가지 옵션 모두 하나의 11 KLOC보다 우수하고 증가하는 파일입니다.


동일한 아이디어로 내가 제안한 솔루션의 대안 +1 : 큰 문제를 작은 문제로 분리하기 위해 API를 변경하십시오.
Benoît

13

한 가지 중요한 조언 : 리팩토링과 버그 수정을 혼합하지 마십시오. 원하는 것은 소스 코드가 다르다는 점을 제외하고는 이전 버전 과 동일한 프로그램 버전입니다 .

한 가지 방법은 가장 큰 기능 / 부분을 자체 파일로 분할 한 다음 헤더를 사용하여 포함시킬 수 있습니다 (따라서 main.cpp를 #includes 목록으로 바꾸면 코드 냄새 자체가 들립니다. C ++ 전문가이지만) 적어도 파일로 나뉩니다.

그런 다음 모든 유지 관리 릴리스를 "new"main.cpp 또는 구조에 관계없이 전환 할 수 있습니다. 다시 : 다른 변경이나 버그 수정을 추적하는 것은 지옥과 혼동되기 때문에.

다른 것 : 한 번에 전체를 리팩토링하는 데 큰 성공을 거두기를 원하는만큼, 씹을 수있는 것보다 더 많이 물릴 수 있습니다. 아마도 하나 또는 두 개의 "파트"를 선택하여 모든 릴리스로 가져간 다음 고객에게 더 많은 가치를 추가하십시오 (결국 리팩토링은 직접적인 가치를 추가하지 않으므로 정당화 해야하는 비용입니다). 하나 또는 두 부분.

분명히 팀에서 실제로 분할 파일을 사용하고 main.cpp에 새로운 것을 추가하는 것뿐만 아니라 대규모 리팩터링을 시도하는 것이 최선의 행동 과정이 아닐 수도 있습니다.


1
팩토링 아웃 및 #include에 +1. 다시 10 개의 브랜치에 대해이 작업을 수행 한 경우 (여기서 약간의 작업이 가능하지만 관리 가능) 모든 브랜치에 변경 사항을 게시하는 문제가 여전히 발생하지만 해당 문제는 ' t는 (필수적으로) 확장되었습니다. 못생긴가요? 예, 여전히 그렇습니다. 그러나 문제에 대해 약간의 합리성을 가져올 수 있습니다. 정말 큰 제품을 유지 관리하고 수리하는 데 몇 년을 보냈지 만 유지 관리에는 많은 어려움이 따른다는 것을 알고 있습니다. 최소한 그것으로부터 배우고 다른 사람들에게주의 이야기를 제공하십시오.
Jay

10

Rofl, 이것은 나의 오래된 직업을 생각 나게한다. 내가 합류하기 전에 모든 것이 하나의 거대한 파일 (또한 C ++) 안에있는 것처럼 보입니다. 그런 다음 (포함을 사용하여 완전히 임의의 지점에서) 약 3 개 (여전히 거대한 파일)로 분할했습니다. 예상대로이 소프트웨어의 품질은 끔찍했습니다. 이 프로젝트는 약 40k LOC에 달했습니다. (의견은 거의 없지만 중복 코드의 LOTS는 포함)

결국 나는 프로젝트를 완전히 다시 작성했습니다. 프로젝트의 최악의 부분을 처음부터 다시 시작했습니다. 물론 나는이 새로운 부분과 나머지 부분 사이에 가능한 (작은) 인터페이스를 염두에 두었습니다. 그런 다음이 부분을 이전 프로젝트에 삽입했습니다. 필요한 인터페이스를 만들기 위해 이전 코드를 리팩터링하지 않고 그냥 교체했습니다. 그런 다음 거기에서 작은 단계를 수행하여 이전 코드를 다시 작성했습니다.

나는 이것이 반 년이 걸렸고 그 기간 동안 버그 수정 옆에 오래된 코드베이스가 개발되지 않았다고 말해야합니다.


편집하다:

크기는 약 40k LOC에 머물 렀지 만 새로운 응용 프로그램에는 8 년 된 소프트웨어보다 초기 버전에서 더 많은 기능과 버그가 거의 포함되지 않았습니다. 재 작성의 한 가지 이유는 또한 새로운 기능이 필요했고 이전 코드에 도입하는 것이 거의 불가능했기 때문입니다.

이 소프트웨어는 내장형 시스템, 라벨 프린터 용이었습니다.

추가해야 할 또 다른 요점은 이론적으로 프로젝트는 C ++이라는 것입니다. 그러나 OO는 아니었고 C 일 수도있었습니다. 새로운 버전은 객체 지향적이었습니다.


9
리팩토링에 대한 주제에서 "처음부터"들을 때마다 나는 고양이를 죽입니다!
Kugel

나는 아주 비슷한 소리를 냈지만, 내가 다루어야 할 주요 프로그램 루프는 ~ 9000 LOC에 불과했습니다. 그리고 그것은 충분히 나빴다.
AndyUK

8

프로덕션 코드의 API를 다시 작성하는 것은 시작으로 나쁜 생각입니다. 두 가지 일이 발생해야합니다.

하나, 실제로 팀에서이 파일의 현재 프로덕션 버전에서 코드 동결을 수행하도록 결정해야합니다.

둘째,이 프로덕션 버전을 가져 와서 사전 처리 지시문을 사용하여 빌드를 관리하는 브랜치를 작성하여 큰 파일을 분할해야합니다. JUST 전 처리기 지시문 (#ifdefs, #includes, #endifs)을 사용하여 컴파일을 나누는 것이 API를 코딩하는 것보다 쉽습니다. SLA 및 지속적인 지원이 훨씬 쉽습니다.

여기서는 클래스 내의 특정 하위 시스템과 관련된 함수를 잘라 내고 mainloop_foostuff.cpp 파일에 넣고 올바른 위치의 mainloop.cpp에 포함시킬 수 있습니다.

또는

더 많은 시간이 걸리지 만 강력한 방법은 사물을 포함시키는 방법에 이중 간접적 인 내부 종속성 구조를 고안하는 것입니다. 이를 통해 사물을 분할하고 공동 종속성을 관리 할 수 ​​있습니다. 이 방법은 위치 코딩이 필요하므로 적절한 주석과 결합해야합니다.

이 방법에는 컴파일하는 변형을 기반으로 사용되는 구성 요소가 포함됩니다.

기본 구조는 mainclass.cpp에 다음과 같은 명령문 블록 뒤에 MainClassComponents.cpp라는 새 파일이 포함된다는 것입니다.

#if VARIANT == 1
#  define Uses_Component_1
#  define Uses_Component_2
#elif VARIANT == 2
#  define Uses_Component_1
#  define Uses_Component_3
#  define Uses_Component_6
...

#endif

#include "MainClassComponents.cpp"

MainClassComponents.cpp 파일의 기본 구조는 다음과 같이 하위 구성 요소 내에서 종속성을 해결하기위한 것입니다.

#ifndef _MainClassComponents_cpp
#define _MainClassComponents_cpp

/* dependencies declarations */

#if defined(Activate_Component_1) 
#define _REQUIRES_COMPONENT_1
#define _REQUIRES_COMPONENT_3 /* you also need component 3 for component 1 */
#endif

#if defined(Activate_Component_2)
#define _REQUIRES_COMPONENT_2
#define _REQUIRES_COMPONENT_15 /* you also need component 15 for this component  */
#endif

/* later on in the header */

#ifdef _REQUIRES_COMPONENT_1
#include "component_1.cpp"
#endif

#ifdef _REQUIRES_COMPONENT_2
#include "component_2.cpp"
#endif

#ifdef _REQUIRES_COMPONENT_3
#include "component_3.cpp"
#endif


#endif /* _MainClassComponents_h  */

이제 각 구성 요소에 대해 component_xx.cpp 파일을 만듭니다.

물론 숫자를 사용하고 있지만 코드를 기반으로 더 논리적 인 것을 사용해야합니다.

전처리기를 사용하면 제작 과정에서 악몽 인 API 변경에 대해 걱정할 필요없이 작업을 분할 할 수 있습니다.

생산이 확정되면 실제로 재 설계 작업을 수행 할 수 있습니다.


이것은 처음에는 고통 스럽지만 경험이 많은 결과처럼 보입니다.
JBR 윌킨슨

실제로 이것은 Borland C ++ 컴파일러에서 헤더 파일 관리를위한 Pascal 스타일 용도를 모방하기 위해 사용 된 기술입니다. 특히 그들이 텍스트 기반 윈도우 시스템의 초기 포트를 할 때.
엘프 킹

8

글쎄, 나는 당신의 고통을 이해합니다 :) 나는 몇 가지 프로젝트에 있었지만 꽤 좋지 않습니다. 이에 대한 쉬운 대답은 없습니다.

당신에게 효과적 일 수있는 한 가지 접근 방식은 모든 기능에 안전 가드를 추가하는 것, 즉 인수, 메소드의 사전 / 사후 조건을 확인한 다음 궁극적으로 소스의 현재 기능을 캡처하기 위해 단위 테스트를 모두 추가하는 것입니다. 이 기능을 사용하면 무언가를 잊어 버린 경우 경고 및 오류 메시지가 표시되므로 코드를 리팩터링하는 것이 좋습니다.

때로는 리팩토링이 이익보다 더 많은 고통을 가져올 때가 있습니다. 그런 다음 원래 프로젝트와 의사 유지 상태를 그대로두고 처음부터 시작한 다음 짐승의 기능을 점차적으로 추가하는 것이 좋습니다.


4

파일 크기를 줄이는 것이 아니라 클래스 크기를 줄이는 데 관심이 없어야합니다. 그것은 거의 동일하지만, 다른 각도에서 문제를 보도록 만듭니다 (@Brian Rasmussen이 제안한 것처럼 수업은 많은 책임을 져야합니다).


항상 그렇듯이, 나는 downvote에 대한 설명을 원합니다.
Björn Pollex

4

당신이 가지고있는 것은 blob 이라고 알려진 유명한 디자인 antipattern의 고전적인 예 입니다. 여기서 지적한 기사를 읽으려면 약간의 시간이 걸리며 유용한 정보가있을 수 있습니다. 또한이 프로젝트가 외형만큼 클 경우 제어 할 수없는 코드로 커지지 않도록 일부 디자인을 고려해야합니다.


4

이것은 큰 문제에 대한 답이 아니라 특정 부분에 대한 이론적 해결책입니다.

  • 큰 파일을 서브 파일로 분할 할 위치를 찾으십시오. 각 지점에서 특별한 형식으로 의견을 작성하십시오.

  • 그 시점에서 파일을 서브 파일로 분리하는 상당히 간단한 스크립트를 작성하십시오. (특별 주석에는 스크립트에서 분할 방법에 대한 지침으로 사용할 수있는 파일 이름이 포함되어있을 수 있습니다.) 주석을 분할의 일부로 유지해야합니다.

  • 스크립트를 실행하십시오. 원본 파일을 삭제하십시오.

  • 분기에서 병합해야 할 경우 먼저 조각을 다시 연결하여 큰 파일을 다시 만들고 병합 한 다음 다시 분할하십시오.

또한 SCC 파일 히스토리를 유지하려면 소스 제어 시스템에 개별 조각 파일이 원본의 사본임을 알리는 것이 가장 좋습니다. 그런 다음 해당 파일에 보관 된 섹션의 기록을 보존하지만 물론 큰 부분이 "삭제"되었음을 기록합니다.


4

너무 위험없이 분할하는 한 가지 방법은 모든 회선 변경 사항을 역사적으로 살펴 보는 것입니다. 다른 것보다 안정적인 특정 기능이 있습니까? 당신이 원한다면 변화의 핫스팟.

몇 년 동안 줄이 변경되지 않았다면 너무 걱정하지 않고 다른 파일로 옮길 수 있습니다. 주어진 줄을 건드린 마지막 개정판으로 주석이 달린 소스를 살펴보고 끌어낼 수있는 기능이 있는지 확인합니다.


다른 사람들도 비슷한 것을 제안했다고 생각합니다. 이것은 짧고 요점이며 이것이 원래 문제의 유효한 출발점이 될 것이라고 생각합니다.
Martin Ba

3

와우. 나는 상사에게 설명하는데 짐승을 리팩터링하는 데 많은 시간이 필요하다고 생각합니다. 그가 동의하지 않으면, 종료는 옵션입니다.

어쨌든, 내가 제안하는 것은 기본적으로 모든 구현을 버리고 새로운 모듈로 다시 그룹화하는 것입니다. "글로벌 서비스"라고합시다. "메인 모듈"은 해당 서비스로만 전달되며, 작성하는 새로운 코드는 "메인 모듈"대신이 코드를 사용합니다. 이 작업은 합리적인 시간 내에 실행 가능해야합니다 (주로 복사하여 붙여 넣기 때문에). 기존 코드를 위반하지 않으며 한 번에 하나의 유지 관리 버전으로 수행 할 수 있습니다. 그리고 여전히 남은 시간이 있다면 글로벌 서비스를 사용하기 위해 모든 오래된 종속 모듈을 리팩토링하는 데 소비 할 수 있습니다.


3

내 동정-이전 직장에서 처리해야 할 파일보다 몇 배 더 큰 파일과 비슷한 상황이 발생했습니다. 해결책은 다음과 같습니다.

  1. 해당 프로그램의 기능을 철저히 테스트하는 코드를 작성하십시오. 아직 손에 들지 않은 것 같습니다.
  2. 도우미 / 유틸리티 클래스로 추상화 할 수있는 코드를 식별하십시오. 크지 않아도됩니다. 단지 '주요'수업의 일부가 아닌 것입니다.
  3. 2.에서 식별 된 코드를 별도의 클래스로 리팩터링하십시오.
  4. 파손 된 것이 없는지 테스트를 다시 실행하십시오.
  5. 시간이 있으면 2로 이동하여 코드 관리가 가능하도록 필요에 따라 반복하십시오.

3 단계에서 빌드 한 클래스는 새로 명확해진 기능에 적합한 더 많은 코드를 흡수하도록 확장 될 것입니다.

나는 또한 추가 할 수있다 :

0 : 레거시 코드 작업에 대한 Michael Feathers의 책 구매

불행히도 이러한 유형의 작업은 너무 일반적이지만 내 경험에 따르면 작업을 유지하면서 작업하지만 끔찍한 코드를 점차 덜 무섭게 만들 수 있다는 점에서 큰 가치가 있습니다.


2

전체 응용 프로그램을보다 합리적인 방법으로 다시 작성하는 방법을 고려하십시오. 아이디어의 실현 가능성을 확인하기 위해 작은 섹션을 프로토 타입으로 다시 작성하십시오.

실행 가능한 솔루션을 식별 한 경우 그에 따라 응용 프로그램을 리 팩터하십시오.

보다 합리적인 아키텍처를 생성하려는 모든 시도가 실패하면 적어도 솔루션이 프로그램 기능을 재정의하고있는 것입니다.


+1-다른 사람이 자신의 더미를 뱉을 수도 있지만 자신의 시간에 다시 작성하십시오.
Jon Black

2

내 0.05 유로 센트 :

전체 엉망을 다시 디자인하고 기술 및 비즈니스 요구 사항을 고려하여 하위 시스템으로 분할하십시오 (= 각각 잠재적으로 다른 코드베이스가있는 많은 병렬 유지 관리 트랙, 수정 가능성이 높은 것 등).

서브 시스템으로 분할 할 때 가장 많이 변경된 위치를 분석하고 변경되지 않은 부분과 분리하십시오. 문제 지점이 표시됩니다. 모듈 API를 그대로 유지하고 항상 BC를 깰 필요가 없도록 가장 많이 변경되는 부분을 자체 모듈 (예 : dll)로 분리하십시오. 이렇게하면 필요한 경우 코어를 변경하지 않고 유지 보수 지점마다 다른 버전의 모듈을 배치 할 수 있습니다.

재 설계는 별도의 프로젝트 일 필요가있을 것입니다. 움직이는 목표물에 대한 시도는 효과가 없습니다.

소스 코드 히스토리에 관해서는 내 의견 : 새 코드에서는 잊어 버린다. 그러나 필요한 경우 기록을 확인할 수 있도록 기록을 어딘가에 보관하십시오. 나는 당신이 처음부터 그렇게 많이 필요하지 않을 것입니다.

이 프로젝트에 대해 경영진을 인수해야 할 가능성이 높습니다. 개발 시간 단축, 버그 감소, 유지 관리 용이성 및 전반적인 혼돈이 줄어든다고 할 수 있습니다. "중요한 소프트웨어 자산의 미래 보장 및 유지 보수 실행 가능성을 사전에 활성화하십시오."

이것이 내가 문제를 해결하기 시작한 방법입니다.


2

주석을 추가하여 시작하십시오. 함수가 호출되는 위치 및 사물을 이동할 수 있는지 여부를 참조하십시오. 이것은 물건을 움직일 수 있습니다. 코드가 얼마나 취약한 지 평가해야합니다. 그런 다음 공통 기능 비트를 함께 이동하십시오. 한 번에 작은 변화.



2

내가 유용하게 생각하는 것은 (그리고 지금 내가 직면하고있는 규모는 아니지만 지금하고 있습니다) 메소드를 클래스 (메소드 객체 리팩토링)로 추출하는 것입니다. 다른 버전에서 다른 메소드는 필요한 다른 동작을 제공하기 위해 공통 기반에 주입 될 수있는 다른 클래스가됩니다.


2

이 문장이 귀하의 게시물에서 가장 흥미로운 부분 인 것으로 나타났습니다.

> 파일은 제품의 여러 유지 보수 버전 (> 10)에서 사용 및 적극적으로 변경되므로 리팩토링하기가 실제로 어렵습니다.

먼저 분기를 지원하는 10 + 유지 보수 버전을 개발하기 위해 소스 제어 시스템을 사용하는 것이 좋습니다.

둘째, 10 개의 분기 (각 유지 관리 버전마다 하나씩)를 만듭니다.

벌써 울고 있음을 느낄 수 있습니다! 그러나 기능 부족으로 인해 소스 제어가 상황에 맞지 않거나 올바르게 사용되지 않습니다.

이제 작업중인 지점으로 이동하십시오. 제품의 다른 9 개 지점을 화나게하지 않을 것이라는 점에서 안전하다고 생각되면 리팩토링하십시오.

main () 함수에 너무 많은 관심이 있습니다.

필자가 작성한 모든 프로젝트에서 main ()을 사용하여 시뮬레이션 또는 응용 프로그램 객체와 같은 핵심 객체의 초기화 만 수행합니다.이 클래스는 실제 작업이 진행되는 곳입니다.

또한 프로그램 전체에서 전역 적으로 사용하기 위해 main에서 응용 프로그램 로깅 객체를 초기화합니다.

마지막으로 DEBUG 빌드에서만 활성화되도록 전 처리기 블록에 누출 감지 코드를 추가합니다. 이것이 내가 main ()에 추가 할 전부입니다. Main ()은 짧아야합니다!

너는 ~라고 말한다

> 파일에는 기본적으로 프로그램의 "메인 클래스"(메인 내부 작업 디스패치 및 조정)가 포함됩니다.

이 두 가지 작업을 코디네이터와 작업 디스패처라는 두 개의 개별 개체로 나눌 수있는 것처럼 들립니다.

이를 분리하면 "SCC 워크 플로"가 엉망이 될 수 있지만 SCC 워크 플로를 엄격하게 준수하면 소프트웨어 유지 관리 문제가 발생합니다. 그것을 버리면 뒤돌아 보지 마십시오. 고치면 곧 잠들기 시작합니다.

결정을 내릴 수 없다면 관리자와 치아와 손톱을 싸워서 응용 프로그램을 리팩터링해야합니다. 대답을 거절하지 마십시오!


내가 알다시피, 문제는 이것입니다 : 총알을 물고 리팩터링하면 더 이상 버전간에 패치를 전달할 수 없습니다. SCC가 완벽하게 설정되었을 수 있습니다.
peterchen

@ peterchen-정확히 문제. SCC는 파일 수준에서 병합합니다. (3 방향 병합) 파일간에 코드를 이동하면 수정 된 코드 블록을 한 파일에서 다른 파일로 수동으로 피들 링해야합니다. (다른 의견에서 언급 된 GIT 기능은 내가 말할 수있는 한 병합하는 것이 아니라 역사에 적합하다)
Martin Ba

2

설명했듯이 주요 문제는 프리 스플릿과 포스트 스플릿을 비교하여 버그 수정 등을 병합하는 것입니다. 펄, 루비 등으로 스크립트를 하드 코딩하는 데 오랜 시간이 걸리지 않아 프리 스플릿 분할에서 발생하는 노이즈를 포스트 스플릿 연결과 비교할 수 있습니다. 소음 처리 측면에서 가장 쉬운 방법을 수행하십시오.

  • 연결 전 / 중에 특정 라인 제거 (예 : 가드 포함)
  • 필요한 경우 diff 출력에서 ​​다른 것들을 제거하십시오

체크인이있을 때마다 연결이 실행되고 단일 파일 버전과 비교할 수있는 준비가되어 있습니다.


2
  1. 이 파일과 코드를 다시 만지지 마십시오!
  2. 치료는 당신이 붙어있는 것과 같습니다. 인코딩 된 기능에 맞는 어댑터 작성을 시작하십시오.
  3. 다른 단위로 새 코드를 작성하고 괴물의 기능을 캡슐화하는 어댑터와 만 대화하십시오.
  4. ... 위의 방법 중 하나만 사용할 수없는 경우 작업을 종료하고 새로운 작업을 받으십시오.

2
+/- 0-진지하게, 당신은 이런 기술적 인 세부 사항에 근거하여 직업을 포기할 것을 권장하는 사람들이 어디에 살고 있습니까?
Martin Ba

1

"파일에는 기본적으로 프로그램의"메인 클래스 "(메인 내부 작업 디스패치 및 조정)가 포함되어 있으므로 기능이 추가 될 때마다이 파일과 그 파일이 커질 때마다 영향을줍니다."

큰 스위치 (내 생각에)가 주요 유지 보수 문제가되면 사전 및 명령 패턴을 사용하도록 리팩터링하고 기존 코드에서 로더로 모든 스위치 로직을 제거하여 해당 맵을 채 웁니다.

    // declaration
    std::map<ID, ICommand*> dispatchTable;
    ...

    // populating using some loader
    dispatchTable[id] = concreteCommand;

    ...
    // using
    dispatchTable[id]->Execute();

2
아니요, 실제로 큰 스위치는 없습니다. 문장은 내가이 혼란을 설명 할 수있는 가장 가까운 것입니다. :)
Martin Ba

1

파일을 분할 할 때 소스 기록을 추적하는 가장 쉬운 방법은 다음과 같습니다.

  1. SCM 시스템이 제공하는 기록 보존 복사 명령을 사용하여 원본 소스 코드를 복사하십시오. 이 시점에서 제출해야 할 수도 있지만 빌드 시스템에 새 파일에 대해 아직 알려줄 필요가 없으므로 괜찮습니다.
  2. 이 사본에서 코드를 삭제하십시오. 그것은 당신이 유지하는 라인의 역사를 깨뜨리지 않아야합니다.

"SCM 시스템이 제공하는 기록 보존 복사 명령 사용"... 제공하지 않는 나쁜 점
Martin Ba

너무 나쁘다. 그것만으로도 더 현대적인 것으로 전환해야 할 좋은 이유처럼 들립니다. :-)
Christopher Creutzig

1

나는이 상황에서 내가 할 일이 총알과 비트라고 생각합니다.

  1. 파일을 어떻게 나누고 싶었는지 파악하십시오 (현재 개발 버전을 기반으로 함)
  2. 파일에 관리 잠금을 설정하십시오 ( "금요일 오후 5시 이후에 아무도 터치하지 마십시오 mainmodule.cpp !!!"
  3. 긴 주말 동안 최신 버전에서 최신 버전으로 변경 사항을 적용하여 현재 버전까지 적용하십시오.
  4. 지원되는 모든 버전의 소프트웨어에서 mainmodule.cpp를 삭제하십시오. 새로운 시대입니다-더 이상 mainmodule.cpp가 없습니다.
  5. 경영진에게 소프트웨어의 여러 유지 관리 버전을 지원해서는 안된다고 확신합니다 (적어도 큰 $$ 지원 계약이없는 경우). 각 고객마다 고유 한 버전이있는 경우 ... yeeeeeshhhh. 10 + 포크를 유지하려고하는 대신 컴파일러 지시문을 추가하고 있습니다.

파일의 오래된 변경 사항을 추적하는 것은 "mainmodule.cpp에서 분리"와 같은 첫 번째 체크인 주석으로 간단히 해결됩니다. 최근에 되돌아 가야 할 경우, 대부분의 사람들은 그 변화를 기억할 것입니다. 지금부터 2 년이 지나면 의견은 어디를 볼 것인지 알려줍니다. 물론 코드를 변경 한 사람과 이유를 확인하기 위해 2 년 이상 거슬러 올라가는 것이 얼마나 가치가 있습니까?

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.