Những điều nên biết khi sử dụng Metaprogramming trong Ruby

Bạn thường nghe nói Metaprogramming là cái mà chỉ dành riêng cho ngôn ngữ Ruby, và nó không đơn giản cho những lập trình viên trung bình. Nhưng sự thật là Metaprogramming không quá đáng sợ như vậy, bài viết này sẽ cung cấp những nền tảng cho những fresh developer để họ có thể sử dụng và gặt hái được những lợi ích từ nó.

Cần lưu ý rằng, metaprogramming rất rộng và nó nên được sử dụng một cách triệt để khi lập trình. Vì vậy, bài viết sẽ cố gắng để đưa ra một số ví dụ thực tế mà tất cả mọi người có thể sử dụng trong việc lập trình hằng ngày.

I, Metaprogramming

Metaprogramming là kỹ thuật cho phép bạn có thể viết code để sinh ra những code khác tự động khi chạy. Điều này có nghĩa là bạn có thể định nghĩa các method và lớp trong suốt thời gian chạy runtime. Quả là thú vi? Tóm lại, sử dụng metaprogramming bạn có thể mở lại và chỉnh sửa các lớp, bắt các method không tồn tại và tạo ra chúng. tạo ra những code DRY bằng việc tránh trùng lặp duplicate...

II, The basics

Trước khi đi sâu vào metaprogramming, chúng ta nên tìm hiểu một số điều cơ bản. Và cách tốt nhất để làm những điều đó là thông qua các ví dụ. Hãy bắt đầu từng bước một để hiểu về metaprogramming trong Ruby. Bạn hoàn toàn có thể đoán được đoạn code sau đang làm gì?

    class Developer

      def self.backend
        "I am backend developer"
      end

      def frontend
        "I am frontend developer"
      end

    end

Chúng ta đã định nghĩa một lớp với 2 method. Method đầu tiên trong lớp này là một class method và cái thứ 2 là một instance method. Đây là những lý thuyết cơ bản của ruby, nhưng có nhiều hơn cái đang xảy ra đằng sau dòng code này, cái mà chúng ta cần hiểu rõ trước khi chúng ta đi sâu hơn. Cần nhấn mạnh rằng, lớp Developer chính là một đối tượng. Trong Ruby, tất cả mọi thứ đều là đối tượng, bao gồm các lớp. khi Developer là một instance thì nó là một instance của lớp Class. Hình dưới đây sẽ mô tả mô hình đối tượng trong Ruby:

toptal-blog-image-1446120487914-384fae8f419347d455a43dab6e20cf25.jpg

    p Developer.class # Class
    p Class.superclass # Module
    p Module.superclass # Object
    p Object.superclass # BasicObject

Một điều quan trọng quan tâm ở đây là ý nghĩa của từ khóa “self”. Method frontend là một method thông thường, được xác định như là một instance của lớp Developer, nhưng tại sao method backend lại là một class method? Tất cả các phần code thực thi trong Ruby được thực hiện bằng từ khóa đặc biệt “self”. Khi Ruby thông dịch bất cứ dòng code nào, nó luôn theo dõi giá trị của self cho bất cứ dòng code nào. self luôn refer đến vài đối tượng, nhưng đối tượng đó có thể thay đổi trên những code được thực thi. Cho ví dụ, trong định nghĩa của một lớp, self refer đến chính nó, chính là một thể instance của lớp Class:

    class Developer
      p self
    end
    # Developer

Trong instance methods, self tham chiếu đến một thể hiện của lớp Developer:

    class Developer
      def frontend
        self
      end
    end

    p Developer.new.frontend
    # #<Developer:0x2c8a148>

Bên trong một class methods, self tham chiếu đến chính lớp đó, là một thể hiện của lớp Class:

    class Developer
      def self.backend
        self
      end
    end

    p Developer.backend
    # Developer

Thật là thú vị, nhưng sau tất cả thì class method là gi? Trước khi trả lời câu hỏi đó, chúng ta cần đề cập đến sự tồn tại của một thứ được gọi là metaclass hay còn được gọi là singleton class và eigenclass. Class method backend mà chúng ta đã định nghĩa ban đầu chính là một instance method được định nghĩa trong lớp metaclass của lớp Developer, được sử dụng cho đối tượng Developer. Một metaclass thực ra chính là một lớp, Ruby đã tạo ra và chèn nó vào hệ thống phân cấp thừa kế lưu giữ các phương thức lớp.

III, Metaclasses

Mỗi đối tượng trong Ruby đều có metaclass riêng của mình. Nó có vẻ vô hình đối với các nhà phát triển. Nhưng nó tồn tại và bạn có thể sử dụng nó dễ dàng. Khi lớp Developer của chúng ta là một đối tượng, nó cũng có một metaclass của chính nó. Ví dụ, hãy tạo một đối tượng của một lớp String và sử dụng metaclass của nó:

    example = "I'm a string object"

    def example.something
      self.upcase
    end

    p example.something
    # I'M A STRING OBJECT

Ở đây, chúng ta đã thêm một singleton method “something” đến một đối tượng. Sự khác nhau giữa class methods và singleton methods là class method được avaiable cho tất cả thể hiện của đối tượng lớp trong khi singleton method chỉ avaiable cho một thể hiện đơn lẻ. Class methods có phạm vị sử dụng rộng hơn singleton method, nhưng cả hai đều add thêm vào metaclass của đối tượng đó một method. Ví dụ trên có thể được viết lại như sau:

    example = "I'm a string object"

    class << example
      def example.something
        self.upcase
      end
    end

Cú pháp khác nhau nhưng ý nghĩa là tương tự. . Bây giờ chúng ta trở lại ví dụ ban đầu khi chúng ta tạo lớp Developer và tìm hiểu một số cú pháp khác được định nghĩa một class method:

    class Developer
      def self.backend
        "I am backend developer"
      end
    end

Đây là định nghĩa cơ bản, được hầu hết mọi người sử dụng:

    def Developer.backend
      "I am backend developer"
    end

Đoạn trên cũng có mục đích tương tự, chúng ta định nghĩa một class method backend cho Developer. Chúng ta đã không dùng self nhưng đã định nghĩa một method có ý nghĩa như một class method.

    class Developer
      class << self
        def backend
          "I am backend developer"
        end
      end
    end

Chúng ta đang định nghĩa một class method., nhưng đang sử dụng cú pháp tương tự như khi định nghĩa một singleton method cho một đối tượng String.

    class << Developer
      def backend
        "I am backend developer"
      end
    end

Bằng việc định nghĩa một block giống như trên, chúng ta đã thiết lập self đến metaclass của Developer. Kết quả là, method backend được add đến metaclass của Developer.

Hãy xem metaclass này thực thiên trong cây kế thừa như nào:

1.jpg

Như bạn đã thấy trong các ví dụ trước đó, không có bằng chứng chứng tỏ metaclass tồn tại nhưng chúng ta vẫn có thể sử dụng chúng để thấy được sự tồn tại của lớp vô hình này:

    class Object
      def metaclass_example
        class << self
          self
        end
      end
    end

Nếu chúng ta định nghĩa một instance method trong lớp nào đó thì với “self” bên trong method đó sẽ refer đến đối tượng của lớp này. Chúng ta có thể sử dụng cú pháp:class << self để thay đổi self đến một thể hiện của metaclass của lớp này.

    class Developer

      def frontend
        p "inside instance method, self is: " + self.to_s
      end

      class << self
        def backend
          p "inside class method, self is: " + self.to_s
        end
      end

    end

    developer = Developer.new
    developer.frontend
    # "inside instance method, self is: #<Developer:0x2ced3b8>"

    Developer.backend
    # "inside class method, self is: Developer"

    p "inside metaclass, self is: " + developer.metaclass_example.to_s
    # "inside metaclass, self is: #<Class:#<Developer:0x2ced3b8>>"

Và ta sẽ thấy frontend là instance method của class Developer con backend là instance method của metaclass của Developer

    p developer.class.instance_methods false
    # [:frontend]

    p developer.class.metaclass_example.instance_methods false
    # [:backend]

Để get vào metaclass bạn không cần mở lại đối tượng mà chỉ cần sử dụng singleton_class mà Ruby cung cấp:

    p developer.class.singleton_class.instance_methods false
    # [:backend]

IV, Defining Methods Using “class_eval” and “instance_eval”

Một trong những cách để tạo class method là sử dụng instance_eval:

    class Developer
    end

    Developer.instance_eval do
      p "instance_eval - self is: " + self.to_s
      def backend
        p "inside a method self is: " + self.to_s
      end
    end
    # "instance_eval - self is: Developer"

    Developer.backend
    # "inside a method self is: Developer"

Ta có thể thấy, trong trường hợp sử dụng instance_eval thì self ở trong luôn trỏ đến đối tượng Developer.

Một cách khác là sử dụng class_eval:

    Developer.class_eval do
      p "class_eval - self is: " + self.to_s
      def frontend
        p "inside a method self is: " + self.to_s
      end
    end
    # "class_eval - self is: Developer"

    p developer = Developer.new
    # #<Developer:0x2c5d640>

    developer.frontend
    # "inside a method self is: #<Developer:0x2c5d640>"

Với class_eval thì self ở trong một instance method sẽ trỏ đến đối tượng của lớp Developer.

V, Defining Missing Methods on the Fly

Một trong những phần của Metaprogramming là method_missing. Khi bạn gọi một method trên một đối tượng., đầu tiên Ruby sẽ vào trong lớp và duyệt các instance methods của nó. Nếu không tìm thấy method tương ứng, nó sẽ tiếp tục tìm trên các lớp cha. Nếu vẫn không tìm thấy, nó gọi đến một method khác có tên là method_missing là một instance method của Kernel.

define_method là một method được định nghĩa trong lớp Module được sử dụng để tạo các method động. Để sử dụng define_method, bạn gọi nó với tên của method mới và một block với tham số của block chính là tham số của method mới:

    class Developer
      define_method :frontend do |*my_arg|
        my_arg.inject(1, :*)
      end

      class << self
        def create_backend
          singleton_class.send(:define_method, "backend") do
            "Born from the ashes!"
          end
        end
      end
    end

    developer = Developer.new
    p developer.frontend(2, 5, 10)
    # => 100

    p Developer.backend
    # undefined method 'backend' for Developer:Class (NoMethodError)

    Developer.create_backend
    p Developer.backend
    # "Born from the ashes!"

Đây là ví dụ sẽ thấy sự hữu ích của define_method:

    class Developer

      def coding_frontend
        p "writing frontend"
      end

      def coding_backend
        p "writing backend"
      end

    end

    developer = Developer.new

    developer.coding_frontend
    # "writing frontend"

    developer.coding_backend
    # "writing backend"

Đoạn code này không DRY nhưng khi sử dụng define_method thì nó có thể DRY:

    class Developer

      ["frontend", "backend"].each do |method|
        define_method "coding_#{method}" do
          p "writing " + method.to_s
        end
      end

    end

    developer = Developer.new

    developer.coding_frontend
    # "writing frontend"

    developer.coding_backend
    # "writing backend"

Đoạn code trên đã tốt hơn nhiều nhưng vẫn chưa hoàn hảo, nếu chúng ta muốn add thêm một method mới coding_debug, chúng ta cần đặt “debug” vào trong mảng. Nhưng sử dụng method_missing thì chúng ta có thể fix nó như sau:

    class Developer

      def method_missing method, *args, &block
        return super method, *args, &block unless method.to_s =~ /^coding_\w+/
        self.class.send(:define_method, method) do
          p "writing " + method.to_s.gsub(/^coding_/, '').to_s
        end
        self.send method, *args, &block
      end

    end

    developer = Developer.new

    developer.coding_frontend
    developer.coding_backend
    developer.coding_debug

Đoạn mã trên sẽ định nghĩa các method có tên dạng coding_*** trong method_missing bằng define_method.

VI, Kết luận

Trên đây là những kiến thức cơ bản khi bạn mới làm quen với metaprogramming trong Ruby.Việc sử dụng metaprogramming sẽ giúp code của chúng ta trở nên thông thoáng và đẹp hơn. Thanks for read!