มรดกทับทิมเทียบกับมิกซ์อิน


127

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

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

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

คำตอบ:


176

ฉันเพิ่งอ่านเกี่ยวกับหัวข้อนี้ใน The Well-Grounded Rubyist (หนังสือดีมาก) ผู้เขียนอธิบายได้ดีกว่าที่ฉันคิดดังนั้นฉันจะพูดเขา:


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

  • โมดูลไม่มีอินสแตนซ์ตามที่ว่าโดยทั่วไปเอนทิตีหรือสิ่งต่าง ๆ จะถูกสร้างแบบจำลองที่ดีที่สุดในคลาสและลักษณะหรือคุณสมบัติของเอนทิตีหรือสิ่งต่างๆจะถูกห่อหุ้มในโมดูล ตามที่ระบุไว้ในหัวข้อ 4.1.1 ชื่อคลาสมักจะเป็นคำนามในขณะที่ชื่อโมดูลมักเป็นคำคุณศัพท์ (Stack กับ Stacklike)

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

การสรุปกฎเหล่านี้ในตัวอย่างเดียวนี่คือสิ่งที่คุณไม่ควรทำ:

module Vehicle 
... 
class SelfPropelling 
... 
class Truck < SelfPropelling 
  include Vehicle 
... 

แต่คุณควรทำสิ่งนี้:

module SelfPropelling 
... 
class Vehicle 
  include SelfPropelling 
... 
class Truck < Vehicle 
... 

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


1
ตัวอย่างแสดงให้เห็นอย่างเรียบร้อย - Truck IS A Vehicle - ไม่มีรถบรรทุกใดที่จะไม่ใช่ยานพาหนะ
PL J

1
ตัวอย่างแสดงให้เห็นอย่างเรียบร้อย - TruckIS A Vehicle- ไม่มีTruckที่จะไม่เป็นไฟล์Vehicle. อย่างไรก็ตามฉันจะเรียกโมดูลว่าSelfPropelable(:?) อืมSelfPropeledฟังดูเหมือนถูก แต่ก็เกือบจะเหมือนกัน: D อย่างไรก็ตามผมจะไม่รวมไว้ในVehicleแต่ในTruck- SelfPropeledที่มียานพาหนะที่ไม่ได้ ข้อบ่งชี้ที่ดีก็คือการถาม - มีสิ่งอื่นที่ไม่ใช่ยานพาหนะSelfPropeledหรือไม่? - บางทีอาจจะหายากกว่า ดังนั้นจึงVehicleสามารถสืบทอดจากคลาส SelfPropelling ได้ (เนื่องจากคลาสมันไม่พอดีSelfPropeled- เนื่องจากมีบทบาทมากกว่า)
PL J

39

ฉันคิดว่ามิกซ์อินเป็นความคิดที่ดี แต่มีปัญหาอีกอย่างที่ไม่มีใครพูดถึง: การชนกันของเนมสเปซ พิจารณา:

module A
  HELLO = "hi"
  def sayhi
    puts HELLO
  end
end

module B
  HELLO = "you stink"
  def sayhi
    puts HELLO
  end
end

class C
  include A
  include B
end

c = C.new
c.sayhi

คนไหนชนะ? ในทับทิมก็จะเปิดออกจะเป็นหลังเพราะคุณรวมมันหลังจากmodule B module Aตอนนี้มันเป็นเรื่องง่ายที่จะหลีกเลี่ยงปัญหานี้ให้แน่ใจว่าทั้งหมดของmodule Aและmodule Bค่าคงที่ 's และวิธีการอยู่ใน namespaces น่า ปัญหาคือคอมไพเลอร์ไม่เตือนคุณเลยเมื่อเกิดการชนกัน

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


2
นี่เป็นคำเตือนที่ชาญฉลาด ชวนให้นึกถึงข้อผิดพลาดในการถ่ายทอดทางพันธุกรรมหลายประการใน C ++
Chris Tonkinson

1
มีการบรรเทาที่ดีสำหรับสิ่งนี้หรือไม่? นี่ดูเหมือนจะเป็นเหตุผลว่าทำไมการสืบทอดหลายรายการของ Python จึงเป็นวิธีแก้ปัญหาที่เหนือกว่า (ไม่ได้พยายามเริ่มการจับคู่ภาษา p * ssing เพียงแค่เปรียบเทียบคุณสมบัติเฉพาะนี้)
Marcin

1
@bazz เยี่ยมมาก แต่การจัดองค์ประกอบในภาษาส่วนใหญ่เป็นเรื่องยุ่งยาก นอกจากนี้ยังเกี่ยวข้องกับภาษาพิมพ์เป็ดเป็นส่วนใหญ่ นอกจากนี้ยังไม่รับประกันว่าคุณจะไม่ได้รับสถานะแปลก ๆ
Marcin

โพสต์เก่าฉันรู้ แต่ยังคงปรากฏในการค้นหา คำตอบคือบางส่วนไม่ถูกต้อง - C#sayhiเอาต์พุตB::HELLOไม่ได้เพราะผสมทับทิมขึ้นคงที่ แต่เพราะคงทับทิมแก้ไขจากที่ใกล้ชิดกับไกล - เพื่อHELLOอ้างอิงในมักจะมีมติให้B B::HELLOสิ่งนี้ถือแม้ว่าคลาส C จะกำหนดว่าเป็นของตัวเองC::HELLOด้วยก็ตาม
Laas

13

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


2
สิ่งนี้ตอบคำถามโดยตรง: การสืบทอดบังคับใช้โครงสร้างองค์กรเฉพาะที่สามารถทำให้โครงการของคุณอ่านง่ายขึ้น
Emery

10

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

และใช่ควรรวม ActiveRecord แทนที่จะขยายโดยคลาสย่อย ORM อื่น - datamapper - บรรลุสิ่งนั้นอย่างแม่นยำ!


4

ฉันชอบคำตอบของ Andy Gaskell มาก - แค่อยากจะเพิ่มว่าใช่ ActiveRecord ไม่ควรใช้การสืบทอด แต่ควรรวมโมดูลเพื่อเพิ่มพฤติกรรม (ส่วนใหญ่คงอยู่) ให้กับโมเดล / คลาส ActiveRecord เป็นเพียงการใช้กระบวนทัศน์ที่ไม่ถูกต้อง

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

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


1

วิธีที่ดีที่สุดที่ฉันเข้าใจมิกซ์อินคือคลาสเสมือน Mixins คือ "คลาสเสมือน" ที่ถูกฉีดเข้าไปในห่วงโซ่บรรพบุรุษของคลาสหรือโมดูล

เมื่อเราใช้ "รวม" และส่งผ่านโมดูลมันจะเพิ่มโมดูลลงในห่วงโซ่บรรพบุรุษก่อนคลาสที่เรากำลังสืบทอดจาก:

class Parent
end 

module M
end

class Child < Parent
  include M
end

Child.ancestors
 => [Child, M, Parent, Object ...

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

module M
  def m
    puts 'm'
  end
end

class Test
end

Test.extend M
Test.m

เราสามารถเข้าถึงคลาส singleton ด้วยเมธอด singleton_class:

Test.singleton_class.ancestors
 => [#<Class:Test>, M, #<Class:Object>, ...

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

module M
  def self.included(target)
    puts "included into #{target}"
  end

  def self.extended(target)
    puts "extended into #{target}"
  end
end

class MyClass
  include M
end

class MyClass2
  extend M
end

สิ่งนี้ทำให้เกิดรูปแบบที่น่าสนใจที่นักพัฒนาสามารถนำไปใช้ได้:

module M
  def self.included(target)
    target.send(:include, InstanceMethods)
    target.extend ClassMethods
    target.class_eval do
      a_class_method
    end
  end

  module InstanceMethods
    def an_instance_method
    end
  end

  module ClassMethods
    def a_class_method
      puts "a_class_method called"
    end
  end
end

class MyClass
  include M
  # a_class_method called
end

อย่างที่คุณเห็นโมดูลเดียวนี้กำลังเพิ่มอินสแตนซ์เมธอดเมธอด "คลาส" และทำหน้าที่โดยตรงกับคลาสเป้าหมาย (ในกรณีนี้เรียก a_class_method ())

ActiveSupport :: ความกังวลห่อหุ้มรูปแบบนี้ นี่คือโมดูลเดียวกันที่เขียนใหม่เพื่อใช้ ActiveSupport :: Concern:

module M
  extend ActiveSupport::Concern

  included do
    a_class_method
  end

  def an_instance_method
  end

  module ClassMethods
    def a_class_method
      puts "a_class_method called"
    end
  end
end

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