การเพิ่มความซับซ้อนในการลบรหัสที่ซ้ำกัน


24

ฉันมีหลายคลาสที่สืบทอดมาจากคลาสพื้นฐานทั่วไป Tชั้นฐานมีการเก็บรวบรวมวัตถุหลายของชนิด

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

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

ฉันเริ่มคิดว่าหลักการ DRY ไม่ได้นำไปใช้มากในสถานการณ์แบบนี้ แต่ฟังดูเหมือนเป็นการดูหมิ่น ความซับซ้อนเท่าไหร่เมื่อพยายามลบการทำสำเนารหัสมากเกินไป?

แก้ไข:

ทางออกที่ดีที่สุดที่ฉันสามารถทำได้คือ:

ชั้นฐาน:

protected T GetInterpolated(int frame)
{
    var index = SortedFrames.BinarySearch(frame);
    if (index >= 0)
        return Data[index];

    index = ~index;

    if (index == 0)
        return Data[index];
    if (index >= Data.Count)
        return Data[Data.Count - 1];

    return GetInterpolatedItem(frame, Data[index - 1], Data[index]);
}

protected abstract T GetInterpolatedItem(int frame, T lower, T upper);

ชั้นเรียนเด็ก A:

public IGpsCoordinate GetInterpolatedCoord(int frame)
{
    ReadData();
    return GetInterpolated(frame);
}

protected override IGpsCoordinate GetInterpolatedItem(int frame, IGpsCoordinate lower, IGpsCoordinate upper)
{
    double ratio = GetInterpolationRatio(frame, lower.Frame, upper.Frame);

    var x = GetInterpolatedValue(lower.X, upper.X, ratio);
    var y = GetInterpolatedValue(lower.Y, upper.Y, ratio);
    var z = GetInterpolatedValue(lower.Z, upper.Z, ratio);

    return new GpsCoordinate(frame, x, y, z);
}

เด็กคลาส B:

public double GetMph(int frame)
{
    ReadData();
    return GetInterpolated(frame).MilesPerHour;
}

protected override ISpeed GetInterpolatedItem(int frame, ISpeed lower, ISpeed upper)
{
    var ratio = GetInterpolationRatio(frame, lower.Frame, upper.Frame);
    var mph = GetInterpolatedValue(lower.MilesPerHour, upper.MilesPerHour, ratio);
    return new Speed(frame, mph);
}

9
การใช้แนวคิดแบบไมโครมากเกินไปเช่น DRY และ Code Reuse นำไปสู่ความบาปที่มากขึ้น
Affe

1
คุณได้รับคำตอบทั่วไปที่ดี การแก้ไขเพื่อรวมฟังก์ชั่นตัวอย่างอาจช่วยให้เราพิจารณาได้ว่าคุณใช้ฟังก์ชันนี้มากเกินไปในอินสแตนซ์นี้หรือไม่
Karl Bielefeldt

นี้ไม่ได้จริงๆคำตอบมากขึ้นของการสังเกต: ถ้าคุณไม่สามารถอธิบายสิ่งที่ชั้นฐานเอาเรื่องเอาไม่ก็อาจจะไม่ดีที่สุดที่จะมีหนึ่ง อีกวิธีในการดูคือ (ฉันคิดว่าคุณคุ้นเคยกับ SOLID หรือไม่) ผู้บริโภคที่มีแนวโน้มของฟังก์ชันนี้ต้องการการทดแทน Liskov หรือไม่? หากไม่มีกรณีธุรกิจสำหรับผู้บริโภคทั่วไปที่มีฟังก์ชั่นการแก้ไขระดับพื้นฐานไม่มีค่า
Tom W

1
สิ่งแรกคือการรวบรวม triplet X, Y, Z เป็นประเภทตำแหน่งและเพิ่มการแก้ไขประเภทนั้นในฐานะสมาชิกหรืออาจเป็นวิธีการคงที่: ตำแหน่งการแก้ไข (ตำแหน่งอื่น ๆ อัตราส่วน)
วินไคลน์

คำตอบ:


30

คุณตอบคำถามของคุณเองด้วยคำพูดนั้นในย่อหน้าสุดท้าย:

ผมเริ่มคิดว่าหลักการ DRY ใช้ไม่ได้เป็นอย่างมากในสถานการณ์แบบนี้ แต่เสียงที่เหมือนดูหมิ่น

เมื่อใดก็ตามที่คุณพบว่ามีการฝึกฝนที่ไม่ได้ผลในการแก้ปัญหาของคุณอย่าพยายามใช้การฝึกฝนนั้นอย่างเคร่งศาสนา (คำดูหมิ่นคำเป็นการเตือนสำหรับเรื่องนี้) การปฏิบัติส่วนใหญ่มีwhensและwhysของพวกเขาและแม้ว่าพวกเขาครอบคลุม 99% ของกรณีที่เป็นไปได้ทั้งหมดยังคงมี 1% ที่คุณอาจต้องการแนวทางที่แตกต่างกัน

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

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

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


10
อีกประเด็นเกี่ยวกับความคล้ายกัน แต่แตกต่างกันเล็กน้อยคือแม้ว่าพวกเขาจะดูคล้ายกันในรหัส แต่ก็ไม่ได้หมายความว่าพวกเขาจะต้องเหมือนกันใน "ธุรกิจ" ขึ้นอยู่กับว่ามันคืออะไร แต่บางครั้งมันก็เป็นความคิดที่ดีที่จะแยกสิ่งต่าง ๆ ออกมาเพราะแม้ว่าพวกเขาจะดูเหมือนกันพวกเขาอาจจะขึ้นอยู่กับการตัดสินใจ / ความต้องการทางธุรกิจที่แตกต่างกัน ดังนั้นคุณอาจต้องการดูพวกเขาว่าการคำนวณที่แตกต่างกันอย่างมากมายแม้ว่าพวกเขารหัสฉลาดอาจมีลักษณะที่คล้ายกัน (ไม่ได้เป็นกฎหรืออะไร แต่เพียงสิ่งที่จะเก็บไว้ในใจเมื่อตัดสินใจถ้าสิ่งที่ควรจะรวมหรือ refactored :)
Svish

@Vishish จุดที่น่าสนใจ ไม่เคยคิดอย่างนั้น
ฟิล

17

คลาสเด็กใช้ชนิดต่าง ๆ การคำนวณแตกต่างกันเล็กน้อยจากคลาสหนึ่งไปอีกชั้นหนึ่ง

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

ทำเช่นนั้นเป็นการออกกำลังกายครั้งแรกก่อนที่จะเริ่มการแฟ ทุกอย่างอื่นจะเข้าที่โดยอัตโนมัติหลังจากนั้น


2
ใส่กัน นี่อาจเป็นปัญหาของฟังก์ชั่นซ้ำ ๆ ที่มีขนาดใหญ่เกินไป
Karl Bielefeldt

8

ฉันเชื่อว่าการทำซ้ำโค้ดมากกว่าสองบรรทัดเกือบทั้งหมดสามารถนำมาแยกเป็นส่วนหนึ่งได้ไม่ทางใดก็ทางหนึ่งและเกือบจะทุกครั้ง

อย่างไรก็ตามการรีแฟคเตอร์นี้ทำได้ง่ายกว่าในบางภาษากว่าภาษาอื่น มันค่อนข้างง่ายในภาษาเช่น LISP, Ruby, Python, Groovy, Javascript, Lua และอื่น ๆ โดยทั่วไปแล้ว C ++ จะใช้เทมเพลตไม่ยากเกินไป เจ็บปวดมากขึ้นใน C ซึ่งเครื่องมือเพียงอย่างเดียวอาจเป็นมาโครโปรเซสเซอร์ก่อน มักจะเจ็บปวดใน Java และบางครั้งก็เป็นไปไม่ได้เช่นพยายามเขียนโค้ดทั่วไปเพื่อจัดการกับชนิดตัวเลขที่มีอยู่ภายในหลายชนิด

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

ฉันจะยอมรับโค้ดซ้ำได้ก็ต่อเมื่อมันสั้นเสถียรและ refactoring นั้นน่าเกลียดเกินไป โดยทั่วไปฉันแยกตัวประกอบการทำซ้ำเกือบทั้งหมดเว้นแต่ฉันจะเขียน Java

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


Lua ไม่ใช่ตัวย่อ
DeadMG

@DeadMG: บันทึกไว้; อย่าลังเลที่จะแก้ไข นั่นเป็นเหตุผลที่เราให้คุณทุกชื่อเสียงที่
วินไคลน์

5

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

ด้วยสิ่งนี้คลาสฐานจะทำการอัลกอริทึมและเมื่อมันมาถึงการเปลี่ยนแปลงสำหรับแต่ละคลาสย่อยมันจะเปลี่ยนไปเป็นวิธีนามธรรมซึ่งเป็นความรับผิดชอบของคลาสย่อยที่จะนำไปใช้ ลองนึกถึงวิธีการและเพจ ASP.NET จะใช้รหัสของคุณในการใช้ Page_Load ตัวอย่างเช่น


4

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

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

Public Class Fraction
{
     public virtual Decimal GetNumerator(params?)
     public virtual Decimal GetDenominator(params?)
     //Some concrete method to actually compute GetNumerator / GetDenominator
}

และแทนที่ชิ้นส่วนแต่ละชิ้นเมื่อคุณต้องการทำให้ "การเปลี่ยนแปลงเล็กน้อย" ในตรรกะของการคำนวณ

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


3

ในความคิดของฉันคุณถูกต้องในแง่หนึ่งว่า DRY อาจถูกนำมาใช้จนเกินไป หากโค้ดสองชิ้นที่คล้ายกันมีแนวโน้มที่จะพัฒนาไปในทิศทางที่แตกต่างกันมากคุณสามารถทำให้เกิดปัญหาด้วยตัวคุณเองโดยพยายามที่จะไม่ทำซ้ำตัวเองในตอนแรก

อย่างไรก็ตามคุณก็ค่อนข้างถูกที่จะระวังความคิดดูหมิ่นเช่นนั้น พยายามอย่างหนักที่จะคิดผ่านตัวเลือกของคุณก่อนที่จะตัดสินใจทิ้งมันไว้คนเดียว

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


2

DRY เป็นแนวทางในการปฏิบัติตามไม่ใช่กฎที่ไม่แตก ในบางจุดคุณต้องตัดสินใจว่ามันไม่คุ้มค่าที่จะมีระดับ X ของการสืบทอดและเท็มเพลต Y ในทุกคลาสที่คุณใช้เพียงเพื่อบอกว่าไม่มีการซ้ำซ้อนในรหัสนี้ สองสามคำถามที่ดีที่จะถามก็คือฉันจะใช้เวลานานกว่านี้ในการแยกวิธีการที่คล้ายกันเหล่านี้และนำมาใช้เป็นหนึ่งแล้วจะทำการค้นหาผ่านพวกเขาทั้งหมดหากจำเป็นต้องเปลี่ยนเกิดขึ้นหรือมีศักยภาพสำหรับการเปลี่ยนแปลงที่จะเกิดขึ้น เลิกทำงานของฉันแยกวิธีการเหล่านี้ในสถานที่แรก? ฉันมาถึงจุดที่สิ่งที่เป็นนามธรรมเพิ่มเติมเริ่มที่จะเข้าใจหรือไม่ว่ารหัสนี้คือความท้าทาย?

หากคุณสามารถตอบว่าใช่สำหรับคำถามใดคำถามหนึ่งคุณก็มีเหตุผลที่ชัดเจนในการทิ้งโค้ดที่ซ้ำ


0

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

ในกรณีนี้การนำอัลกอริทึมกลับมาใช้ใหม่จะทำให้มันซับซ้อนเกินไปทำให้ยากสำหรับการบำรุงรักษาในอนาคต ดังนั้นหากคุณไม่สามารถแยกแยะสิ่งที่เป็นเรื่องธรรมดาได้ง่าย ๆ :

// this code is similar to class x function b

ความคิดเห็นจะเพียงพอ แก้ไขปัญหา.


0

ในการตัดสินใจว่าจะมีวิธีที่ใหญ่กว่าหนึ่งหรือสองวิธีที่เล็กกว่าพร้อมกับฟังก์ชันการซ้อนทับดีกว่าคำถาม $ 50,000 คำถามแรกคือว่าส่วนที่ทับซ้อนกันของพฤติกรรมอาจเปลี่ยนไปหรือไม่และการเปลี่ยนแปลงใด ๆ ถ้าคำตอบของคำถามแรกคือใช่ แต่คำตอบของคำถามที่สองคือไม่มีแล้ววิธีการที่ควรจะอยู่แยกต่างหาก หากคำตอบของทั้งสองคำถามคือใช่คุณต้องทำบางสิ่งเพื่อให้แน่ใจว่ารหัสทุกรุ่นยังคงซิงค์อยู่ ในหลายกรณีวิธีที่ง่ายที่สุดที่จะทำคือมีเพียงเวอร์ชันเดียวเท่านั้น

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

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