Làm thế nào để triển khai một lớp trừu tượng trong ruby?


121

Tôi biết không có khái niệm về lớp trừu tượng trong ruby. Nhưng nếu nó cần phải được thực hiện, làm thế nào để tiếp tục nó? Tôi đã thử một cái gì đó như ...

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

class B < A
  ...
  ...
end

Nhưng khi tôi cố gắng khởi tạo B, nó sẽ gọi nội bộ A.newmà sẽ tăng ngoại lệ.

Ngoài ra, các mô-đun không thể được khởi tạo nhưng chúng cũng không thể được kế thừa. đặt phương thức mới ở chế độ riêng tư cũng sẽ không hoạt động. Bất kỳ gợi ý?


1
Các mô-đun có thể được trộn vào, nhưng tôi cho rằng bạn cần kế thừa cổ điển vì một số lý do khác?
Zach

6
Tôi không cần phải triển khai một lớp trừu tượng. Tôi đã tự hỏi về cách làm điều đó, nếu có cần phải làm điều đó. Một vấn đề lập trình. Đó là nó.
Chirantan

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

Câu trả lời:


61

Tôi không thích sử dụng các lớp trừu tượng trong Ruby (hầu như luôn có một cách tốt hơn). Nếu bạn thực sự nghĩ rằng đó là kỹ thuật tốt nhất cho tình huống này, bạn có thể sử dụng đoạn mã sau để khai báo rõ hơn về những phương thức nào là trừu tượng:

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

Về cơ bản, bạn chỉ cần gọi abstract_methodsvới danh sách các phương thức trừu tượng và khi chúng được gọi bởi một thể hiện của lớp trừu tượng, một NotImplementedErrorngoại lệ sẽ được đưa ra.


Đây thực sự giống như một giao diện nhưng tôi có ý tưởng. Cảm ơn.
Chirantan

6
Điều này nghe có vẻ không phải là trường hợp sử dụng hợp lệ NotImplementedErrormà về cơ bản có nghĩa là "phụ thuộc vào nền tảng, không có sẵn trên của bạn". Xem tài liệu .
skalee

6
Bạn đang thay thế một phương thức một dòng bằng lập trình meta, bây giờ cần bao gồm một mixin và gọi một phương thức. Tôi không nghĩ rằng điều này là khai báo hơn chút nào.
Pascal

1
Tôi đã chỉnh sửa câu trả lời này, sửa một số lỗi mã và cập nhật nó, vì vậy nó hiện chạy. Cũng đơn giản hóa mã một chút, để sử dụng mở rộng thay vì bao gồm, do: yehudakatz.com/2009/11/12/better-ruby-idioms
Magne

1
@ManishShrivastava: Vui lòng xem đoạn mã này ngay bây giờ, để biết nhận xét về tầm quan trọng của việc sử dụng END tại đây so với kết thúc
Magne

113

Tôi nghĩ rằng không có lý do gì để ngăn ai đó khởi tạo lớp trừu tượng, đặc biệt là vì họ có thể thêm các phương thức vào nó một cách nhanh chóng .

Các ngôn ngữ gõ kiểu vịt, như Ruby, sử dụng sự hiện diện / vắng mặt hoặc hành vi của các phương thức trong thời gian chạy để xác định xem chúng có nên được gọi hay không. Do đó, câu hỏi của bạn, vì nó áp dụng cho một phương pháp trừu tượng , có ý nghĩa

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

và đó sẽ là phần cuối của câu chuyện. Lý do duy nhất để sử dụng các lớp trừu tượng trong Java là để nhấn mạnh rằng một số phương thức nhất định được "điền vào" trong khi các phương thức khác có hành vi của chúng trong lớp trừu tượng. Trong ngôn ngữ gõ vịt, trọng tâm là các phương thức, không phải vào các lớp / loại, vì vậy bạn nên chuyển nỗi lo lắng của mình sang cấp độ đó.

Trong câu hỏi của bạn, về cơ bản bạn đang cố gắng tạo lại abstracttừ khóa từ Java, đây là một mã để thực hiện Java trong Ruby.


3
@Christopher Perry: Có lý do gì không?
SasQ

10
@ChristopherPerry Tôi vẫn chưa hiểu. Tại sao tôi không muốn sự phụ thuộc này nếu xét cho cùng thì cha mẹ và anh chị em quan hệ họ hàng với nhau và tôi muốn mối quan hệ này rõ ràng? Ngoài ra, để soạn một đối tượng của một số lớp bên trong một số lớp khác, bạn cũng cần biết định nghĩa của nó. Kế thừa thường được thực hiện dưới dạng thành phần, nó chỉ làm cho giao diện của đối tượng được tạo thành một phần của giao diện của lớp nhúng nó. Vì vậy, bạn cần định nghĩa đối tượng được nhúng hoặc đối tượng kế thừa. Hoặc có thể bạn đang nói về điều gì đó khác? Bạn có thể giải thích thêm về điều đó?
SasQ

2
@SasQ, Bạn không cần biết chi tiết triển khai của lớp cha để soạn nó, bạn chỉ cần biết 'API của nó. Tuy nhiên, nếu bạn thừa kế bạn đang phụ thuộc vào sự thực hiện của cha mẹ. Nếu việc triển khai thay đổi mã của bạn có thể bị hỏng theo những cách không mong muốn. Thêm chi tiết ở đây
Christopher Perry

16
Xin lỗi, nhưng "Thành phần ủng hộ hơn là thừa kế" không có nghĩa là "Luôn luôn là thành phần người dùng". Mặc dù nói chung nên tránh thừa kế, có một số trường hợp sử dụng khi chúng phù hợp hơn. Đừng làm theo cuốn sách một cách mù quáng.
Nowaker

1
@Nowaker Điểm rất quan trọng. Vì vậy, chúng ta thường có xu hướng bị che khuất bởi những thứ mà chúng ta đọc hoặc nghe, thay vì nghĩ "cách tiếp cận thực dụng trong trường hợp này là gì". Nó hiếm khi hoàn toàn đen hoặc trắng.
Per Lundberg

44

Thử cái này:

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

class B < A
  def initialize
  end
end

38
Nếu bạn muốn có thể sử dụng super in #initializecủa B, bạn thực sự có thể nâng bất cứ thứ gì trong A # khởi tạo if self.class == A.
mk 12

17
class A
  private_class_method :new
end

class B < A
  public_class_method :new
end

7
Ngoài ra, người ta có thể sử dụng hook kế thừa của lớp cha để làm cho phương thức khởi tạo tự động hiển thị trong tất cả các lớp con: def A.inhe inherit (subclass); subclass.instance_eval {public_class_method: new}; kết thúc
t6d

1
T6d rất hay. Đối với một nhận xét, chỉ cần đảm bảo rằng nó được ghi lại, vì đó là hành vi đáng ngạc nhiên (vi phạm ít bất ngờ nhất).
bluehavana

16

đối với bất kỳ ai trong thế giới rails, việc triển khai mô hình ActiveRecord dưới dạng lớp trừu tượng được thực hiện với khai báo này trong tệp mô hình:

self.abstract_class = true

12

2 ¢ của tôi: Tôi chọn một kết hợp DSL đơn giản, nhẹ:

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

Và, tất nhiên, việc thêm một lỗi khác để khởi tạo lớp cơ sở sẽ rất nhỏ trong trường hợp này.


1
EDIT: Đối với biện pháp tốt, vì phương pháp này sử dụng define_method, người ta có thể muốn chắc chắn backtrace vẫn còn nguyên vẹn, ví dụ như: err = NotImplementedError.new(message); err.set_backtrace caller()YMMV
Anthony Navarre

Tôi thấy cách tiếp cận này khá thanh lịch. Cảm ơn bạn đã đóng góp cho câu hỏi này.
wes.hysell

12

Trong 6 năm rưỡi qua lập trình Ruby, tôi chưa một lần cần đến một lớp trừu tượng.

Nếu bạn đang nghĩ rằng bạn cần một lớp trừu tượng, bạn đang nghĩ quá nhiều về một ngôn ngữ cung cấp / yêu cầu chúng, không phải trong Ruby như vậy.

Như những người khác đã đề xuất, mixin thích hợp hơn cho những thứ được cho là giao diện (như Java định nghĩa chúng) và suy nghĩ lại thiết kế của bạn phù hợp hơn cho những thứ "cần" các lớp trừu tượng từ các ngôn ngữ khác như C ++.

Cập nhật 2019: Tôi không cần các lớp trừu tượng trong Ruby trong 16 năm rưỡi sử dụng. Mọi thứ mà tất cả những người bình luận về phản hồi của tôi đang nói đều được giải quyết bằng cách thực sự học Ruby và sử dụng các công cụ thích hợp, như các mô-đun (thậm chí cung cấp cho bạn các triển khai chung). Có những người trong các nhóm mà tôi quản lý đã tạo các lớp có triển khai cơ sở không thành công (như lớp trừu tượng), nhưng những người này hầu hết là lãng phí mã hóa vì NoMethodErrorsẽ tạo ra kết quả chính xác giống như AbstractClassErrortrong quá trình sản xuất.


24
Bạn cũng không cần / cần / một lớp trừu tượng trong Java. Đó là một cách để ghi lại rằng nó là một lớp cơ sở và không nên được khởi tạo cho những người mở rộng lớp của bạn.
fijiaaron

3
IMO, một số ngôn ngữ sẽ hạn chế quan niệm của bạn về lập trình hướng đối tượng. Điều gì phù hợp trong một tình huống nhất định không nên phụ thuộc vào ngôn ngữ, trừ khi có lý do liên quan đến hiệu suất (hoặc điều gì đó hấp dẫn hơn).
thekingoftruth

10
@fijiaaron: Nếu bạn nghĩ vậy thì chắc chắn bạn không hiểu lớp cơ sở trừu tượng là gì. Chúng không nhiều để "tài liệu hóa" rằng một lớp không nên được khởi tạo (nó là một tác dụng phụ của nó là trừu tượng). Nó thiên về việc nêu một giao diện chung cho một loạt các lớp dẫn xuất, sau đó đảm bảo rằng nó sẽ được triển khai (nếu không, lớp dẫn xuất cũng sẽ vẫn trừu tượng). Mục tiêu của nó là hỗ trợ Nguyên tắc thay thế Liskov cho các lớp mà việc khởi tạo không có nhiều ý nghĩa.
SasQ

1
(tiếp) Tất nhiên người ta có thể tạo chỉ một số lớp với một số phương thức và thuộc tính chung trong chúng, nhưng khi đó trình biên dịch / thông dịch sẽ không biết rằng các lớp này có liên quan theo cách nào. Tên của các phương thức & thuộc tính có thể giống nhau trong mỗi lớp này, nhưng riêng lớp này không có nghĩa là chúng đại diện cho cùng một chức năng (sự tương ứng tên có thể chỉ là ngẫu nhiên). Cách duy nhất để nói với trình biên dịch về mối quan hệ này là sử dụng một lớp cơ sở, nhưng không phải lúc nào các cá thể của chính lớp cơ sở này cũng tồn tại.
SasQ


4

Cá nhân tôi nâng cao NotImplementedError trong các phương thức của các lớp trừu tượng. Nhưng bạn có thể muốn loại bỏ nó khỏi phương pháp 'mới', vì những lý do bạn đã đề cập.


Nhưng sau đó làm thế nào để ngăn nó không bị khởi tạo?
Chirantan

Cá nhân tôi mới bắt đầu với Ruby, nhưng trong Python, các lớp con với các phương thức __init ___ () được khai báo không tự động gọi các phương thức __init __ () của lớp cha của chúng. Tôi hy vọng sẽ có một khái niệm tương tự trong Ruby, nhưng giống như tôi đã nói, tôi chỉ mới bắt đầu.
Zack

Trong initializecác phương thức của cha mẹ ruby không được gọi tự động trừ khi được gọi rõ ràng bằng super.
mk12

4

Nếu bạn muốn sử dụng một lớp không ổn định, trong phương thức A.new của bạn, hãy kiểm tra xem self == A trước khi đưa ra lỗi.

Nhưng thực sự, một mô-đun có vẻ giống như những gì bạn muốn ở đây - ví dụ, Enumerable là một loại thứ có thể là một lớp trừu tượng trong các ngôn ngữ khác. Về mặt kỹ thuật, bạn không thể phân loại chúng, nhưng việc gọi include SomeModuleđạt được gần như cùng một mục tiêu. Có một số lý do mà điều này không hiệu quả với bạn?


4

Bạn đang cố gắng phục vụ mục đích gì với một lớp trừu tượng? Có lẽ có một cách tốt hơn để làm điều đó trong ruby, nhưng bạn đã không cung cấp bất kỳ chi tiết nào.

Con trỏ của tôi là cái này; sử dụng một mixin không kế thừa.


Thật. Việc trộn một Mô-đun sẽ tương đương với việc sử dụng một AbstractClass: wiki.c2.com/?AbstractClass PS: Hãy gọi chúng là mô-đun chứ không phải mixin, vì mô-đun chính là chúng và trộn chúng vào là việc bạn làm với chúng.
Magne

3

Một câu trả lời khác:

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

Điều này dựa vào #method_missing bình thường để báo cáo các phương thức chưa hoàn thành, nhưng giữ cho các lớp trừu tượng không được triển khai (ngay cả khi chúng có phương thức khởi tạo)

class A
  include Abstract
end
class B < A
end

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

Giống như các áp phích khác đã nói, bạn có thể nên sử dụng một mixin, thay vì một lớp trừu tượng.


3

Tôi đã làm theo cách này, vì vậy nó xác định lại new trên lớp con để tìm một mới trên lớp không trừu tượng. Tôi vẫn không thấy bất kỳ thực tế nào trong việc sử dụng các lớp trừu tượng trong ruby.

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

Ngoài ra còn có abstract_typeviên ngọc nhỏ này , cho phép khai báo các lớp và mô-đun trừu tượng theo một cách không cần can thiệp.

Ví dụ (từ tệp 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

Không có gì sai với cách tiếp cận của bạn. Việc tạo ra một lỗi trong khởi tạo có vẻ tốt, miễn là tất cả các lớp con của bạn ghi đè quá trình khởi tạo. Nhưng bạn không muốn định nghĩa self.new như thế. Đây là những gì tôi sẽ làm.

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

Một cách tiếp cận khác sẽ được đặt tất cả chức năng đó trong một mô-đun, mà như bạn đã đề cập không bao giờ có thể được thực hiện. Sau đó, bao gồm mô-đun trong các lớp của bạn thay vì kế thừa từ một lớp khác. Tuy nhiên, điều này sẽ phá vỡ những thứ như siêu.

Vì vậy, nó phụ thuộc vào cách bạn muốn cấu trúc nó. Mặc dù các mô-đun dường như là một giải pháp gọn gàng hơn để giải quyết vấn đề "Làm cách nào để viết một số nội dung được ký hiệu cho các lớp khác sử dụng"


Tôi cũng không muốn làm điều đó. Vậy thì bọn trẻ không thể gọi là "siêu".
Austin Ziegler


0

Mặc dù điều này không giống như Ruby, bạn có thể làm điều này:

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

    puts 'initialized'
  end
end

class B < A
end

Kết quả:

>> A.new
  (rib):2:in `main'
  (rib):2:in `new'
  (rib):3:in `initialize'
RuntimeError: abstract class
>> B.new
initialized
=> #<B:0x00007f80620d8358>
>>
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.