มีรูปแบบภาษาหรือการออกแบบที่อนุญาต * การกำจัด * ของพฤติกรรมวัตถุหรือคุณสมบัติในลำดับชั้นของคลาสหรือไม่?


28

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

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

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

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

ดังนั้นคำถามของฉันอาจมีค่าเป็น "ฉันจะยกเลิกการใช้คุณลักษณะได้อย่างไร" หากซุปเปอร์คลาสของคุณเป็น Java Serializable คุณก็ต้องเป็นหนึ่งด้วยแม้ว่าคุณจะไม่มีวิธีในการทำให้เป็นอันดับสถานะของคุณเช่นถ้าคุณมี "Socket"

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


3
'โดยไม่ต้องใช้แฮ็คที่น่ากลัวทุกที่': การปิดใช้งานพฤติกรรมเป็นแฮ็คที่น่ากลัว: มันจะบอกเป็นนัยว่าfunction save_yourself_from_crashing_airplane(Bird b) { f.fly() }จะมีความซับซ้อนมากขึ้น (ดังที่ Peter Törökกล่าวว่าเป็นการละเมิด LSP)
keppla

การรวมกันของรูปแบบกลยุทธ์และการสืบทอดอาจทำให้คุณ "เขียนทับ" พฤติกรรมที่สืบทอดสำหรับประเภทพิเศษบางประเภทได้หรือไม่ เมื่อคุณพูดว่า: " it would be nice if one could define "exceptions" afterward, without having to use some horrible hacks everywhere"คุณพิจารณาวิธีการของโรงงานที่ควบคุมพฤติกรรมการแฮ็กหรือไม่?
StuperUser

1
หนึ่งสามารถของหลักสูตรเพียงแค่โยนจากNotSupportedException Penguin.fly()
เฟลิกซ์โดเบก

ตราบใดที่ภาษายังคงดำเนินต่อไปคุณสามารถยกเลิกการใช้วิธีการในชั้นเรียนย่อยได้ class Penguin < Bird; undef fly; end;ยกตัวอย่างเช่นในทับทิม ไม่ว่าคุณควรเป็นคำถามอื่น
นาธานลอง

สิ่งนี้จะทำลายหลักการ liskov และเนื้อหาทั้งหมดของ OOP
deadalnix

คำตอบ:


17

ดังที่คนอื่น ๆ พูดถึงคุณจะต้องต่อต้าน LSP

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

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

โดยทั่วไปภาษาแบบไดนามิกช่วยให้คุณสามารถแสดงสิ่งนี้ได้อย่างง่ายดายตัวอย่างการใช้ JavaScript ดังต่อไปนี้:

var Penguin = Object.create(Bird);
Penguin.fly = undefined;
Penguin.swim = function () { ... };

ในกรณีพิเศษนี้Penguinกำลังแชโดว์Bird.flyวิธีที่สืบทอดโดยการเขียนflyคุณสมบัติที่มีค่าundefinedไปยังวัตถุ

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

ทางเลือกคือการไม่ทำให้สมมติฐานกว้าง ๆ ว่านกสามารถบินได้ มันจะมีเหตุผลที่จะมีสิ่งที่เป็นBirdนามธรรมซึ่งทำให้นกทุกตัวได้รับมรดกจากมันโดยไม่ล้มเหลว นี่หมายถึงการตั้งสมมติฐานว่าคลาสย่อยทั้งหมดสามารถเก็บได้

โดยทั่วไปแล้วแนวคิดของ Mixin จะมีผลบังคับใช้ที่นี่ มีคลาสพื้นฐานที่บางมากและผสมพฤติกรรมอื่น ๆ ทั้งหมดเข้าด้วยกัน

ตัวอย่าง:

// for some value of Object.make
var Penguin = Object.make(
  /* base class: */ Bird,
  /* mixins: */ Swimmer, ...
);
var Hawk = Object.make(
  /* base class: */ Bird,
  /* mixins: */ Flyer, Carnivore, ...
);

หากคุณอยากรู้อยากเห็นฉันมีการดำเนินการObject.make

นอกจากนี้:

ดังนั้นคำถามของฉันอาจมีค่าเป็น "ฉันจะยกเลิกการใช้คุณลักษณะได้อย่างไร" หากซุปเปอร์คลาสของคุณเป็น Java Serializable คุณก็ต้องเป็นหนึ่งด้วยแม้ว่าคุณจะไม่มีวิธีในการทำให้เป็นอันดับสถานะของคุณเช่นถ้าคุณมี "Socket"

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

นี่คือที่การส่องสว่างขององค์ประกอบวัตถุ

ในฐานะที่เป็นกัน Serializable ไม่ได้หมายความว่าทุกอย่างควรจะต่อเนื่องก็หมายความว่า "รัฐที่คุณสนใจ" ควรต่อเนื่อง

คุณไม่ควรใช้คุณลักษณะ "NotX" นั่นเป็นเพียงการขยายตัวของรหัสที่น่ากลัว หากฟังก์ชั่นคาดว่าวัตถุบินมันควรผิดพลาดและเผาไหม้เมื่อคุณให้มันมหึมา


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

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

2
@ Raynos: เพนกวินเป็นขนนกแน่นอน ขนของพวกเขาค่อนข้างสั้นและแน่นหนาแน่นอน
Jon Purdy

@ JonPurdy ยุติธรรมพอฉันมักจะนึกว่าพวกเขามีขน
Raynos

+1 โดยทั่วไปและโดยเฉพาะอย่างยิ่งสำหรับ "แมมมอ ธ " ฮ่า ๆ!
Sebastien Diot

28

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

บางภาษามีลักษณะหรือมิกซ์อินเพื่อจัดการกับปัญหาดังกล่าวอย่างยืดหยุ่นยิ่งขึ้น


1
LSP สำหรับประเภทไม่ได้สำหรับการเรียน
Jörg W Mittag

2
@ PéterTörök: คำถามนี้ไม่มีอยู่จริง :-) ฉันสามารถนึกถึงสองตัวอย่างจาก Ruby Classเป็น subclass ของModuleแม้ว่าIS-Not-AClass Moduleแต่มันก็ยังสมเหตุสมผลที่จะเป็นคลาสย่อยเนื่องจากมันจะนำโค้ดจำนวนมากกลับมาใช้ใหม่ OTOH, StringIOIS-A IO, แต่ทั้งสองไม่มีความสัมพันธ์ในการสืบทอด (นอกเหนือจากที่แน่นอนของการสืบทอดทั้งสองจากObjectแน่นอน) เพราะพวกเขาไม่แบ่งปันรหัสใด ๆ ชั้นเรียนมีไว้สำหรับการแบ่งปันรหัสประเภทสำหรับการอธิบายโปรโตคอล IOและStringIOมีโปรโตคอลเดียวกันดังนั้นประเภทเดียวกัน แต่คลาสของพวกเขาไม่เกี่ยวข้อง
Jörg W Mittag

1
@ JörgWMittagโอเคตอนนี้ฉันเข้าใจดีขึ้นแล้วว่าคุณหมายถึงอะไร อย่างไรก็ตามสำหรับฉันตัวอย่างแรกของคุณฟังดูเป็นการใช้งานในทางที่ผิดมากกว่าการแสดงออกของปัญหาพื้นฐานบางอย่างที่คุณดูเหมือนจะแนะนำ ไม่ควรใช้การสืบทอดสาธารณะ IMO เพื่อนำไปใช้งานซ้ำเพื่อแสดงความสัมพันธ์ย่อย (is-a) เท่านั้น และความจริงที่ว่ามันสามารถนำไปใช้ในทางที่ผิดไม่ได้ตัดสิทธิ์ - ฉันไม่สามารถจินตนาการเครื่องมือที่ใช้งานได้จากโดเมนใด ๆ ที่ไม่สามารถใช้ในทางที่ผิด
PéterTörök

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

1
ลองจินตนาการถึงจาวาที่อินเทอร์เฟซเป็นประเภทเท่านั้นคลาสไม่ได้และคลาสย่อยสามารถใช้อินเทอร์เฟซ "เลิกใช้งาน" ของซูเปอร์คลาสของพวกเขาได้และคุณมีความคิดคร่าวๆ
Jörg W Mittag

15

Fly()อยู่ในตัวอย่างแรกใน: รูปแบบการออกแบบแรกสำหรับรูปแบบกลยุทธ์และนี่เป็นสถานการณ์ที่ดีว่าทำไมคุณควร"สนับสนุนการแต่งเพลงมากกว่าการสืบทอด" .

คุณสามารถผสมองค์ประกอบและมรดกโดยมี supertypes ของFlyingBird, FlightlessBirdที่มีพฤติกรรมที่ถูกต้องฉีดโดยโรงงานที่เชื้อที่เกี่ยวข้องเช่นPenguin : FlightlessBirdได้รับโดยอัตโนมัติและสิ่งอื่นจริงๆโดยเฉพาะได้รับการจัดการโดยโรงงานเป็นเรื่องของหลักสูตร


1
ฉันพูดถึงรูปแบบของมัณฑนากรในคำตอบของฉัน แต่รูปแบบกลยุทธ์นั้นใช้งานได้ดีเช่นกัน
jhocking

1
+1 สำหรับ "การจัดองค์ประกอบรายการโปรดมากกว่ามรดก" อย่างไรก็ตามความจำเป็นของรูปแบบการออกแบบพิเศษเพื่อใช้การจัดองค์ประกอบในภาษาที่พิมพ์แบบคงที่จะตอกย้ำอคติของฉันต่อภาษาแบบไดนามิกเช่น Ruby
Roy Tinker

11

ไม่ใช่ปัญหาที่แท้จริงที่คุณสมมติว่าBirdมีFlyวิธีใช่หรือไม่ ทำไมไม่:

class Bird
{
    // features that all birds have
}

class BirdThatCanSwim : Bird
{
    public void Swim() {...};
}

class BirdThatCanFly : Bird
{
    public void Fly() {...};
}


class Penguin : BirdThatCanSwim { }
class Sparrow : BirdThatCanFly { }

ตอนนี้ปัญหาที่ชัดเจนคือการรับมรดกหลายรายการ ( Duck) ดังนั้นสิ่งที่คุณต้องการจริงๆคืออินเทอร์เฟซ:

interface IBird { }
interface IBirdThatCanSwim : IBird { public void Swim(); }
interface IBirdThatCanFly : IBird { public void Fly(); }
interface IBirdThatCanQuack : IBird { public void Quack(); }

class Duck : BirdThatCanFly, IBirdThatCanSwim, IBirdThatCanQuack
{
    public void Swim() {...};
    public void Quack() {...};
}

3
ปัญหาคือวิวัฒนาการไม่ได้เป็นไปตามหลักการทดแทนของ Liskov และทำการถ่ายทอดโดยการลบคุณลักษณะออก
Donal Fellows

7

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

แต่เป็นPéterTörökกล่าวว่ามันจะละเมิด LSP


ในส่วนนี้ฉันจะลืมเกี่ยวกับ LSP และสมมติว่า:

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

คุณพูดว่า:

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

ดูเหมือนว่าสิ่งที่คุณต้องการคือ Python " ขอการอภัยมากกว่าการขออนุญาต "

เพียงให้เพนกวินของคุณโยนข้อยกเว้นหรือสืบทอดจากคลาส NonFlyingBird ที่ส่งข้อยกเว้น (รหัสหลอก):

class Penguin extends Bird {
     function fly():void {
          throw new Exception("Hey, I'm a penguin, I can't fly !");
     }
}

สิ่งที่คุณเลือก: เพิ่มข้อยกเว้นหรือลบวิธีการในที่สุดรหัสต่อไปนี้ (สมมติว่าภาษาของคุณรองรับการลบวิธี):

var bird:Bird = new Penguin();
bird.fly();

จะโยนข้อยกเว้นรันไทม์


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

@pdr: มันไม่ได้บอกว่านกเพนกวินสามารถบินได้ แต่มันควรจะบิน (เป็นสัญญา) ยกเว้นในกรณีที่จะบอกคุณว่ามันไม่สามารถ โดยวิธีการที่ฉันไม่ได้อ้างว่ามันเป็นแบบฝึกหัดที่ดี OOP ฉันแค่ให้คำตอบกับคำถามบางส่วน
David

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

@pdr "ฉันต้องลองใช้ / ลองดูในเวอร์ชันของคุณ" -> นั่นคือประเด็นทั้งหมดในการขอการให้อภัยมากกว่าการขออนุญาต (เพราะแม้แต่เป็ดก็สามารถหักปีกและไม่สามารถบินได้) ฉันจะแก้ไขถ้อยคำ
David

"นั่นคือจุดรวมของการขอการให้อภัยมากกว่าการอนุญาต" ใช่ยกเว้นว่าจะอนุญาตให้เฟรมเวิร์กมีข้อยกเว้นชนิดเดียวกันสำหรับวิธีการที่ขาดหายไปดังนั้น Python จึง "ลอง: ยกเว้น AttributeError:" เทียบเท่ากับ C # 's "ถ้า (X คือ Y) {} else {}" และจดจำได้ทันที เช่นนี้ แต่ถ้าคุณจงใจโยน CANFlyException เพื่อแทนที่ฟังก์ชันการบินเริ่มต้น () ในเบิร์ดมันจะกลายเป็นที่รู้จักน้อยกว่า
pdr

7

ในขณะที่มีคนชี้ให้เห็นข้างต้นในความคิดเห็นเพนกวินเป็นนกเพนกวินไม่บินดังนั้นนกทุกชนิดไม่สามารถบินได้

ดังนั้น Bird.fly () ไม่ควรมีอยู่หรือไม่ได้รับอนุญาต ฉันชอบอดีต

การมี FlyingBird ขยายนกมีวิธี. fly () จะถูกต้องแน่นอน


ฉันเห็นด้วย Fly ควรเป็นอินเทอร์เฟซที่นกสามารถนำไปใช้ได้ มันสามารถนำมาใช้เป็นวิธีการเช่นเดียวกับพฤติกรรมเริ่มต้นที่สามารถ overriden แต่วิธีการที่สะอาดกว่าคือการใช้อินเตอร์เฟซ
Jon Raynor

6

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

ดังนั้นแทนที่จะเป็น:

class Bird {
public:
   virtual void fly()=0;
};

คุณควรมีสิ่งนี้:

   class Bird {
   public:
      virtual float fly(float x) const=0;
   };

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

พฤติกรรมที่แท้จริงอาจเข้ารหัสได้ยาก แต่เป็นวิธีเดียวที่จะระบุอินเทอร์เฟซของคุณได้อย่างถูกต้อง

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


1
ฉันค่อนข้างชอบคำตอบของคุณ ฉันคิดว่ามันสมควรได้รับคะแนนเสียงมากขึ้น
Sebastien Diot

2
คำตอบที่ยอดเยี่ยม ปัญหาคือว่าคำถามเพียงแค่โบกมือของมันเป็นสิ่งที่บิน () ไม่จริง การดำเนินการใด ๆ ที่แท้จริงของการบินจะมีอย่างน้อยที่สุดปลายทาง - บิน (พิกัดปลายทาง) ซึ่งในกรณีของนกเพนกวินอาจถูกแทนที่ในการดำเนินการ {return currentPosition)}
Chris Cudmore

4

ในทางเทคนิคคุณสามารถทำสิ่งนี้ในภาษาที่พิมพ์แบบไดนามิก / เป็ด (JavaScript, Ruby, Lua และอื่น ๆ ) แต่มันก็เป็นความคิดที่แย่จริงๆ การลบเมธอดจากคลาสเป็นฝันร้ายของการบำรุงรักษาซึ่งคล้ายกับการใช้ตัวแปรโกลบอล (เช่นคุณไม่สามารถบอกได้ในโมดูลเดียวว่าสถานะโกลบอลยังไม่ได้รับการแก้ไขที่อื่น)

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


3

ปีเตอร์ได้พูดถึงหลักการทดแทน Liskov แต่ฉันรู้สึกว่าต้องอธิบาย

ให้ q (x) เป็นคุณสมบัติที่พิสูจน์ได้เกี่ยวกับวัตถุ x ชนิด T จากนั้น q (y) ควรจะพิสูจน์ได้สำหรับวัตถุ y ประเภท S ที่ S เป็นชนิดย่อยของ T

ดังนั้นหากนก (วัตถุ x ชนิด T) สามารถบินได้ (q (x)) ดังนั้นนกเพนกวิน (วัตถุ y ประเภท S) สามารถบินได้ (q (y)) โดยคำจำกัดความ แต่นั่นไม่ใช่กรณีที่ชัดเจน นอกจากนี้ยังมีสิ่งมีชีวิตอื่น ๆ ที่สามารถบินได้ แต่ไม่มีประเภทของนก

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

แต่ทุกคุณสมบัติของซูเปอร์คลาสควรใช้กับคลาสย่อยทั้งหมด

[ตอบกลับเพื่อแก้ไข]

การใช้ "ลักษณะ" ของ CanFly กับ Bird นั้นไม่ได้ดีไปกว่านี้แล้ว มันยังคงแนะนำให้เรียกรหัสว่านกทุกตัวสามารถบินได้

ลักษณะในคำที่คุณกำหนดเป็นสิ่งที่ Liskov หมายถึงเมื่อเธอพูดว่า "คุณสมบัติ"


2

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

ทางเลือกคือการปรับโครงสร้างชั้นเรียนของคุณ ลองสร้างคลาส "FlyingCreature" (หรืออินเทอร์เฟซที่ดีกว่าถ้าคุณกำลังจัดการกับภาษาที่อนุญาต) "เบิร์ด" ไม่ได้สืบทอดมาจาก FlyingCreature แต่คุณสามารถสร้าง "FlyingBird" ได้ Lark, Vulture และ Eagle ล้วนสืบทอดมาจาก FlyingBird เพนกวินไม่ได้ มันเพิ่งได้รับมรดกมาจากนก

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


0

วิธีทั่วไปในการจัดการกับสถานการณ์เช่นนี้คือการทิ้งสิ่งที่ต้องการUnsupportedOperationException(Java) resp NotImplementedException(C #)


ตราบใดที่คุณบันทึกความเป็นไปได้นี้ใน Bird
DJClayworth

0

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

0) อย่าสันนิษฐานว่า "การพิมพ์แบบคงที่" (ฉันทำเมื่อฉันถามเพราะฉันใช้ Java เป็นพิเศษ) โดยทั่วไปปัญหาจะขึ้นอยู่กับประเภทของภาษาที่ใช้

1) หนึ่งควรแยกลำดับชั้นของประเภทจากลำดับชั้นนำมาใช้รหัสในการออกแบบและในหัวของพวกเขาแม้ว่าพวกเขาส่วนใหญ่ทับซ้อนกัน โดยทั่วไปให้ใช้คลาสเพื่อนำมาใช้ซ้ำและอินเทอร์เฟซสำหรับชนิด

2) สาเหตุที่ปกติ Bird IS-A Fly เป็นเพราะนกส่วนใหญ่สามารถบินได้ดังนั้นจึงเป็นประโยชน์จากมุมมองการใช้รหัสซ้ำ แต่การพูดว่า Bird IS-A Fly นั้นผิดจริง ๆ เนื่องจากมีข้อยกเว้นอย่างน้อยหนึ่งข้อ (เพนกวิน).

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

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

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

6) ทางเลือกอื่นที่ไม่ต้องการการสืบทอดหลายแบบคือการใช้การแต่งเพลงแทนการสืบทอด แต่ละด้านของสัตว์เป็นแบบจำลองโดยชั้นอิสระและนกคอนกรีตเป็นองค์ประกอบของนกและอาจบินหรือว่ายน้ำ ... คุณจะได้รับรหัสเต็ม - นำมาใช้ใหม่ แต่ต้องทำอย่างน้อยหนึ่งขั้นตอนเพิ่มเติมเพื่อให้ได้ที่ ฟังก์ชั่นการบินเมื่อคุณมีการอ้างอิงของนกคอนกรีต นอกจากนี้ภาษาธรรมชาติ "object IS-A Fly" และ "object AS-A (cast) Fly" จะไม่ทำงานอีกต่อไปดังนั้นคุณต้องประดิษฐ์ไวยากรณ์ของคุณเอง (ภาษาไดนามิกบางภาษาอาจมีวิธีแก้ไข) นี่อาจทำให้รหัสของคุณยุ่งยากขึ้น

7) กำหนดคุณลักษณะ Fly ของคุณเพื่อให้มีวิธีการที่ชัดเจนสำหรับสิ่งที่ไม่สามารถบินได้ Fly.getNumberOfWings () สามารถส่งคืน 0 หาก Fly.fly (ทิศทาง currentPotinion) ควรส่งคืนตำแหน่งใหม่หลังจากเที่ยวบินกว่า Penguin.fly () สามารถส่งคืนตำแหน่งปัจจุบันได้โดยไม่ต้องเปลี่ยน คุณอาจจบลงด้วยรหัสที่ใช้งานได้ในทางเทคนิค แต่มีบางประการ ประการแรกรหัสบางอย่างอาจไม่มีพฤติกรรม "ไม่ทำอะไรเลย" ที่ชัดเจน นอกจากนี้หากมีใครเรียก x.fly () พวกเขาคาดหวังว่าจะทำอะไรบางอย่างแม้ว่าความคิดเห็นจะบอกว่า fly () ไม่ได้รับอนุญาตให้ทำอะไรเลย ในที่สุดนกเพนกวิน IS-A Flying ก็ยังคงกลับมาจริงซึ่งอาจสร้างความสับสนให้กับโปรแกรมเมอร์

8) ทำตาม 5) แต่ใช้การเรียงความเพื่อหลีกเลี่ยงกรณีที่ต้องใช้การสืบทอดหลายแบบ นี่คือตัวเลือกที่ฉันต้องการสำหรับภาษาแบบคงที่เนื่องจาก 6) ดูเหมือนจะยุ่งยากกว่า (และอาจต้องใช้หน่วยความจำมากขึ้นเพราะเรามีวัตถุมากกว่า) ภาษาแบบไดนามิกอาจทำให้ 6) ยุ่งยากน้อยลง แต่ฉันสงสัยว่ามันจะยุ่งยากน้อยกว่า 5)


0

กำหนดพฤติกรรมเริ่มต้น (ทำเครื่องหมายเป็นเสมือน) ในคลาสฐานและแทนที่มันตามความจำเป็น ด้วยวิธีนี้นกทุกตัวสามารถ "บิน"

แม้แต่นกเพนกวินก็บินมันก็ลื่นบนน้ำแข็งที่ระดับความสูงศูนย์!

พฤติกรรมการบินสามารถถูกแทนที่ได้ตามความจำเป็น

ความเป็นไปได้อีกอย่างหนึ่งคือการมีส่วนต่อประสาน Fly ไม่ใช่นกทุกตัวที่จะใช้ส่วนต่อประสานนั้น

class eagle : bird, IFly
class penguin : bird

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


-1

ฉันคิดว่ารูปแบบที่คุณกำลังมองหาคือความแตกต่างที่ดี แม้ว่าคุณจะสามารถลบอินเทอร์เฟซออกจากชั้นเรียนในบางภาษาได้ แต่ก็ไม่ใช่ความคิดที่ดีสำหรับเหตุผลที่PéterTörökให้ไว้ อย่างไรก็ตามในภาษา OO คุณสามารถแทนที่วิธีการในการเปลี่ยนพฤติกรรมของมันและนั่นรวมถึงการไม่ทำอะไรเลย ในการขอยืมตัวอย่างของคุณคุณอาจมีวิธีการ Penguin :: fly () ที่ทำสิ่งใดสิ่งหนึ่งต่อไปนี้:

  • ไม่มีอะไร
  • โยนข้อยกเว้น
  • เรียกเมธอด Penguin :: swim () แทน
  • ยืนยันว่าเพนกวินอยู่ใต้น้ำ (พวกมันทำ "บิน" ผ่านน้ำ)

คุณสมบัติสามารถเพิ่มและลบได้ง่ายขึ้นเล็กน้อยหากคุณวางแผนล่วงหน้า คุณสามารถจัดเก็บคุณสมบัติในแผนที่ / พจนานุกรม / เชื่อมโยงอาร์เรย์แทนที่จะใช้ตัวแปรอินสแตนซ์ คุณสามารถใช้รูปแบบ Factory เพื่อสร้างอินสแตนซ์มาตรฐานของโครงสร้างดังกล่าวดังนั้น Bird ที่มาจาก BirdFactory จะเริ่มด้วยชุดคุณสมบัติเดียวกันเสมอ การเข้ารหัสค่าคีย์วัตถุประสงค์ของ C เป็นตัวอย่างที่ดีของสิ่งนี้

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

การใช้ตัวอย่างนกเพนกวินของคุณวิธีหนึ่งในการรีแฟคเตอร์คือการแยกความสามารถในการบินจากคลาสเบิร์ด เนื่องจากไม่ใช่นกทุกชนิดที่สามารถบินได้รวมถึงวิธีการบิน () ใน Bird ที่ไม่เหมาะสมและนำไปสู่ปัญหาที่คุณถามโดยตรง ดังนั้นย้ายเมธอด fly () (และอาจจะบินขึ้น () และ land ()) ไปยังคลาส Aviator หรือส่วนต่อประสาน (ขึ้นอยู่กับภาษา) สิ่งนี้ช่วยให้คุณสร้างคลาส FlyingBird ที่สืบทอดมาจากทั้งนกและนักบิน (หรือสืบทอดมาจากนกและเครื่องมือนักบิน) นกเพนกวินสามารถสืบทอดต่อจากนกโดยตรง แต่ไม่ใช่นักบินดังนั้นจึงหลีกเลี่ยงปัญหาได้ ข้อตกลงดังกล่าวอาจทำให้ง่ายต่อการสร้างคลาสสำหรับสิ่งบินอื่น ๆ : FlyingFish, FlyingMammal, FlyingMachine, AnnoyingInsect และอื่น ๆ


2
-1 เพื่อแนะนำให้โทรไปที่ Penguin :: swim () ที่ละเมิดหลักการของความประหลาดใจอย่างน้อยและจะทำให้โปรแกรมเมอร์บำรุงรักษาทุกที่จะสาปแช่งชื่อของคุณ
DJClayworth

1
@DJClayworth เป็นตัวอย่างในด้านที่ไร้สาระในสถานที่แรก downvoting สำหรับการละเมิดพฤติกรรมการอนุมานของแมลงวัน () และว่ายน้ำ () ดูเหมือนว่ามาก แต่ถ้าคุณต้องการดูสิ่งนี้อย่างจริงจังจริง ๆ ฉันยอมรับว่าเป็นไปได้มากกว่าที่คุณจะไปทางอื่นและใช้การว่ายน้ำ () ในรูปแบบของการบิน () เป็ดว่ายน้ำโดยการพายเท้า เพนกวินว่ายน้ำโดยการกระพือปีกของพวกเขา
คาเลบ

1
ฉันเห็นด้วยกับคำถามที่ว่าโง่ แต่ปัญหาคือฉันเห็นคนทำสิ่งนี้ในชีวิตจริง - ใช้สายที่มีอยู่ว่า "ไม่ทำอะไรเลย" เพื่อใช้งานฟังก์ชั่นที่หายาก มันทำให้รหัสผิดพลาดและมักจะจบลงด้วยการเขียนว่า "if (! (myBird instanceof Penguin)) fly ();" ในหลาย ๆ ที่ในขณะที่หวังว่าจะไม่มีใครสร้างชั้นเรียนของนกกระจอกเทศ
DJClayworth

การยืนยันนั้นแย่ยิ่งกว่าเดิม หากฉันมีฝูง Birds ซึ่งทั้งหมดมีวิธี fly () ฉันไม่ต้องการความล้มเหลวในการยืนยันเมื่อฉันเรียกใช้ fly () กับพวกเขา
DJClayworth

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