วิธีการใช้คลาสนามธรรมในทับทิม?


121

ฉันรู้ว่าไม่มีแนวคิดเรื่องชนชั้นนามธรรมในทับทิม แต่ถ้าจำเป็นต้องมีการดำเนินการจะดำเนินการอย่างไร? ฉันลองทำสิ่งที่ชอบ ...

class A
  def self.new
    raise 'Doh! You are trying to write Java in Ruby!'
  end
end

class B < A
  ...
  ...
end

แต่เมื่อฉันพยายามสร้างอินสแตนซ์ B มันกำลังจะเรียกภายในA.newซึ่งจะเพิ่มข้อยกเว้น

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


1
สามารถผสมโมดูลได้ แต่ฉันคิดว่าคุณต้องการการสืบทอดแบบคลาสสิกด้วยเหตุผลอื่น?
Zach

6
ไม่ใช่ว่าฉันต้องใช้คลาสนามธรรม ฉันสงสัยเกี่ยวกับวิธีการทำถ้าจำเป็นต้องทำ ปัญหาการเขียนโปรแกรม แค่นั้นแหละ.
Chirantan

127
raise "Doh! You are trying to write Java in Ruby".
Andrew Grimm

คำตอบ:


61

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

module Abstract
  def abstract_methods(*args)
    args.each do |name|
      class_eval(<<-END, __FILE__, __LINE__)
        def #{name}(*args)
          raise NotImplementedError.new("You must implement #{name}.")
        end
      END
      # important that this END is capitalized, since it marks the end of <<-END
    end
  end
end

require 'rubygems'
require 'rspec'

describe "abstract methods" do
  before(:each) do
    @klass = Class.new do
      extend Abstract

      abstract_methods :foo, :bar
    end
  end

  it "raises NoMethodError" do
    proc {
      @klass.new.foo
    }.should raise_error(NoMethodError)
  end

  it "can be overridden" do
    subclass = Class.new(@klass) do
      def foo
        :overridden
      end
    end

    subclass.new.foo.should == :overridden
  end
end

โดยพื้นฐานแล้วคุณจะเรียกabstract_methodsด้วยรายการวิธีการที่เป็นนามธรรมและเมื่อพวกเขาถูกเรียกโดยอินสแตนซ์ของคลาสนามธรรมNotImplementedErrorจะมีการเพิ่มข้อยกเว้น


นี่เป็นเหมือนอินเทอร์เฟซจริงๆ แต่ฉันได้รับความคิด ขอบคุณ
Chirantan

6
นี่ไม่ใช่กรณีการใช้งานที่ถูกต้องNotImplementedErrorซึ่งโดยพื้นฐานแล้วหมายถึง "ขึ้นอยู่กับแพลตฟอร์มไม่สามารถใช้ได้กับคุณ" ดูเอกสาร
skalee

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

1
ฉันแก้ไขคำตอบนี้เพื่อแก้ไขข้อบกพร่องบางอย่างของโค้ดและอัปเดตโค้ดเพื่อให้ทำงานได้ในขณะนี้ ทำให้โค้ดง่ายขึ้นเล็กน้อยเพื่อใช้ขยายแทนการรวมเนื่องจาก: yehudakatz.com/2009/11/12/better-ruby-idioms
Magne

1
@ManishShrivastava: โปรดดูรหัสนี้ตอนนี้สำหรับความคิดเห็นเกี่ยวกับความสำคัญของการใช้ END here vs end
Magne

113

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

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

def get_db_name
   raise 'this method should be overriden and return the db name'
end

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

ในคำถามของคุณโดยพื้นฐานแล้วคุณกำลังพยายามสร้างabstractคำหลักจาก Java ซึ่งเป็นรหัสกลิ่นสำหรับการทำ Java ใน Ruby


3
@ คริสโตเฟอร์เพอร์รี: เหตุผลใด ๆ ?
SasQ

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

2
@SasQ คุณไม่จำเป็นต้องรู้รายละเอียดการใช้งานของคลาสพาเรนต์เพื่อสร้างมันคุณต้องรู้แค่ 'API เท่านั้น อย่างไรก็ตามหากคุณได้รับมรดกคุณต้องพึ่งพาการนำไปใช้ของผู้ปกครอง หากการนำไปใช้งานเปลี่ยนแปลงโค้ดของคุณอาจเสียหายในรูปแบบที่ไม่คาดคิด รายละเอียดเพิ่มเติมที่นี่
Christopher Perry

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

1
@Nowaker จุดสำคัญมาก บ่อยครั้งที่เรามักจะมองไม่เห็นสิ่งที่เราอ่านหรือได้ยินแทนที่จะคิดว่า "อะไรคือแนวทางปฏิบัติในกรณีนี้" มันแทบจะไม่เป็นสีดำหรือสีขาวเลย
Per Lundberg

44

ลองสิ่งนี้:

class A
  def initialize
    raise 'Doh! You are trying to instantiate an abstract class!'
  end
end

class B < A
  def initialize
  end
end

38
หากคุณต้องการที่จะสามารถที่จะใช้ในสุด#initializeของ B คุณสามารถจริงเพียงแค่เพิ่มสิ่งใน if self.class == AA
mk12

17
class A
  private_class_method :new
end

class B < A
  public_class_method :new
end

7
นอกจากนี้เราสามารถใช้ hook ที่สืบทอดมาของคลาสพาเรนต์เพื่อทำให้เมธอดตัวสร้างมองเห็นได้โดยอัตโนมัติในคลาสย่อยทั้งหมด: def A.inherited (คลาสย่อย); subclass.instance_eval {public_class_method: new}; สิ้นสุด
t6d

1
t6d ดีมาก ในการแสดงความคิดเห็นเพียงตรวจสอบให้แน่ใจว่าได้บันทึกไว้เนื่องจากเป็นพฤติกรรมที่น่าแปลกใจ (ละเมิดความประหลาดใจน้อยที่สุด)
bluehavana

16

สำหรับทุกคนในโลกแห่งรางการใช้โมเดล ActiveRecord เป็นคลาสนามธรรมทำได้ด้วยการประกาศนี้ในไฟล์โมเดล:

self.abstract_class = true

12

2 ¢ของฉัน: ฉันเลือกมิกซ์อิน DSL ที่เรียบง่ายและมีน้ำหนักเบา:

module Abstract
  extend ActiveSupport::Concern

  included do

    # Interface for declaratively indicating that one or more methods are to be
    # treated as abstract methods, only to be implemented in child classes.
    #
    # Arguments:
    # - methods (Symbol or Array) list of method names to be treated as
    #   abstract base methods
    #
    def self.abstract_methods(*methods)
      methods.each do |method_name|

        define_method method_name do
          raise NotImplementedError, 'This is an abstract base method. Implement in your subclass.'
        end

      end
    end

  end

end

# Usage:
class AbstractBaseWidget
  include Abstract
  abstract_methods :widgetify
end

class SpecialWidget < AbstractBaseWidget
end

SpecialWidget.new.widgetify # <= raises NotImplementedError

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


1
แก้ไข: เพื่อการวัดที่ดีเนื่องจากวิธีการนี้ใช้วิธีการ define_ เราอาจต้องการตรวจสอบให้แน่ใจว่า backtrace ยังคงอยู่เช่น: err = NotImplementedError.new(message); err.set_backtrace caller()YMMV
Anthony Navarre

ฉันพบว่าแนวทางนี้ค่อนข้างสง่างาม ขอขอบคุณสำหรับการสนับสนุนคำถามนี้
wes.hysell

12

ในช่วง 6 1/2 ปีที่ผ่านมาของการเขียนโปรแกรม Ruby ฉันไม่ต้องการคลาสนามธรรมเลยสักครั้ง

หากคุณคิดว่าคุณต้องการคลาสนามธรรมคุณกำลังคิดมากเกินไปในภาษาที่ให้ / ต้องการไม่ใช่ใน Ruby

ตามที่คนอื่น ๆ แนะนำมิกซ์อินเหมาะสมกว่าสำหรับสิ่งที่ควรจะเป็นอินเทอร์เฟซ (ตามที่ Java กำหนด) และการคิดใหม่ในการออกแบบของคุณจะเหมาะสมกว่าสำหรับสิ่งที่ "ต้องการ" คลาสนามธรรมจากภาษาอื่นเช่น C ++

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


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

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

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

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


4

โดยส่วนตัวแล้วฉันเพิ่ม NotImplementedError ในวิธีการของคลาสนามธรรม แต่คุณอาจต้องการออกจากวิธีการ 'ใหม่' ด้วยเหตุผลที่คุณกล่าวถึง


แต่แล้วจะหยุดไม่ให้เป็นอินสแตนซ์ได้อย่างไร?
Chirantan

โดยส่วนตัวแล้วฉันเพิ่งเริ่มต้นกับ Ruby แต่ใน Python คลาสย่อยที่มีการประกาศ __init ___ () เมธอดจะไม่เรียกเมธอด superclasses '__init __ () โดยอัตโนมัติ ฉันหวังว่าจะมีแนวคิดที่คล้ายกันใน Ruby แต่อย่างที่ฉันบอกว่าฉันเพิ่งเริ่มต้น
Zack

ในinitializeวิธีการของผู้ปกครองทับทิมจะไม่ถูกเรียกโดยอัตโนมัติเว้นแต่จะเรียกด้วย super อย่างชัดเจน
mk12

4

หากคุณต้องการไปกับคลาสที่ไม่หยุดนิ่งในวิธีใหม่ของคุณให้ตรวจสอบว่า self == A ก่อนที่จะโยนข้อผิดพลาด

แต่จริงๆแล้วโมดูลดูเหมือนสิ่งที่คุณต้องการมากกว่าที่นี่ตัวอย่างเช่น Enumerable คือประเภทของสิ่งที่อาจเป็นคลาสนามธรรมในภาษาอื่น ในทางเทคนิคคุณไม่สามารถ subclass ได้ แต่การโทรinclude SomeModuleจะบรรลุเป้าหมายเดียวกันโดยประมาณ มีเหตุผลบางอย่างที่ไม่เหมาะกับคุณหรือไม่?


4

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

ตัวชี้ของฉันคือนี่ ใช้mixinไม่ใช่การถ่ายทอดทางพันธุกรรม


จริง การผสมโมดูลจะเทียบเท่ากับการใช้ AbstractClass: wiki.c2.com/?AbstractClass PS: ขอเรียกว่าโมดูลไม่ใช่มิกซ์อินเนื่องจากโมดูลคือสิ่งที่พวกเขาเป็นและการผสมเข้าด้วยกันคือสิ่งที่คุณทำกับพวกมัน
Magne

3

คำตอบอื่น:

module Abstract
  def self.append_features(klass)
    # access an object's copy of its class's methods & such
    metaclass = lambda { |obj| class << obj; self ; end }

    metaclass[klass].instance_eval do
      old_new = instance_method(:new)
      undef_method :new

      define_method(:inherited) do |subklass|
        metaclass[subklass].instance_eval do
          define_method(:new, old_new)
        end
      end
    end
  end
end

สิ่งนี้อาศัย #method_missing ปกติในการรายงานวิธีการที่ไม่ได้นำไปใช้ แต่จะป้องกันไม่ให้ใช้คลาสนามธรรม (แม้ว่าจะมีวิธีการเริ่มต้นก็ตาม)

class A
  include Abstract
end
class B < A
end

B.new #=> #<B:0x24ea0>
A.new # raises #<NoMethodError: undefined method `new' for A:Class>

เช่นเดียวกับผู้โพสต์คนอื่น ๆ ได้กล่าวไว้คุณน่าจะใช้มิกซ์อินมากกว่าคลาสนามธรรม


3

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

puts 'test inheritance'
module Abstract
  def new
    throw 'abstract!'
  end
  def inherited(child)
    @abstract = true
    puts 'inherited'
    non_abstract_parent = self.superclass;
    while non_abstract_parent.instance_eval {@abstract}
      non_abstract_parent = non_abstract_parent.superclass
    end
    puts "Non abstract superclass is #{non_abstract_parent}"
    (class << child;self;end).instance_eval do
      define_method :new, non_abstract_parent.method('new')
      # # Or this can be done in this style:
      # define_method :new do |*args,&block|
        # non_abstract_parent.method('new').unbind.bind(self).call(*args,&block)
      # end
    end
  end
end

class AbstractParent
  extend Abstract
  def initialize
    puts 'parent initializer'
  end
end

class Child < AbstractParent
  def initialize
    puts 'child initializer'
    super
  end
end

# AbstractParent.new
puts Child.new

class AbstractChild < AbstractParent
  extend Abstract
end

class Child2 < AbstractChild

end
puts Child2.new

3

นอกจากนี้ยังมีขนาดเล็ก abstract_typeอัญมณีซึ่งช่วยให้สามารถประกาศคลาสและโมดูลที่เป็นนามธรรมได้อย่างไม่สะดุด

ตัวอย่าง (จากไฟล์README.md ):

class Foo
  include AbstractType

  # Declare abstract instance method
  abstract_method :bar

  # Declare abstract singleton method
  abstract_singleton_method :baz
end

Foo.new  # raises NotImplementedError: Foo is an abstract type
Foo.baz  # raises NotImplementedError: Foo.baz is not implemented

# Subclassing to allow instantiation
class Baz < Foo; end

object = Baz.new
object.bar  # raises NotImplementedError: Baz#bar is not implemented

1

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

class A
  class AbstractClassInstiationError < RuntimeError; end
  def initialize
    raise AbstractClassInstiationError, "Cannot instantiate this class directly, etc..."
  end
end

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

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


ฉันก็ไม่อยากทำอย่างนั้นเช่นกัน เด็กไม่สามารถเรียก "ซุปเปอร์" ได้แล้ว
Austin Ziegler


0

แม้ว่าจะไม่รู้สึกเหมือน Ruby แต่คุณสามารถทำได้:

class A
  def initialize
    raise 'abstract class' if self.instance_of?(A)

    puts 'initialized'
  end
end

class B < A
end

ผลลัพธ์:

>> A.new
  (rib):2:in `main'
  (rib):2:in `new'
  (rib):3:in `initialize'
RuntimeError: abstract class
>> B.new
initialized
=> #<B:0x00007f80620d8358>
>>
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.