วิธี refactor แอปพลิเคชั่นที่มีสวิตช์หลายตัว?


10

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

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

คุณคิดอย่างไร?


2
ปัญหาที่เกิดขึ้นจริงกับรหัสปัจจุบันคืออะไร?
Philip Kendall

จะเกิดอะไรขึ้นเมื่อคุณต้องทำการเปลี่ยนแปลงอย่างใดอย่างหนึ่งเหล่านี้ คุณต้องเพิ่มswitchเคสและเรียกใช้เมธอดที่มีอยู่แล้วในระบบที่ซับซ้อนของคุณหรือคุณต้องประดิษฐ์ทั้งเมธอดและการเรียกใช้หรือไม่?
Kilian Foth

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

2
ฉันคิดว่าคุณต้องแสดงตัวอย่างย่อของสิ่งที่เกิดขึ้น
whatsisname

1
@KaushikChakraborty: จากนั้นทำตัวอย่างจากหน่วยความจำ มีหลายกรณีที่มี uber-switch กว่า 250+ เคสที่เหมาะสมและมี case ที่ switch ไม่ดีไม่ว่าจะมีกี่กรณีก็ตาม ปีศาจอยู่ในรายละเอียดและเราไม่มีรายละเอียด
whatsisname

คำตอบ:


13

ตอนนี้มีสวิตช์อยู่ 50 กรณีและทุกครั้งที่ฉันต้องเพิ่มเคสฉันต้องสั่น

ฉันรักความแตกต่าง ฉันรักโซลิด ฉันรักการเขียนโปรแกรมเชิงวัตถุบริสุทธิ์ ฉันเกลียดที่จะเห็นสิ่งเหล่านี้ได้รับตัวแทนที่ไม่ดีเพราะพวกเขาได้รับการประยุกต์ใช้ดันทุรัง

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

ฉันพบคำแนะนำที่ตรงประเด็นสำหรับคุณจากc2.com :

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

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

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

สิ่งที่ฉันแนะนำคือการมองอย่างใกล้ชิดใน 50 กรณีเหล่านี้สำหรับคนธรรมดาสามัญที่สามารถแยกตัวออกได้ ฉันหมายถึง 50 จริงๆ? คุณแน่ใจหรือว่าต้องการหลายกรณี? คุณอาจพยายามทำอะไรมากมายที่นี่


ฉันเห็นด้วยกับสิ่งที่คุณพูด รหัสมีความซ้ำซ้อนมากมายอาจเป็นไปได้ว่ามีหลายกรณีที่ไม่จำเป็นแม้กระทั่งจากการมองคร่าวๆคร่าวๆดูเหมือนจะไม่เป็นเช่นนั้น แต่ละกรณีเรียกวิธีการที่เรียกหลายระบบและรวมผลลัพธ์และกลับไปที่รหัสการโทร ทุกชั้นมีการบรรจุตัวเองทำงานเดียวและฉันกลัวว่าฉันจะละเมิดหลักการทำงานร่วมกันสูงฉันจะลดจำนวนผู้ป่วย
Kaushik Chakraborty

2
ฉันสามารถรับ 50 โดยไม่ละเมิดการติดต่อกันสูงและเก็บสิ่งต่าง ๆ ในตัวเอง ฉันไม่สามารถทำได้ด้วยหมายเลขเดียว ฉันต้องการ 2, 5 และอีก 5 นั่นคือสาเหตุที่เรียกว่าแฟคตอริ่ง อย่างจริงจังดูที่สถาปัตยกรรมทั้งหมดของคุณและดูว่าคุณไม่สามารถหาทางออกจากกรณีนี้ 50 กรณีที่คุณเข้ามา Refactoring เป็นเรื่องเกี่ยวกับการยกเลิกการตัดสินใจที่ไม่ดี ไม่ทำให้เป็นอมตะในรูปแบบใหม่
candied_orange

ทีนี้ถ้าคุณสามารถเห็นวิธีลด 50 โดยใช้การปรับสภาพนี้ หากต้องการใช้ประโยชน์จากแนวคิด Doc Browns: แผนที่ของแผนที่สามารถใช้สองปุ่ม สิ่งที่ต้องคิด
candied_orange

1
ฉันเห็นด้วยกับความคิดเห็นของ Candied ปัญหาไม่ใช่กรณี 50 คำสั่งในสวิทช์ปัญหาคือการออกแบบสถาปัตยกรรมระดับสูงกว่าที่ทำให้คุณเรียกใช้ฟังก์ชันที่ต้องตัดสินใจระหว่าง 50 ตัวเลือก ฉันได้ออกแบบระบบที่มีขนาดใหญ่และซับซ้อนบางอย่างและไม่เคยถูกบังคับให้อยู่ในสถานการณ์เช่นนั้น
Dunk

@Candied "คุณใช้ refactoring นี้เมื่อคุณพบว่าคุณต้องการสร้าง switch statement อื่นในอินพุตเดียวกันที่อื่น" คุณช่วยอธิบายเรื่องนี้ได้อย่างไรเพราะฉันมีกรณีที่คล้ายกันที่ฉันมีกรณีสวิตช์ แต่ในเลเยอร์ต่าง ๆ เหมือนที่เรามี อนุมัติโครงการแรก, การตรวจสอบ, ขั้นตอน CRUD แล้ว dao ดังนั้นในทุกๆเลเยอร์จะมีกรณีสวิตช์บนอินพุตเดียวกันนั่นคือจำนวนเต็ม แต่ทำหน้าที่แตกต่างกันเช่นรับรองความถูกต้อง ดังนั้นเราควรสร้างชั้นเรียนหนึ่งประเภทซึ่งมีวิธีการที่แตกต่างกันหรือไม่ กรณีของเราเหมาะสมกับสิ่งที่คุณพยายามจะพูดโดย "ทำซ้ำสวิตช์เดียวกันกับอินพุตเดียวกัน" หรือไม่?
Siddharth Trikha

9

แผนที่วัตถุทางกลยุทธ์เพียงอย่างเดียวซึ่งเริ่มต้นได้ในฟังก์ชั่นบางอย่างของโค้ดของคุณซึ่งคุณมีโค้ดหลายบรรทัด

     myMap.Add(1,new Strategy1());
     myMap.Add(2,new Strategy2());
     myMap.Add(3,new Strategy3());

คุณและเพื่อนร่วมงานของคุณต้องใช้ฟังก์ชั่น / กลยุทธ์ที่จะถูกเรียกในชั้นเรียนแยกกันอย่างสม่ำเสมอ (เนื่องจากวัตถุกลยุทธ์ของคุณจะต้องใช้อินเทอร์เฟซเดียวกันทั้งหมด) รหัสดังกล่าวมักจะครอบคลุมมากกว่าเล็กน้อย

     case 1:
          MyClass1.Doit1(someParameters);
          break;
     case 2:
          MyClass2.Doit2(someParameters);
          break;
     case 3:
          MyClass3.Doit3(someParameters);
          break;

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

  • การเริ่มต้นของแผนที่ในขณะนี้กลายเป็นแยกออกจากรหัสการจัดส่งซึ่งอันที่จริงเรียกฟังก์ชั่นที่เชื่อมโยงไปยังหมายเลขที่เฉพาะเจาะจงและหลังไม่ได้มีผู้ที่ 50 myMap[number].DoIt(someParameters)ซ้ำอีกมันก็จะมีลักษณะเช่น ดังนั้นรหัสการจัดส่งนี้ไม่จำเป็นต้องแตะเมื่อใดก็ตามที่มีหมายเลขใหม่มาถึงและสามารถใช้งานได้ตามหลักการ Open-Closed ยิ่งไปกว่านั้นเมื่อคุณได้รับข้อกำหนดที่คุณต้องการขยายรหัสการจัดส่งเองคุณจะไม่ต้องเปลี่ยนสถานที่ 50 แห่งอีกต่อไป แต่จะมีเพียงแห่งเดียวเท่านั้น

  • เนื้อหาของแผนที่จะถูกกำหนดในเวลาทำงาน (ในขณะที่เนื้อหาของโครงสร้างสวิตช์ถูกกำหนดก่อนที่จะคอมไพล์เวลา) ดังนั้นสิ่งนี้จะทำให้คุณมีโอกาสที่จะทำให้ตรรกะการเริ่มต้นมีความยืดหยุ่นมากขึ้นหรือขยายได้

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


ในขณะที่ฉันลังเลที่จะเปลี่ยนสวิตช์ด้วยความหลากหลายแบบสุ่มสี่สุ่มห้าฉันจะบอกว่าการใช้แผนที่วิธีที่ Doc Brown แนะนำที่นี่ทำงานได้ดีสำหรับฉันในอดีต เมื่อคุณใช้อินเตอร์เฟซเดียวกันโปรดแทนที่Doit1, Doit2ฯลฯ กับหนึ่งในDoitวิธีการที่มีการใช้งานที่แตกต่างกัน
candied_orange

และถ้าคุณมีการควบคุมประเภทของสัญลักษณ์อินพุตที่ใช้เป็นคีย์คุณสามารถไปอีกขั้นด้วยการสร้างdoTheThing()เมธอดของสัญลักษณ์อินพุต จากนั้นคุณไม่จำเป็นต้องบำรุงรักษาแผนที่
Kevin Krumwiede

1
@KevinKrumwiede: สิ่งที่คุณแนะนำหมายถึงเพียงแค่ส่งกลยุทธ์ให้ตัวเองในโปรแกรมแทนการใช้จำนวนเต็ม อย่างไรก็ตามเมื่อโปรแกรมใช้จำนวนเต็มเป็นอินพุตจากแหล่งข้อมูลภายนอกบางรายการจะต้องมีการจับคู่จากจำนวนเต็มไปยังกลยุทธ์ที่เกี่ยวข้องอย่างน้อยในที่เดียวของระบบ
Doc Brown

ขยายคำแนะนำของ Doc Brown: คุณสามารถสร้างโรงงานที่มีตรรกะในการสร้างวัตถุกลยุทธ์ได้หากคุณตัดสินใจที่จะทำเช่นนี้ ที่กล่าวว่าคำตอบที่ได้รับจาก CandiedOrange ทำให้รู้สึกถึงฉันมากที่สุด
Vladimir Stokic

@DocBrown นั่นคือสิ่งที่ฉันได้รับด้วย "ถ้าคุณสามารถควบคุมประเภทของสัญลักษณ์อินพุต"
Kevin Krumwiede

0

ฉันขอสนับสนุนกลยุทธ์ที่ระบุไว้ในคำตอบของ @DocBrown@DocBrown

ฉันจะแนะนำการปรับปรุงคำตอบ

การโทรออก

 myMap.Add(1,new Strategy1());
 myMap.Add(2,new Strategy2());
 myMap.Add(3,new Strategy3());

สามารถกระจาย คุณไม่จำเป็นต้องกลับไปที่ไฟล์เดียวกันเพื่อเพิ่มกลยุทธ์อื่นซึ่งปฏิบัติตามหลักการ Open-Closed ที่ดียิ่งขึ้น

สมมติว่าคุณใช้งานStrategy1ในไฟล์ Strategy1.cpp คุณสามารถมีบล็อคของรหัสต่อไปนี้ได้

namespace Strategy1_Impl
{
   struct Initializer
   {
      Initializer()
      {
         getMap().Add(1, new Strategy1());
      }
   };
}
using namespace Strategy1_Impl;

static Initializer initializer;

คุณสามารถทำซ้ำรหัสเดียวกันในทุกไฟล์ StategyN.cpp อย่างที่คุณเห็นนั่นจะเป็นรหัสซ้ำหลายครั้ง เพื่อลดความซ้ำซ้อนของรหัสคุณสามารถใช้เทมเพลตที่สามารถใส่ในไฟล์ที่ทุกStrategyคลาสสามารถเข้าถึงได้

namespace StrategyHelper
{
   template <int N, typename StrategyType> struct Initializer
   {
      Initializer()
      {
         getMap().Add(N, new StrategyType());
      }
   };
}

หลังจากนั้นสิ่งเดียวที่คุณต้องใช้ใน Strategy1.cpp คือ:

static StrategyHelper::Initializer<1, Strategy1> initializer;

บรรทัดที่เกี่ยวข้องใน StrategyN.cpp คือ:

static StrategyHelper::Initializer<N, StrategyN> initializer;

คุณสามารถใช้เทมเพลตไปอีกระดับโดยใช้เทมเพลตชั้นเรียนสำหรับชั้นเรียนกลยุทธ์ที่เป็นรูปธรรม

class Strategy { ... };

template <int N> class ConcreteStrategy;

และจากนั้นแทนการใช้Strategy1ConcreteStrategy<1>

template <> class ConcreteStrategy<1> : public Strategy { ... };

เปลี่ยนคลาสตัวช่วยเพื่อลงทะเบียนStrategys เป็น:

namespace StrategyHelper
{
   template <int N> struct Initializer
   {
      Initializer()
      {
         getMap().Add(N, new ConcreteStrategy<N>());
      }
   };
}

เปลี่ยนรหัสใน Strateg1.cpp เป็น:

static StrategyHelper::Initializer<1> initializer;

เปลี่ยนรหัสใน StrategN.cpp เป็น:

static StrategyHelper::Initializer<N> initializer;
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.