일반적인 코드로 코드를 리팩터링하는 방법은 무엇입니까?


16

배경

C # 프로젝트를 진행 중입니다. 저는 C # 프로그래머가 아니며 주로 C ++ 프로그래머입니다. 그래서 기본적으로 쉽고 리팩토링 작업이 할당되었습니다.

코드는 엉망입니다. 거대한 프로젝트입니다. 고객이 새로운 기능과 버그 수정을 통해 빈번한 릴리스를 요구함에 따라 다른 모든 개발자는 코딩하는 동안 무차별 대입 방식을 취해야했습니다. 코드는 유지 관리가 불가능하며 다른 모든 개발자가 동의합니다.

나는 그들이 옳은지를 논하기 위해 여기에 있지 않습니다. 리팩토링 할 때 리팩토링 된 코드가 복잡해 보일 때 올바른 방식으로 수행하고 있는지 궁금합니다. 다음은 간단한 예입니다.

문제

여섯 종류가 있습니다 : A, B, C, D, EF. 모든 클래스에는 함수가 ExecJob()있습니다. 6 가지 구현은 모두 매우 유사합니다. 기본적으로 처음 A::ExecJob()에 작성되었습니다. 그런 다음의 B::ExecJob()복사 붙여 넣기 수정 으로 구현 된 약간 다른 버전이 필요했습니다 A::ExecJob(). 약간 다른 버전이 필요할 때 C::ExecJob()작성되었습니다. 6 개의 구현에는 모두 공통 코드가 있고, 다른 코드 줄이 있으며, 다시 공통 코드가 있습니다. 다음은 간단한 구현 예입니다.

A::ExecJob()
{
    S1;
    S2;
    S3;
    S4;
    S5;
}

B::ExecJob()
{
    S1;
    S3;
    S4;
    S5;
}

C::ExecJob()
{
    S1;
    S3;
    S4;
}

SN정확히 같은 진술 그룹이 어디에 있습니까 ?

그것들을 공통으로 만들기 위해 다른 클래스를 만들고 공통 코드를 함수로 옮겼습니다. 매개 변수를 사용하여 실행할 명령문 그룹을 제어하십시오.

Base::CommonTask(param)
{
    S1;
    if (param.s2) S2;
    S3;
    S4;
    if (param.s5) S5;
}

A::ExecJob() // A inherits Base
{
    param.s2 = true;
    param.s5 = true;
    CommonTask(param);
}

B::ExecJob() // B inherits Base
{
    param.s2 = false;
    param.s5 = true;
    CommonTask(param);
}

C::ExecJob() // C inherits Base
{
    param.s2 = false;
    param.s5 = false;
    CommonTask(param);
}

이 예제에서는 세 개의 클래스와 지나치게 단순화 된 명령문 만 사용합니다. 실제로이 CommonTask()함수는 모든 매개 변수 검사와 함께 매우 복잡해 보이고 더 많은 명령문이 있습니다. 또한 실제 코드에는 여러 가지 모양의 CommonTask()함수가 있습니다.

모든 구현이 공통 코드를 공유하고 ExecJob()함수가 더 깔끔해 보이지만 나를 귀찮게하는 두 가지 문제가 있습니다.

  • 의 변경 사항이 CommonTask()있을 경우 6 가지 기능 (및 향후 추가 기능)이 모두 테스트되어야합니다.
  • CommonTask()이미 복잡합니다. 시간이 지남에 따라 더 복잡해집니다.

올바른 방법으로하고 있습니까?


Martin Fowler의 리팩토링 책에는 유용한 리팩토링 코드에 대한 많은 특정 기술이 있습니다.
Allan

답변:


14

예, 당신은 절대적으로 올바른 길에 있습니다!

내 경험상, 상황이 복잡 할 때 작은 단계에서 변화가 일어난다는 것을 알았습니다. 당신이 한 일은 진화 과정 (또는 리팩토링 과정)의 1 단계 입니다. 2 단계와 3 단계는 다음과 같습니다.

2 단계

class Base {
  method ExecJob() {
    S1();
    S2();
    S3();
    S4();
    S5();
  }
  method S1() { //concrete implementation }
  method S3() { //concrete implementation }
  method S4() { //concrete implementation}
  abstract method S2();
  abstract method S5();
}

class A::Base {
  method S2() {//concrete implementation}
  method S5() {//concrete implementation}
}

class B::Base {
  method S2() { // empty implementation}
  method S5() {//concrete implementation}
}

class C::Base {
  method S2() { // empty implementation}
  method S5() { // empty implementation}
}

이것이 '템플릿 디자인 패턴'이며 리팩토링 프로세스에서 한 단계 앞서 있습니다. 기본 클래스가 변경되면 하위 클래스 (A, B, C)는 영향을받을 필요가 없습니다. 새로운 서브 클래스를 비교적 쉽게 추가 할 수 있습니다. 그러나 위의 그림에서 바로 추상화가 손상되었음을 알 수 있습니다. '빈 구현'의 필요성은 좋은 지표입니다. 추상화에 문제가 있음을 나타냅니다. 단기적으로 수용 가능한 솔루션 일 수도 있지만 더 나은 방법이있는 것 같습니다.

3 단계

interface JobExecuter {
  void executeJob();
}
class A::JobExecuter {
  void executeJob(){
     helper = new Helper();
     helper->S1();
     helper->S2();
     helper->S3();
     helper->S4();
     helper->S5();
  }
}

class B::JobExecuter {
  void executeJob(){
     helper = new Helper();
     helper->S1();
     helper->S3();
     helper->S4();
     helper->S5();
  }
}

class C::JobExecuter {
  void executeJob(){
     helper = new Helper();
     helper->S1();
     helper->S3();
     helper->S4();
  }
}

class Base{
   void ExecJob(JobExecuter executer){
       executer->executeJob();
   }
}

class Helper{
    void S1(){//Implementation} 
    void S2(){//Implementation}
    void S3(){//Implementation}
    void S4(){//Implementation} 
    void S5(){//Implementation}
}

이것은 '전략 디자인 패턴'이며 귀하의 경우에 적합한 것으로 보입니다. 작업을 실행하는 전략은 다양하며 각 클래스 (A, B, C)는 다르게 구현합니다.

이 프로세스에는 4 단계 또는 5 단계가 있거나 훨씬 더 나은 리팩토링 접근법이 있다고 확신합니다. 그러나이 코드를 사용하면 중복 코드를 제거하고 변경 사항이 현지화되었는지 확인할 수 있습니다.


"2 단계"에 요약 된 솔루션에서 볼 수있는 주요 문제는 S5의 구체적인 구현이 두 번 존재한다는 것입니다.
user281377

1
예, 코드 중복이 제거되지 않습니다! 그리고 그것은 추상화가 작동하지 않는 또 다른 지표입니다. 프로세스에 대해 어떻게 생각하는지 보여주기 위해 2 단계를 진행하고 싶었습니다. 더 나은 것을 찾는 단계별 접근.
Guven

1
+1 매우 좋은 전략 (그리고 나는 패턴 에 대해 이야기하고 있지 않습니다 )!
Jordão

7

당신은 실제로 옳은 일을하고 있습니다. 나는 이것을 말하기 때문에 :

  1. 공통 작업 기능을 위해 코드를 변경해야하는 경우 공통 클래스에서 코드를 작성하지 않으면 코드를 포함하는 6 개 클래스 모두에서 코드를 변경할 필요가 없습니다.
  2. 코드 줄 수가 줄어 듭니다.

3

이러한 종류의 코드는 이벤트 중심 디자인 (특히 .NET)과 많이 공유됩니다. 가장 유지 관리 가능한 방법은 공유 동작을 가능한 한 작은 덩어리로 유지하는 것입니다.

높은 수준의 코드는 여러 가지 작은 방법을 재사용하고 높은 수준의 코드는 공유 기반에서 제외시킵니다.

리프 / 콘크리트 구현에 많은 보일러 플레이트가 있습니다. 당황하지 마십시오. 괜찮습니다. 모든 코드는 직접적이고 이해하기 쉽습니다. 물건이 부러 질 때 가끔씩 재정렬해야하지만 쉽게 바꿀 수 있습니다.

고급 코드에는 많은 패턴이 있습니다. 때때로 그들은 실제적이며, 대부분 그렇지 않습니다. 거기에 최대 5 개 개의 매개 변수의 "구성" 보면 비슷한,하지만 그들은되지 않습니다. 그들은 완전히 다른 세 가지 전략입니다.

또한 작곡 으로이 모든 작업을 수행 할 수 있으며 상속에 대해 걱정하지 않아도됩니다. 커플 링이 줄어 듭니다.


3

내가 당신이라면 아마도 시작 부분에 UML 기반 연구 1 단계를 더 추가 할 것입니다.

모든 공통 부분을 병합하는 코드를 리팩터링하는 것이 항상 최선의 방법은 아니며, 좋은 접근 방식보다 임시 솔루션처럼 들립니다.

UML 구성표를 작성하고, 간단하지만 효과적인 것을 유지하고, "이 소프트웨어의 기능은 무엇입니까?"와 같은 프로젝트에 대한 기본 개념을 명심하십시오. "이 소프트웨어를 추상적, 모듈 식, 확장 성 등을 유지하는 가장 좋은 방법은 무엇입니까?" "캡슐화를 최상으로 구현하는 방법은 무엇입니까?"

나는 단지 이것을 말하고 있습니다 : 지금 코드를 신경 쓰지 마십시오. 논리를 염두에두면 나머지는 정말 쉬운 작업이 될 수 있습니다. 결국이 모든 종류의 당신이 직면하고있는 문제의 단지 잘못된 논리에 의한 것입니다.


리팩토링을 수행하기 전에 첫 번째 단계 여야합니다. 코드가 매핑 될 정도로 충분히 이해 될 때까지 (uml 또는 다른 야생지도) 리팩토링은 어둠 속에서 설계 될 것입니다.
Kzqai

3

첫 단계는 어디로 가든지 명백하게 큰 방법 A::ExecJob을 작은 조각으로 쪼개는 것입니다.

따라서 대신

A::ExecJob()
{
    S1; // many lines of code
    S2; // many lines of code
    S3; // many lines of code
    S4; // many lines of code
    S5; // many lines of code
}

당신은 얻을

A::ExecJob()
{
    S1();
    S2();
    S3();
    S4();
    S5();
}

A:S1()
{
   // many lines of code
}

A:S2()
{
   // many lines of code
}

A:S3()
{
   // many lines of code
}

A:S4()
{
   // many lines of code
}

A:S5()
{
   // many lines of code
}

여기부터는 여러 가지 방법이 있습니다. 내 취지 : A를 클래스 계층 구조와 ExecJob 가상의 기본 클래스로 만들고 너무 많은 복사 붙여 넣기없이 B, C 등을 쉽게 만들 수 있습니다-ExecJob (현재 5 라이너)을 수정 된 것으로 바꾸십시오. 버전.

B::ExecJob()
{
    S1();
    S3();
    S4();
    S5();
}

그런데 왜 수업이 많을까요? 어쩌면에 필요한 동작을 알 수있는 생성자가있는 단일 클래스로 이들을 모두 대체 할 수 있습니다 ExecJob.



1

먼저 상속이 실제로 작업에 적합한 도구인지 확인해야합니다. 클래스 A에서 사용하는 함수에 공통 위치가 필요하기 때문에 F공통 기본 클래스가 여기에 올바른 것임을 의미하지는 않습니다. 때로는 별도의 도우미 수업은 일을 더 잘합니다. 아닐 수도 있습니다. A에서 F와 공통 기본 클래스 사이의 "is-a"관계에 따라 인공 이름 AF로는 말할 수 없습니다. 여기이 주제를 다루는 블로그 게시물이 있습니다.

공통 기본 클래스가 귀하의 경우에 올바른 것으로 결정한다고 가정 해 봅시다. 그런 다음 두 번째로 코드 조각 S1에서 S5가 각각 기본 클래스 에 대해 별도의 메소드 S1()로 구현되도록해야합니다 S5(). 이후 "ExecJob"기능은 다음과 같아야합니다.

A::ExecJob()
{
    S1();
    S2();
    S3();
    S4();
    S5();
}

B::ExecJob()
{
    S1();
    S3();
    S4();
    S5();
}

C::ExecJob()
{
    S1();
    S3();
    S4();
}

보시다시피 S1 ~ S5는 메소드 호출이므로 더 이상 코드를 차단하지 않으며 코드 중복이 거의 완전히 제거되었으며 더 이상 매개 변수를 확인할 필요가 없으므로 복잡성이 증가하는 문제를 피할 수 있습니다. 그렇지 않으면.

마지막으로, 세 번째 단계 (!)만으로, 모든 ExecJob 메소드를 기본 클래스 중 하나로 결합하는 방법에 대해 생각할 수 있습니다. 여기에서 해당 파트의 실행은 매개 변수, 제안한 방식 또는 제어 방법으로 제어 할 수 있습니다 템플릿 방법 패턴. 실제 코드를 기반으로 귀하의 경우에 노력할만한 가치가 있는지 스스로 결정해야합니다.

그러나 큰 방법을 작은 방법으로 나누는 기본 기술인 IMHO는 패턴을 적용하는 것보다 코드 중복을 피하는 데 훨씬 중요합니다.

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