thừa kế ruby ​​vs mixins


127

Trong Ruby, vì bạn có thể bao gồm nhiều mixin nhưng chỉ mở rộng một lớp, có vẻ như mixin sẽ được ưu tiên hơn kế thừa.

Câu hỏi của tôi: nếu bạn viết mã phải được mở rộng / bao gồm là hữu ích, tại sao bạn lại biến nó thành một lớp? Hoặc đặt một cách khác, tại sao bạn không luôn biến nó thành một mô-đun?

Tôi chỉ có thể nghĩ ra một lý do tại sao bạn muốn có một lớp học, và đó là nếu bạn cần khởi tạo lớp học. Tuy nhiên, trong trường hợp ActiveRecord :: Base, bạn không bao giờ khởi tạo nó trực tiếp. Vì vậy, nó không nên là một mô-đun thay thế?

Câu trả lời:


176

Tôi chỉ đọc về chủ đề này trong The Well-Grounded Rubyist (cuốn sách tuyệt vời, nhân tiện). Tác giả làm một công việc giải thích tốt hơn tôi nên tôi sẽ trích dẫn anh ta:


Không có quy tắc hoặc công thức duy nhất luôn dẫn đến thiết kế đúng. Nhưng thật hữu ích khi ghi nhớ một vài điều cần lưu ý khi bạn đưa ra quyết định theo mô-đun:

  • Các mô-đun không có trường hợp. Nó theo sau rằng các thực thể hoặc mọi thứ thường được mô hình hóa tốt nhất trong các lớp và các đặc tính hoặc thuộc tính của các thực thể hoặc các thứ được gói gọn trong các mô-đun. Tương ứng, như đã lưu ý trong phần 4.1.1, tên lớp có xu hướng là danh từ, trong khi tên mô-đun thường là tính từ (Stack so với Stacklike).

  • Một lớp có thể chỉ có một siêu lớp, nhưng nó có thể trộn thành nhiều mô-đun như nó muốn. Nếu bạn đang sử dụng tính kế thừa, hãy ưu tiên tạo mối quan hệ siêu lớp / lớp con hợp lý. Đừng sử dụng mối quan hệ siêu lớp của một lớp và duy nhất để ban cho lớp với những gì có thể chỉ là một trong một số các đặc điểm.

Tóm tắt các quy tắc này trong một ví dụ, đây là những điều bạn không nên làm:

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

Thay vào đó, bạn nên làm điều này:

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

Phiên bản thứ hai mô hình các thực thể và thuộc tính gọn gàng hơn nhiều. Xe tải đi xuống từ Xe cộ (có ý nghĩa), trong khi SelfPropelling là một đặc điểm của phương tiện (ít nhất, tất cả những gì chúng ta quan tâm trong mô hình này của thế giới) hoặc hình thức chuyên biệt, của Xe.


1
Ví dụ cho thấy nó gọn gàng - Xe tải là phương tiện - không có xe tải nào không phải là phương tiện.
PL J

1
Ví dụ cho thấy nó gọn gàng - TruckLÀ A Vehicle- không có gì Trucklà không có Vehicle. Tuy nhiên, tôi gọi mô-đun có lẽ SelfPropelable(:?) Hmm SelfPropelednghe có vẻ đúng, nhưng nó gần giống nhau: D. Dù sao, tôi sẽ không bao gồm nó trong Vehiclenhưng trong Truck- vì có những phương tiện KHÔNG CÓ SelfPropeled. Ngoài ra dấu hiệu tốt là hỏi - có những thứ khác, KHÔNG phải là phương tiện SelfPropeledkhông? - Có lẽ, nhưng tôi sẽ khó tìm hơn. Vì vậy, Vehiclecó thể kế thừa từ lớp SelfPropelling (lớp này sẽ không phù hợp với SelfPropeled- vì đó là vai trò nhiều hơn)
PL J

39

Tôi nghĩ mixins là một ý tưởng tuyệt vời, nhưng có một vấn đề khác ở đây mà không ai đề cập đến: va chạm không gian tên. Xem xét:

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

Cái nào thắng? Trong Ruby, hóa ra là cái sau module B, bởi vì bạn đã bao gồm nó sau module A. Bây giờ, thật dễ dàng để tránh vấn đề này: chắc chắn rằng tất cả module Amodule Bhằng số và phương pháp 's là trong không gian tên khó xảy ra. Vấn đề là trình biên dịch không cảnh báo bạn chút nào khi va chạm xảy ra.

Tôi lập luận rằng hành vi này không mở rộng cho các nhóm lập trình viên lớn-- bạn không nên cho rằng người thực hiện class Cbiết về mọi tên trong phạm vi. Ruby thậm chí sẽ cho phép bạn ghi đè một hằng hoặc phương thức của một loại khác . Tôi không chắc rằng có thể đã từng được coi là hành vi đúng.


2
Đây là một lời cảnh báo khôn ngoan. Gợi nhớ về nhiều cạm bẫy thừa kế trong C ++.
Chris Tonkinson

1
Có sự giảm thiểu tốt cho việc này? Điều này có vẻ như là một lý do tại sao Python thừa kế nhiều là một giải pháp ưu việt (không cố gắng bắt đầu một kết hợp ngôn ngữ p * ssing; chỉ so sánh tính năng cụ thể này).
Marcin

1
@bazz Điều đó thật tuyệt vời và tất cả, nhưng thành phần trong hầu hết các ngôn ngữ là cồng kềnh. Nó cũng chủ yếu liên quan đến các ngôn ngữ gõ vịt. Nó cũng không đảm bảo rằng bạn không nhận được trạng thái kỳ lạ.
Marcin

Bài cũ, tôi biết, nhưng vẫn bật ra trong các tìm kiếm. Câu trả lời là một phần không chính xác - C#sayhikết quả đầu ra B::HELLOkhông phải vì Ruby trộn lẫn các hằng số, mà bởi vì ruby ​​giải quyết các hằng số từ gần đến xa hơn - vì vậy, HELLOtham chiếu trong Bsẽ luôn luôn giải quyết B::HELLO. Điều này đúng ngay cả khi lớp C cũng xác định nó cũng thuộc sở hữu của C::HELLOnó.
Laas

13

My Take: Các mô-đun là để chia sẻ hành vi, trong khi các lớp là để mô hình hóa mối quan hệ giữa các đối tượng. Về mặt kỹ thuật, bạn chỉ có thể biến mọi thứ thành đối tượng của Object và trộn vào bất kỳ mô-đun nào bạn muốn để có được tập hợp hành vi mong muốn, nhưng đó sẽ là một thiết kế nghèo nàn, khó hiểu và khá khó đọc.


2
Điều này trả lời câu hỏi theo cách trực tiếp: kế thừa thực thi một cấu trúc tổ chức cụ thể có thể làm cho dự án của bạn dễ đọc hơn.
emery

10

Câu trả lời cho câu hỏi của bạn chủ yếu theo ngữ cảnh. Chắt lọc sự quan sát của pubb, sự lựa chọn chủ yếu được điều khiển bởi miền đang được xem xét.

Và vâng, ActiveRecord nên được đưa vào thay vì được mở rộng bởi một lớp con. Một ORM khác - datamapper - chính xác đạt được điều đó!


4

Tôi rất thích câu trả lời của Andy Gaskell - chỉ muốn thêm rằng có, ActiveRecord không nên sử dụng tính kế thừa, mà nên bao gồm một mô-đun để thêm hành vi (chủ yếu là sự kiên trì) vào mô hình / lớp. ActiveRecord chỉ đơn giản là sử dụng mô hình sai.

Vì lý do tương tự, tôi rất thích MongoId hơn MongoMapper, vì nó cho phép nhà phát triển cơ hội sử dụng tính kế thừa như một cách mô hình hóa một cái gì đó có ý nghĩa trong miền vấn đề.

Thật đáng buồn khi có khá nhiều người trong cộng đồng Rails đang sử dụng "thừa kế Ruby" theo cách nó được sử dụng - để xác định hệ thống phân cấp lớp, không chỉ để thêm hành vi.


1

Cách tốt nhất tôi hiểu mixins là các lớp ảo. Mixins là "các lớp ảo" đã được đưa vào chuỗi tổ tiên của một lớp hoặc mô-đun.

Khi chúng ta sử dụng "bao gồm" và truyền cho nó một mô-đun, nó sẽ thêm mô-đun vào chuỗi tổ tiên ngay trước lớp mà chúng ta đang kế thừa từ:

class Parent
end 

module M
end

class Child < Parent
  include M
end

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

Mọi đối tượng trong Ruby cũng có một lớp đơn. Các phương thức được thêm vào lớp singleton này có thể được gọi trực tiếp trên đối tượng và do đó chúng hoạt động như các phương thức "lớp". Khi chúng ta sử dụng "mở rộng" trên một đối tượng và truyền cho đối tượng một mô-đun, chúng ta sẽ thêm các phương thức của mô-đun vào lớp singleton của đối tượng:

module M
  def m
    puts 'm'
  end
end

class Test
end

Test.extend M
Test.m

Chúng ta có thể truy cập lớp singleton bằng phương thức singleton_group:

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

Ruby cung cấp một số hook cho các mô-đun khi chúng được trộn vào các lớp / mô-đun. includedlà một phương thức hook được cung cấp bởi Ruby, được gọi bất cứ khi nào bạn đưa mô-đun vào một số mô-đun hoặc lớp. Cũng giống như bao gồm, có một extendedcái móc liên quan để mở rộng. Nó sẽ được gọi khi một mô-đun được mở rộng bởi một mô-đun hoặc lớp khác.

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

Điều này tạo ra một mô hình thú vị mà các nhà phát triển có thể sử dụng:

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

Như bạn có thể thấy, mô-đun đơn này đang thêm các phương thức cá thể, phương thức "lớp" và hoạt động trực tiếp trên lớp đích (gọi a_group_method () trong trường hợp này).

ActiveSupport :: Mối quan tâm gói gọn mẫu này. Đây là cùng một mô-đun được viết lại để sử dụng ActiveSupport :: Mối quan tâm:

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

-1

Ngay bây giờ, tôi đang nghĩ về templatemẫu thiết kế. Nó chỉ không cảm thấy đúng với một mô-đun.

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.