จะ refactor code เป็นรหัสทั่วไปได้อย่างไร


16

พื้นหลัง

ฉันกำลังทำงานในโครงการ C # ต่อเนื่อง ฉันไม่ใช่โปรแกรมเมอร์ C # ซึ่งส่วนใหญ่เป็นโปรแกรมเมอร์ C ++ ดังนั้นฉันจึงได้รับมอบหมายงานที่ง่ายและเป็นโครงสร้างใหม่

รหัสเป็นระเบียบ มันเป็นโครงการขนาดใหญ่ เนื่องจากลูกค้าของเราเรียกร้องให้มีการเปิดตัวคุณสมบัติใหม่ ๆ และการแก้ไขข้อผิดพลาดบ่อยครั้งนักพัฒนาอื่น ๆ ทั้งหมดจึงถูกบังคับให้ใช้วิธีการเดรัจฉานบังคับในขณะที่เข้ารหัส รหัสไม่สามารถรักษาได้สูงนักและผู้พัฒนารายอื่นก็เห็นด้วย

ฉันไม่ได้มาที่นี่เพื่ออภิปรายว่าพวกเขาทำถูกหรือไม่ ขณะที่ฉันกำลังปรับโครงสร้างใหม่ฉันสงสัยว่าฉันกำลังทำอย่างถูกวิธีเพราะรหัสการปรับโครงสร้างใหม่ของฉันดูเหมือนจะซับซ้อน! นี่คืองานของฉันเป็นตัวอย่างง่ายๆ

ปัญหา

มีหกชั้นเรียน: A, B, C, D, และE ทุกชั้นเรียนมีฟังก์ชั่นF ExecJob()การใช้งานทั้งหกแบบนั้นคล้ายคลึงกันมาก โดยทั่วไปในตอนแรกA::ExecJob()เขียน จากนั้นรุ่นที่แตกต่างกันเล็กน้อยที่ถูกต้องซึ่งได้ดำเนินการในB::ExecJob()โดย A::ExecJob()copy-paste-การเปลี่ยนแปลงของ เมื่อจำเป็นต้องใช้เวอร์ชันอื่นที่แตกต่างกันเล็กน้อยC::ExecJob()เขียนขึ้นและต่อไป การใช้งานทั้งหกมีรหัสทั่วไปบางส่วนจากนั้นบางบรรทัดที่แตกต่างกันรหัสแล้วอีกบางรหัสทั่วไปและอื่น ๆ นี่เป็นตัวอย่างง่ายๆของการใช้งาน:

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()คุณสมบัติทั้งหก (และอาจมีมากขึ้นในอนาคต) จำเป็นต้องได้รับการทดสอบ
  • CommonTask()ซับซ้อนอยู่แล้ว มันจะซับซ้อนขึ้นเมื่อเวลาผ่านไป

ฉันกำลังทำอย่างถูกวิธี?


หนังสือRefactoringของ Martin Fowler มีเทคนิคเฉพาะมากมายสำหรับการเปลี่ยนรหัสที่คุณอาจพบว่ามีประโยชน์
อัลลัน

คำตอบ:


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 อย่างเป็นรูปธรรมมีอยู่สองครั้ง
281377

1
ใช่การทำสำเนารหัสไม่ได้ถูกกำจัด! และนั่นก็เป็นตัวบ่งชี้ว่าสิ่งที่เป็นนามธรรมไม่ทำงาน ฉันแค่อยากจะวางขั้นตอนที่ 2 ไว้ที่นั่นเพื่อแสดงว่าฉันคิดอย่างไรเกี่ยวกับกระบวนการ วิธีการทีละขั้นตอนในการค้นหาสิ่งที่ดีกว่า
Guven

1
+1 กลยุทธ์ที่ดีมาก (และฉันไม่ได้พูดถึงรูปแบบ )!
Jordão

7

คุณกำลังทำสิ่งที่ถูกต้องจริง ๆ ฉันพูดแบบนี้เพราะ:

  1. หากคุณต้องการเปลี่ยนรหัสสำหรับฟังก์ชั่นการทำงานทั่วไปคุณไม่จำเป็นต้องเปลี่ยนรหัสในคลาสทั้งหมด 6 คลาสซึ่งจะมีรหัสหากคุณไม่ได้เขียนมันในคลาสทั่วไป
  2. จำนวนบรรทัดของรหัสจะลดลง

3

คุณเห็นการเรียงลำดับของรหัสนี้มากในการออกแบบที่ขับเคลื่อนด้วยเหตุการณ์ (. NET โดยเฉพาะ) วิธีบำรุงรักษามากที่สุดคือการทำให้พฤติกรรมที่ใช้ร่วมกันของคุณเป็นชิ้นเล็ก ๆ เท่าที่จะทำได้

ปล่อยให้รหัสระดับสูงนำมาใช้ใหม่เป็นวิธีการเล็ก ๆ แล้วปล่อยให้รหัสระดับสูงออกจากฐานที่ใช้ร่วมกัน

คุณจะมีจานหม้อไอน้ำจำนวนมากในการใช้งานใบ / คอนกรีตของคุณ อย่าตกใจมันก็โอเค รหัสทั้งหมดนั้นตรงและเข้าใจง่าย คุณจะต้องจัดเรียงใหม่เป็นครั้งคราวเมื่อสิ่งต่าง ๆ แตก แต่มันจะง่ายต่อการเปลี่ยนแปลง

คุณจะเห็นรูปแบบมากมายในรหัสระดับสูง บางครั้งพวกเขาก็เป็นจริงส่วนใหญ่เวลาที่พวกเขาไม่ได้ "การกำหนดค่า" ของพารามิเตอร์ห้าตัวที่นั่นดูคล้ายกัน แต่ไม่ใช่ พวกเขาเป็นสามกลยุทธ์ที่แตกต่างอย่างสิ้นเชิง

นอกจากนี้ต้องการทราบว่าคุณสามารถทำสิ่งนี้ทั้งหมดด้วยองค์ประกอบและไม่ต้องกังวลเกี่ยวกับการสืบทอด คุณจะมีเพศสัมพันธ์น้อย


3

ถ้าฉันเป็นคุณฉันอาจจะเพิ่มอีก 1 ขั้นตอนในตอนเริ่มต้น: การศึกษาโดยใช้ UML

การปรับเปลี่ยนรหัสที่รวมส่วนที่พบบ่อยทั้งหมดเข้าด้วยกันนั้นไม่ใช่วิธีที่ดีที่สุดเสมอไปฟังดูเหมือนเป็นทางออกชั่วคราวมากกว่าวิธีที่ดี

วาดแบบแผน UML ทำให้สิ่งต่าง ๆ เรียบง่าย แต่มีประสิทธิภาพโปรดระลึกถึงแนวคิดพื้นฐานบางอย่างเกี่ยวกับโครงการของคุณเช่น "สิ่งที่ควรทำกับซอฟต์แวร์นี้" "อะไรคือวิธีที่ดีที่สุดในการเก็บชิ้นส่วนของนามธรรมซอฟต์แวร์, โมดูลาร์, ส่วนขยาย, ... ฯลฯ ฯลฯ " "ฉันจะใช้การห่อหุ้มที่ดีที่สุดได้อย่างไร"

ฉันแค่พูดแบบนี้: อย่าสนใจรหัสตอนนี้คุณแค่ต้องใส่ใจเรื่องตรรกะเมื่อคุณมีเหตุผลที่ชัดเจนในใจส่วนที่เหลือทั้งหมดจะกลายเป็นงานง่าย ๆ ในท้ายที่สุดทุกชนิดนี้ ปัญหาที่คุณเผชิญนั้นเกิดจากตรรกะที่ไม่ดี


นี่ควรเป็นขั้นตอนแรกก่อนที่จะทำการปรับโครงสร้างใด ๆ จนกว่ารหัสจะเข้าใจได้มากพอที่จะถูกแมป (uml หรือแผนที่อื่น ๆ ของ wilds) การปรับโครงสร้างจะสร้างสถาปัตยกรรมในที่มืด
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
}

จากที่นี่ไปมีหลายวิธีที่เป็นไปได้ ทำในสิ่งที่ฉันทำ: สร้างคลาสพื้นฐานของคลาสของคุณและ ExecJob เสมือนจริงและกลายเป็นเรื่องง่ายในการสร้าง B, C, ... โดยไม่ต้องคัดลอกมากเกินไป - เพียงแค่แทนที่ ExecJob (ตอนนี้ห้าซับ) ด้วยการแก้ไข รุ่น

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

แต่ทำไมมีคลาสจำนวนมากเลย? ExecJobบางทีคุณอาจจะแทนที่พวกเขาทั้งหมดที่มีชั้นเดียวที่มีคอนสตรัคซึ่งสามารถบอกที่อยู่ในการดำเนินการที่จำเป็น


2

ฉันเห็นด้วยกับคำตอบอื่น ๆ ที่แนวทางของคุณอยู่ในแนวที่ถูกต้องแม้ว่าฉันจะไม่คิดว่าการสืบทอดเป็นวิธีที่ดีที่สุดในการติดตั้งโค้ดทั่วไป - ฉันชอบการแต่งเพลงมากกว่า จากคำถามที่พบบ่อย C ++ ซึ่งสามารถอธิบายได้ดีกว่าที่ฉันเคยทำได้: http://www.parashift.com/c++-faq/priv-inherit-vs-compos.html


1

อันดับแรกคุณควรตรวจสอบให้แน่ใจว่าการสืบทอดเป็นเครื่องมือที่เหมาะสมสำหรับงานนี้เท่านั้นเพราะคุณต้องการสถานที่ทั่วไปสำหรับฟังก์ชั่นที่ใช้โดยคลาสของคุณAเพื่อFไม่ได้หมายความว่าคลาสพื้นฐานทั่วไปเป็นสิ่งที่ถูกต้องที่นี่ ชั้นเรียนทำงานได้ดีขึ้น มันอาจจะเป็นมันอาจจะไม่ ขึ้นอยู่กับการมีความสัมพันธ์ "is-a" ระหว่าง A ถึง F และคลาสพื้นฐานทั่วไปของคุณซึ่งเป็นไปไม่ได้ที่จะพูดจากชื่อเทียม 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.