มันเป็นกลิ่นรหัสที่จะตั้งค่าสถานะในวงเพื่อใช้ในภายหลัง?


30

ฉันมีรหัสบางส่วนที่ฉันทำซ้ำแผนที่จนกว่าเงื่อนไขบางอย่างจะเป็นจริงและต่อมาก็ใช้เงื่อนไขนั้นเพื่อทำบางสิ่งเพิ่มเติม

ตัวอย่าง:

Map<BigInteger, List<String>> map = handler.getMap();

if(map != null && !map.isEmpty())
{
    for (Map.Entry<BigInteger, List<String>> entry : map.entrySet())
    {
        fillUpList();

        if(list.size() > limit)
        {
            limitFlag = true;
            break;
        }
    }
}
else
{
    logger.info("\n>>>>> \n\t 6.1 NO entries to iterate over (for given FC and target) \n");
}

if(!limitFlag) // Continue only if limitFlag is not set
{
    // Do something
}

ฉันรู้สึกว่าการตั้งค่าสถานะแล้วใช้สิ่งนั้นเพื่อทำสิ่งต่าง ๆ มากขึ้นคือกลิ่นรหัส

ฉันถูกไหม? ฉันจะลบสิ่งนี้ได้อย่างไร


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

13
@ gnasher729 แค่อยากรู้อยากเห็นคุณจะใช้คำใดแทน
Ben Cottrell

11
-1 ตัวอย่างของคุณไม่สมเหตุสมผล entryไม่มีการใช้ภายในลูปฟังก์ชันและเราสามารถเดาได้ว่าอะไรlistคือ คือfillUpListควรจะเติมlist? ทำไมมันไม่ได้เป็นพารามิเตอร์?
Doc Brown

13
ฉันจะพิจารณาการใช้ช่องว่างและบรรทัดว่างของคุณอีกครั้ง
Daniel Jour

11
ไม่มีสิ่งเช่นกลิ่นรหัส "Code ดมกลิ่น" เป็นคำที่คิดค้นโดยนักพัฒนาซอฟต์แวร์ที่ต้องการกลั้นเมื่อพวกเขาเห็นรหัสที่ไม่ตรงตามมาตรฐานชั้นนำ
Robert Harvey

คำตอบ:


70

ไม่มีอะไรผิดปกติกับการใช้ค่าบูลีนเพื่อจุดประสงค์: เพื่อบันทึกความแตกต่างแบบไบนารี

ถ้าฉันได้รับคำสั่งให้ refactor รหัสนี้ฉันอาจจะใส่ loop เข้าไปในวิธีการของมันเองเพื่อให้การมอบหมาย + breakกลายเป็น a return; จากนั้นคุณไม่จำเป็นต้องมีตัวแปรคุณก็สามารถพูดได้

if(fill_list_from_map()) {
  ...

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

2
วลีที่ดีกว่าที่อธิบายถึงฟังก์ชั่นที่มีประโยชน์ของส่วนแรกของรหัสนั้นคือการค้นหาว่าเกินขีด จำกัด หลังจากที่รวบรวมบางสิ่งจากรายการที่แมปหรือไม่ เราสามารถสันนิษฐานได้ว่าfillUpList()เป็นรหัส (OP ตัดสินใจไม่ให้แชร์) ซึ่งใช้ค่าentryจากการวนซ้ำจริง ๆ หากไม่มีข้อสันนิษฐานนี้ดูเหมือนว่าตัวลูปไม่ได้ใช้อะไรเลยจากการวนซ้ำ
วงเวียน

4
@ Kilian: ฉันมีเพียงหนึ่งความกังวล วิธีนี้จะเติมรายการและจะส่งคืน Boolean ซึ่งแสดงว่าขนาดรายการเกินขีด จำกัด หรือไม่ดังนั้นชื่อ 'fill_list_from_map' จึงไม่ทำให้ชัดเจนว่า Boolean ที่ส่งคืนนั้นแทนอะไร (ไม่สามารถเติมเต็มได้ เกินขีด จำกัด ฯลฯ ) เนื่องจากบูลีนที่ส่งคืนมีไว้สำหรับกรณีพิเศษซึ่งไม่ชัดเจนจากชื่อฟังก์ชัน มีคำแนะนำอะไรมั้ย ? PS: เราสามารถนำการแยกแบบสอบถามคำสั่งมาพิจารณาด้วย
Siddharth Trikha

2
@SiddharthTrikha คุณพูดถูกและฉันก็มีข้อกังวลเหมือนกันเมื่อฉันแนะนำบรรทัดนั้น แต่มันก็ไม่มีความชัดเจนว่ารายการรหัสควรจะเติม หากเป็นรายการเดียวกันเสมอคุณไม่จำเป็นต้องตั้งค่าสถานะคุณสามารถตรวจสอบความยาวได้หลังจากนั้น หากคุณจำเป็นต้องรู้ว่าบุคคลใดเติมเต็มเกินขีด จำกัด แล้วคุณจะต้องขนส่งข้อมูลภายนอกอย่างใดและ IMO หลักการแยกคำสั่ง / แบบสอบถามไม่ได้เป็นเหตุผลเพียงพอที่จะปฏิเสธวิธีที่ชัดเจน: ผ่านการส่งคืน ราคา.
Kilian Foth

6
ลุงบ๊อบพูดในหน้า 45 ของรหัสสะอาด : "ฟังก์ชั่นควรทำอะไรหรือตอบบางอย่าง แต่ไม่ใช่ทั้งสองอย่างฟังก์ชั่นของคุณควรเปลี่ยนสถานะของวัตถุหรือควรคืนข้อมูลบางอย่างเกี่ยวกับวัตถุนั้น ความสับสน."
CJ Dennis

25

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

ปัญหาคือคุณมีบล็อกเพื่อกำหนดขอบเขต แต่จากนั้นคุณมีแฟล็กที่สื่อสารข้ามขอบเขตทำลายการแยกเชิงตรรกะของบล็อก ยกตัวอย่างเช่นlimitFlagจะเป็นเท็จถ้าmapเป็นnullดังนั้น "ทำอะไร" -code จะถูกดำเนินการถ้ามีmap nullนี่อาจเป็นสิ่งที่คุณตั้งใจ แต่อาจเป็นข้อผิดพลาดที่พลาดง่ายเนื่องจากเงื่อนไขสำหรับการตั้งค่าสถานะนี้ถูกกำหนดไว้ที่อื่นภายในขอบเขตที่ซ้อนกัน หากคุณสามารถเก็บข้อมูลและตรรกะไว้ในขอบเขตที่แคบที่สุดเท่าที่จะเป็นไปได้คุณควรพยายามทำเช่นนั้น


2
นี่คือเหตุผลที่ฉันรู้สึกว่ามันเป็นกลิ่นรหัสเนื่องจากบล็อกไม่ได้ถูกแยกอย่างสมบูรณ์และอาจติดตามได้ยากในภายหลัง ดังนั้นฉันเดารหัสในคำตอบของ @Kilian ได้มากที่สุด
Siddharth Trikha

1
@SiddharthTrikha: มันยากที่จะพูดเนื่องจากฉันไม่รู้ว่ารหัสควรทำอะไร หากคุณต้องการตรวจสอบว่าแผนที่มีรายการอย่างน้อยหนึ่งรายการที่มีขนาดใหญ่กว่าขีด จำกัด หรือไม่ฉันคิดว่าคุณสามารถทำได้ด้วยนิพจน์ใด ๆ แมตช์ที่ตรงกัน
JacquesB

2
@SiddharthTrikha: ปัญหาขอบเขตสามารถแก้ไขได้อย่างง่ายดายโดยการเปลี่ยนการทดสอบเริ่มต้นเป็นประโยคคำสั่งเช่นif(map==null || map.isEmpty()) { logger.info(); return;}นี้จะทำงานเฉพาะในกรณีที่รหัสที่เราเห็นเป็นฟังก์ชันแบบเต็มและ// Do somethingไม่จำเป็นต้องใช้ส่วนใด ๆ ในกรณีที่แผนที่ ว่างเปล่าหรือว่างเปล่า
Doc Brown

14

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

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

"กลิ่นของรหัส" มีไว้สำหรับเมื่อคุณไม่ได้คิด หากคุณจะคิดถึงรหัสจริงๆให้ทำถูกต้อง!

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

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

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


5

ใช่มันเป็นกลิ่นรหัส (คิว downvotes จากทุกคนที่ทำมัน)

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

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

เมื่อรหัสนั้นง่ายอย่างที่เป็นตัวอย่างของคุณมันสามารถลดให้เป็นwhileลูปหรือแผนที่ที่เทียบเท่าโครงสร้างตัวกรอง

เมื่อรหัสมีความซับซ้อนเพียงพอที่จะต้องมีการตั้งค่าสถานะและแบ่งมันจะมีแนวโน้มที่จะเกิดข้อบกพร่อง

ดังนั้นในขณะที่มีรหัสทุกกลิ่น: whileถ้าคุณเห็นธงพยายามที่จะแทนที่ด้วย หากคุณทำไม่ได้ให้เพิ่มการทดสอบหน่วยเพิ่มเติม


+1 จากฉัน มันเป็นกลิ่นของรหัสอย่างแน่นอนและคุณจะอธิบายว่าทำไมและวิธีจัดการกับมันด้วย
David Arno

@Ewan: SO as with all code smells: If you see a flag, try to replace it with a whileคุณช่วยอธิบายตัวอย่างนี้ได้ไหม?
Siddharth Trikha

2
มีจุดออกจากหลายวงที่อาจทำให้มันยากที่จะเหตุผลเกี่ยวกับ แต่ในกรณีนี้นั้นจะ refactoring มันเพื่อให้สภาพ loop ขึ้นอยู่กับธง - มันจะหมายถึงการเปลี่ยนด้วยfor (Map.Entry<BigInteger, List<String>> entry : map.entrySet()) for (Iterator<Map.Entry<BigInteger, List<String>>> iterator = map.entrySet().iterator(); iterator.hasNext() && !limitFlag; Map.Entry<BigInteger, List<String>> entry = iterator.next())นั่นเป็นรูปแบบที่ผิดปกติมากพอที่ฉันจะมีปัญหาในการเข้าใจมันมากกว่าการพักแบบง่ายๆ
James_pic

@ James_pic java ของฉันค่อนข้างสนิม แต่ถ้าเราใช้แผนที่ฉันจะใช้ตัวสะสมเพื่อสรุปจำนวนรายการและกรองสิ่งเหล่านั้นหลังจากถึงขีด จำกัด อย่างไรก็ตามอย่างที่ฉันบอกว่าตัวอย่างคือ "ไม่เลว" กลิ่นรหัสเป็นกฎทั่วไปที่เตือนคุณถึงปัญหาที่อาจเกิดขึ้น ไม่ใช่กฎหมายศักดิ์สิทธิ์คุณต้องเชื่อฟังเสมอ
Ewan

1
คุณไม่ได้หมายถึง "คิว" มากกว่า "คิว" ใช่ไหม
psmears

0

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


0

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

คุณควรย้ายโลจิคัลวนลูปสำหรับลงในเมธอด fillUpList เพื่อให้มันแตกถ้าถึงขีด จำกัด จากนั้นตรวจสอบขนาดของรายการโดยตรงหลังจากนั้น

หากรหัสของคุณถูกทำลายทำไม?


0

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

private <T> boolean checkCollection(Collection<T> collection)
{
    for (T element : collection)
        if (checkElement(element))
            return true;
    return false;
}

ตั้งแต่ Java 8 มีวิธีการใช้ที่กระชับกว่าStream.anyMatch(…):

collection.stream().anyMatch(this::checkElement);

ในกรณีของคุณอาจมีลักษณะเช่นนี้ (สมมติlist == entry.getValue()ในคำถามของคุณ):

map.values().stream().anyMatch(list -> list.size() > limit);

fillUpList()ปัญหาในตัวอย่างเฉพาะของคุณคือการเรียกเพิ่มเติมเพื่อ คำตอบนั้นขึ้นอยู่กับว่าวิธีนี้ควรทำอย่างไร

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

ดังนั้นฉันคิดว่ารหัสจริงผ่านกระแสentryไปยังวิธีการ

แต่มีคำถามเพิ่มเติมที่จะถาม:

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

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

อาจหมายถึงสิ่งนี้ ("all or nothing" และถึงขีด จำกัด หมายถึงข้อผิดพลาด):

/**
 * Computes the list of all foo strings for each passed number.
 * 
 * @param numbers the numbers to process. Must not be {@code null}.
 * @return all foo strings for each passed number. Never {@code null}.
 * @throws InvalidArgumentException if any number produces a list that is too long.
 */
public Map<BigInteger, List<String>> computeFoos(Set<BigInteger> numbers)
        throws InvalidArgumentException
{
    if (numbers.isEmpty())
    {
        // Do you actually need to log this here?
        // The caller might know better what to do in this case...
        logger.info("Nothing to compute");
    }
    return numbers.stream().collect(Collectors.toMap(
            number -> number,
            number -> computeListForNumber(number)));
}

private List<String> computeListForNumber(BigInteger number)
        throws InvalidArgumentException
{
    // compute the list and throw an exception if the limit is exceeded.
}

หรืออาจหมายถึงสิ่งนี้ ("อัปเดตจนถึงปัญหาแรก"):

/**
 * Refreshes all foo lists after they have become invalid because of bar.
 * 
 * @param map the numbers with all their current values.
 *            The values in this map will be modified.
 *            Must not be {@code null}.
 * @throws InvalidArgumentException if any new foo list would become too long.
 *             Some other lists may have already been updated.
 */
public void updateFoos(Map<BigInteger, List<String>> map)
        throws InvalidArgumentException
{
    map.replaceAll(this::computeUpdatedList);
}

private List<String> computeUpdatedList(
        BigInteger number, List<String> currentValues)
        throws InvalidArgumentException
{
    // compute the new list and throw an exception if the limit is exceeded.
}

หรือนี่ ("อัปเดตรายการทั้งหมด แต่เก็บรายการดั้งเดิมหากรายการใหญ่เกินไป")

/**
 * Refreshes all foo lists after they have become invalid because of bar.
 * Lists that would become too large will not be updated.
 * 
 * @param map the numbers with all their current values.
 *            The values in this map will be modified.
 *            Must not be {@code null}.
 * @return {@code true} if all updates have been successful,
 *         {@code false} if one or more elements have been skipped
 *         because the foo list size limit has been reached.
 */
public boolean updateFoos(Map<BigInteger, List<String>> map)
{
    boolean allUpdatesSuccessful = true;
    for (Entry<BigInteger, List<String>> entry : map.entrySet())
    {
        List<String> newList = computeListForNumber(entry.getKey());
        if (newList.size() > limit)
            allUpdatesSuccessful = false;
        else
            entry.setValue(newList);
    }
    return allUpdatesSuccessful;
}

private List<String> computeListForNumber(BigInteger number)
{
    // compute the new list
}

หรือแม้แต่ต่อไปนี้ (ใช้computeFoos(…)จากตัวอย่างแรก แต่ไม่มีข้อยกเว้น):

/**
 * Processes the passed numbers. An optimized algorithm will be used if any number
 * produces a foo list of a size that justifies the additional overhead.
 * 
 * @param numbers the numbers to process. Must not be {@code null}.
 */
public void process(Collection<BigInteger> numbers)
{
    Map<BigInteger, List<String>> map = computeFoos(numbers);
    if (isLimitReached(map))
        processLarge(map);
    else
        processSmall(map);
}

private boolean isLimitReached(Map<BigInteger, List<String>> map)
{
    return map.values().stream().anyMatch(list -> list.size() > limit);
}

หรืออาจหมายถึงบางสิ่งที่แตกต่างอย่างสิ้นเชิง… ;-)

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