การเอาชนะวิธีที่เป็นรูปธรรมนั้นเป็นกลิ่นของรหัสหรือไม่?


31

มันเป็นความจริงหรือไม่ที่วิธีการที่เป็นรูปธรรมที่สำคัญคือกลิ่นรหัส? เพราะฉันคิดว่าถ้าคุณต้องการแทนที่วิธีที่เป็นรูปธรรม:

public class A{
    public void a(){
    }
}

public class B extends A{
    @Override
    public void a(){
    }
}

มันสามารถเขียนใหม่เป็น

public interface A{
    public void a();
}

public class ConcreteA implements A{
    public void a();
}

public class B implements A{
    public void a(){
    }
}

และถ้า B ต้องการนำมาใช้ใหม่ () ใน A มันสามารถเขียนใหม่เป็น:

public class B implements A{
    public ConcreteA concreteA;
    public void a(){
        concreteA.a();
    }
}

ซึ่งไม่ต้องการการสืบทอดเพื่อแทนที่เมธอดเป็นจริงหรือไม่?



4
Downvoted เนื่องจาก (ในความคิดเห็นด้านล่าง), Telastyn และ Doc Brown เห็นด้วยเกี่ยวกับการเอาชนะ แต่ให้คำตอบใช่ / ไม่ใช่ตรงข้ามกับคำถามพาดหัวเพราะพวกเขาไม่เห็นด้วยกับความหมายของ "กลิ่นรหัส" ดังนั้นคำถามที่ตั้งใจจะเกี่ยวกับการออกแบบรหัสจึงได้รับการควบคู่ไปกับคำสแลง และฉันคิดว่าไม่จำเป็นดังนั้นเนื่องจากคำถามนั้นเกี่ยวกับเทคนิคหนึ่งต่ออีกเทคนิคหนึ่ง
Steve Jessop

5
คำถามไม่ได้ติดแท็ก Java แต่รหัสดูเหมือนจะเป็น Java ใน C # (หรือ C ++) คุณจะต้องใช้virtualคำหลักเพื่อสนับสนุนการแทนที่ ดังนั้นปัญหานี้เกิดขึ้นมาก (หรือน้อยกว่า) คลุมเครือโดยรายละเอียดของภาษา
Erik Eidt

1
ดูเหมือนว่าคุณสมบัติการเขียนโปรแกรมภาษาเกือบจะทุกอย่างย่อมถูกจัดเป็น "กลิ่นรหัส" หลังจากปีที่ผ่านมา
Brandon

2
ฉันรู้สึกว่าfinalตัวแก้ไขมีอยู่จริงสำหรับสถานการณ์เช่นนี้ finalหากลักษณะการทำงานของวิธีการที่เป็นเช่นนั้นมันไม่ได้ออกแบบมาเพื่อแทนที่แล้วทำเครื่องหมายว่า มิฉะนั้นคาดว่านักพัฒนาซอฟต์แวร์อาจเลือกที่จะแทนที่
Martin Tuskevicius

คำตอบ:


34

ไม่มันไม่ใช่กลิ่นรหัส

  • หากคลาสไม่ได้เป็นคลาสสุดท้ายจะอนุญาตให้ subclassed
  • หากวิธีการไม่เป็นที่สิ้นสุดจะอนุญาตให้มีการแทนที่

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

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

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

class A {
    public String getDescription(Element e) {
        // return default description for element
    }
}

class B extends A {
    public String getDescription(Element e) {
        // return customized description for element
    }
}

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

interface DescriptionProvider {
    String getDescription(Element e);
}

class A {
    private DescriptionProvider provider=new DefaultDescriptionProvider();

    public final String getDescription(Element e) {
       return provider.getDescription(e);
    }

    public final void setDescriptionProvider(@NotNull DescriptionProvider provider) {
        this.provider=provider;
    }
}

class B extends A {
    public B() {
        setDescriptionProvider(new CustomDescriptionProvider());
    }
}

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

ในตัวอย่างสุดท้ายของคุณไม่ควรAใช้DescriptionProviderหรือ
เห็น

@Spotted: A สามารถนำไปใช้DescriptionProviderและเป็นผู้ให้คำอธิบายเริ่มต้น แต่ความคิดที่นี่คือการแยกความกังวล: Aต้องการคำอธิบาย (บางคลาสอื่นอาจเกินไป) ในขณะที่การใช้งานอื่นให้พวกเขา
Peter Walser

2
ตกลงเสียงมากขึ้นเช่นรูปแบบกลยุทธ์
เห็น

@Spotted: คุณพูดถูกตัวอย่างเป็นรูปแบบกลยุทธ์ไม่ใช่รูปแบบมัณฑนากร (มัณฑนากรจะเพิ่มลักษณะการทำงานให้กับองค์ประกอบ) ฉันจะแก้ไขคำตอบของฉันขอบคุณ
Peter Walser

25

มันเป็นความจริงหรือไม่ที่วิธีการที่เป็นรูปธรรมที่สำคัญคือกลิ่นรหัส?

โดยทั่วไปแล้ววิธีการที่เป็นรูปธรรมที่สำคัญคือกลิ่นรหัส

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

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

ดังนั้นนี่ดูเหมือนจะเป็นกลิ่นสำหรับฉัน - บางครั้งก็ดีไม่ดีมักจะดูให้แน่ใจ


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

2
@DocBrown แน่นอน! ฉันเห็นด้วยว่าคำอธิบาย (และโดยเฉพาะอย่างยิ่งข้อสรุป) คัดค้านข้อความเริ่มต้น
Tersosauros

1
@Spotted: counterexample ที่ง่ายที่สุดคือNumberคลาสที่มีincrement()วิธีการตั้งค่าเป็นค่าที่ถูกต้องถัดไป หากคุณซับคลาสลงในEvenNumberตำแหน่งที่increment()เพิ่มสองแทนหนึ่ง (และตัวตั้งบังคับ enness), ข้อเสนอแรกของ OP ต้องการการใช้งานที่ซ้ำกันของทุกวิธีในชั้นเรียนและที่สองของเขาต้องเขียนวิธีการ wrapper เพื่อเรียกวิธีการในสมาชิก จุดประสงค์ของการสืบทอดส่วนหนึ่งมีไว้สำหรับชั้นเรียนเด็กเพื่อให้ชัดเจนว่าพวกเขาแตกต่างจากผู้ปกครองโดยให้วิธีการที่แตกต่างกันไปเท่านั้น
Blrfl

5
@DocBrown: เป็นกลิ่นรหัสไม่ได้หมายความถึงอะไรบางอย่างที่จำเป็นต้องเลวร้ายเพียงว่ามันมีแนวโน้มที่
mikołak

3
@ docbrown - สำหรับฉันนี้ตกอยู่ข้าง "องค์ประกอบที่โปรดปรานมากกว่ามรดก" หากคุณกำลังขี่วิธีการที่เป็นรูปธรรมคุณก็อยู่ในดินแดนเล็ก ๆ ที่มีมรดกอยู่แล้ว อะไรคืออัตราต่อรองที่คุณกำลังขับรถข้ามสิ่งที่คุณไม่ควรทำ จากประสบการณ์ของฉันฉันคิดว่ามันน่าจะเป็น YMMV
Telastyn

7

หากคุณต้องการแทนที่วิธีที่เป็นรูปธรรม [... ] สามารถเขียนใหม่เป็น

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

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


หากคลาส A นั้นได้รับการออกแบบมาเพื่อจะไม่เหมาะสมกว่าที่จะมีวิธีที่เป็นนามธรรมเพื่อแสดงเจตนาอย่างชัดเจนหรือไม่ คนที่เอาชนะวิธีนี้จะรู้ได้อย่างไรว่าวิธีการนั้นได้รับการออกแบบในลักษณะนี้
เห็น

@Spotted มีหลายกรณีที่มีการติดตั้งแคมป์เริ่มต้นไว้และแต่ละความเชี่ยวชาญเฉพาะต้องการแทนที่บางส่วนไม่ว่าจะขยายฟังก์ชันการทำงานหรือเพื่อประสิทธิภาพ ตัวอย่างที่เยี่ยมยอดคือผู้เยี่ยมชม ผู้ใช้รู้วิธีการออกแบบวิธีการจากเอกสาร; จะต้องมีเพื่ออธิบายสิ่งที่คาดหวังจากการแทนที่ด้วยวิธีใด
Jan Hudec

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

@ เห็น 1) ถ้ามันจะต้อง overriden ก็ควรจะเป็นabstract; แต่มีหลายกรณีที่ไม่จำเป็นต้องเป็น 2) ถ้ามันจะต้องไม่ถูกแทนที่มันควรจะเป็นfinal(ไม่ใช่เสมือน) แต่ยังมีอีกหลายกรณีที่วิธีการอาจถูกแทนที่ 3) หากผู้พัฒนาปลายน้ำไม่ได้อ่านเอกสารพวกเขาจะยิงตัวเองด้วยการเดินเท้า แต่พวกเขาจะทำเช่นนั้นไม่ว่าคุณจะวางมาตรการอะไร
Jan Hudec

ตกลงดังนั้นมุมมองของเราความแตกต่างอยู่ใน 1 คำเท่านั้น คุณบอกว่าทุกวิธีควรเป็นนามธรรมหรือสุดท้ายในขณะที่ฉันบอกว่าทุกวิธีต้องเป็นนามธรรมหรือสุดท้าย :) กรณีเหล่านี้เกิดขึ้นเมื่อมีความจำเป็นในการเอาชนะเมธอด (และปลอดภัย)?
เห็น

3

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

นี่คือ (ชัด) มากแตกต่างจากขอบเขตของภาษาที่พิมพ์แบบคงที่เช่น Java, C ++, ฯลฯ รวมถึงลักษณะของพิมพ์ที่แข็งแกร่งเช่นที่ใช้ใน Java & C ++ รวมถึง C # และอื่น ๆ


ฉันเดาว่าคุณมาจากพื้นหลังของจาวาซึ่งวิธีการนั้นจะต้องถูกทำเครื่องหมายอย่างชัดเจนหากผู้ออกแบบสัญญาตั้งใจที่จะไม่ลบล้าง อย่างไรก็ตามในภาษาเช่น C # ตรงกันข้ามเป็นค่าเริ่มต้น วิธีการคือ (ไม่มีการตกแต่ง, คำหลักพิเศษ) " ปิดผนึก " (รุ่น C # ของfinal), และจะต้องประกาศอย่างชัดเจนว่าvirtualว่าพวกเขาได้รับอนุญาตให้ถูกแทนที่

หาก API หรือไลบรารีได้รับการออกแบบในภาษาเหล่านี้ด้วยวิธีการที่เป็นรูปธรรมที่overridable นั้นจะต้อง (อย่างน้อยในบางกรณี) ทำให้รู้สึกว่ามันจะถูกแทนที่ ดังนั้นจึงไม่มีกลิ่นรหัสเพื่อแทนที่วิธีการที่ไม่ได้ปิดผนึก / เสมือน / ไม่ใช่finalคอนกรีต     (ซึ่งถือว่าแน่นอนว่าผู้ออกแบบ API กำลังทำตามอนุสัญญาและทำเครื่องหมายเป็นvirtual(C #) หรือทำเครื่องหมายเป็นfinal(Java) วิธีการที่เหมาะสมสำหรับสิ่งที่พวกเขากำลังออกแบบ)


ดูสิ่งนี้ด้วย


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

2

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

ฉันจะไปให้ดียิ่งขึ้นโดยกล่าวว่ากลิ่นรหัสแรกคือเมื่อวิธีการที่ไม่เป็นไปมิได้abstract finalเพราะมันช่วยให้การเปลี่ยนแปลง (โดยการเอาชนะ) พฤติกรรมของกิจการที่สร้างขึ้น (พฤติกรรมนี้สามารถสูญหายหรือเปลี่ยนแปลง) ด้วยabstractและfinalเจตนาไม่ชัดเจน:

  • บทคัดย่อ: คุณต้องระบุพฤติกรรม
  • สุดท้าย: คุณไม่ได้รับอนุญาตให้ปรับเปลี่ยนพฤติกรรม

วิธีที่ปลอดภัยที่สุดในการเพิ่มลักษณะการทำงานให้กับคลาสที่มีอยู่คือตัวอย่างล่าสุดของคุณ: การใช้รูปแบบมัณฑนากร

public interface A{
    void a();
}

public final class ConcreteA implements A{
    public void a() {
        ...
    }
}

public final class B implements A{
    private final ConcreteA concreteA;
    public void a(){
        //Additionnal behaviour
        concreteA.a();
    }
}

9
นี้สีดำและสีขาววิธีการไม่ได้จริงๆทุกสิ่งที่มีประโยชน์ ลองคิดดูObject.toString()ใน Java ... ถ้าเป็นอย่างนี้สิ่งที่เป็นabstractค่าเริ่มต้นจะไม่ทำงานและรหัสก็จะไม่ถูกคอมไพล์แม้บางคนไม่ได้ถูกแทนที่ด้วยtoString()วิธีการ! ถ้าเป็นfinalเช่นนั้นสิ่งต่าง ๆ จะยิ่งแย่ลงไปกว่าเดิม - ไม่มีความสามารถในการแปลงออบเจ็กต์เป็นสตริงยกเว้นวิธีจาวา เนื่องจาก@Peter Walserพูดถึงคำตอบการใช้งานเริ่มต้น (เช่นObject.toString()) จึงมีความสำคัญ โดยเฉพาะอย่างยิ่งในวิธีการเริ่มต้น Java 8
Tersosauros

@ Tersosauros คุณพูดtoString()ถูกabstractหรือถ้าfinalมันเป็นความเจ็บปวด ความคิดเห็นส่วนตัวของฉันคือวิธีนี้เสียและมันจะดีกว่าที่จะไม่ใช้มันเลย (เช่นเท่ากับและ hashCode ) วัตถุประสงค์เริ่มต้นของการเริ่มต้นของ Java คือการอนุญาตให้มีการวิวัฒนาการ API ด้วย retrocompatibility
เห็น

คำว่า "ความเข้ากันได้ย้อนหลัง" มีพยางค์ที่น่ากลัวมากมาย
Craig

@ เห็นฉันผิดหวังมากที่การยอมรับทั่วไปของการสืบทอดที่เป็นรูปธรรมในเว็บไซต์นี้ฉันคาดหวังได้ดีกว่า: / คำตอบของคุณควรมี upvotes อีกมากมาย
TheCatWhisperer

1
@TheCatWhisperer ฉันเห็นด้วย แต่ในทางกลับกันฉันใช้เวลานานมากในการหาข้อดีของการใช้คำบรรยายในการตกแต่งเหนือสิ่งที่เป็นรูปธรรม (อันที่จริงมันชัดเจนเมื่อฉันเริ่มเขียนบททดสอบ) ซึ่งคุณไม่สามารถตำหนิใครได้ . สิ่งที่ดีที่สุดที่ต้องทำคือพยายามให้การศึกษาแก่ผู้อื่นจนกว่าพวกเขาจะค้นพบความจริง (เรื่องตลก) :)
เห็น
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.