Closed18

NestJSに慣れた仙人がRuby(Rails)をキャッチアップする様

おひたしおひたし
  • bundleコマンドのtips: 通常はrailsを打つ場合はbundle execを利用しなければならないが、direnvでgemの実行ファイルのPATHを指定することでbundle execを利用せずともシェルがコマンドを見つけることができる。
    • ex. bundle exec rails serverrails serverだけで済むので楽
    • bundle execはRubyのコマンドを実行する際に、そのコマンドがgemfile.lockで指定されたバージョンンのgemを使用することを保証するためのコマンド。
  • bundle configでgemをbundle installでインストールする時の設定を色々いじれる。
    • 設定スコープ(--local、--global、--system)によって設定が保存される場所が異なる。
    • --localは現在のプロジェクトのディレクトリ(具体的には.bundle/config)、--globalはユーザーディレクトリ(具体的には~/.bundle/config)、--systemはシステム全体(具体的には/usr/local/bundle/configや/opt/rubies/ruby-x.y.z/lib/ruby/gems/x.y.z/configなど)
    • ex. https://zenn.dev/orange634nty/articles/d3dd8ec1df63ec (--with-cflags=-Wno-error=implicit-function-declarationで、ビルド時のc99エラーを無視する、など)
  • 慣れ親しんだnode.jsとrubyの依存管理の違いは、node.jsはnpm(or yarn)単体でパッケージのダウンロード・依存関係管理ができるのに対して、rubyはパッケージのinstall自体はgem, 依存管理はbundleと分かれている。
おひたしおひたし

@@でクラス変数(そのクラス内やサブクラス内をスコープとして使える変数)を利用できる。ただスコープが広く意図せずして参照・変更されるリスクがあり、インスタンス変数(@hoge)を利用するのが良さげ。

class MyClass
  @@class_variable = "Hello, world!"

  def self.print_class_variable
    puts @@class_variable
  end

  def print_class_variable
    puts @@class_variable
  end
end

obj = MyClass.new
MyClass.print_class_variable  # => "Hello, world!"
obj.print_class_variable  # => "Hello, world!"

おひたしおひたし
  • クラス内でclass << self(クラスメソッド)を記述できる
    • 利用例:
    • ファクトリーパターン
    • シングルトンパターン
    • ↑はイマイチピンと来ていない。クラスメソッドを使わなくてもいい気がするが。
  • new.Animal("Dog")new("Dog")と省略できる。
  • クラスメソッド内で呼び出せるのはクラスメソッドだけ。インスタンスメソッドは直接呼び出せない
  • クラス内でそのクラス自体をインスタンス化するという挙動ができる。インスタンス化すればインスタンスメソッドは呼び出せる。

ファクトリーパターンのコード:

class Animal
  attr_accessor :name

  def initialize(name)
    @name = name
  end

  class << self
    def create_dog
      new("Dog")  # Animalクラスの新しいインスタンスを生成
    end

    def create_cat
      new("Cat")  # Animalクラスの新しいインスタンスを生成
    end
  end
end

dog = Animal.create_dog
cat = Animal.create_cat
puts dog.name  # => "Dog"
puts cat.name  # => "Cat"
おひたしおひたし

イマイチクラスメソッドを使うメリットがわかっていない。。使わなくても色々書けそうだが。

おひたしおひたし

(余談)
クラスメソッドはシングルトンメソッド(特例メソッド)と言われているが、これはRuby固有の概念でデザインパターンのシングルトンとは関係ない。(紛らわしい...)

おひたしおひたし

JSでは!0はtrueだったが、Rubyではfalse。これはnil以外はtrueという性質があるため。truthy, falthyがわかりやすくてgood

おひたしおひたし

モジュールについて

おひたしおひたし

モジュールはクラスをまたがって共通化したい処理に使う。多重継承を避けるため。クラスにモジュールをincludeすることをmix-inという。

無関係なクラスで継承を使うのはだめ。なぜかというと、継承はis-aの関係が成り立たないのであれば使わないのが吉であるため。「has-a」(「〜が〜を持っている」という)関係を示したい場合はコンポジション(クラスが他のクラスのインスタンスを属性として持つ)を使い、複数のクラス間で特定の振る舞いを共有したい場合はモジュールを使う方がメンテしやすくなる。

おひたしおひたし

モジュール内でinclude先のメソッドを定義することもできる。これは「メソッドを実行する瞬間にそのメソッドが呼び出せればいい」という思想がRubyにはあるため。

module Taggable
  def price_tag:
    "#{price}円"
  end

class Product
  include Taggable
 
  def price
    1000
  end
end

product = Product.new
product.price_tag # => 1000円
おひたしおひたし

例外処理のJS / Ruby比較

おひたしおひたし

JS
例外処理: try-carch-finnaly
例外発生: throw

const hoge = () => {
  try {
    fuga()
  } catch (e) { 
    throw new Error()
  } finally {
    console.log("process is done")
  }
}

Ruby
例外処理: begin-rescue-ensure
例外生成: raise

def hoge
  begin
    fuga()
  rescue => e
    raise StandardError.new
  ensure
    puts "process is done"
  end
end
おひたしおひたし

Rubyでオブジェクト指向プログラミング

おひたしおひたし

ダックタイピング

def quack(duck)
  duck.quack
end

class Duck
  def quack
    puts 'Quack!'
  end
end

class Dog
  def quack
    puts 'Woof!'
  end
end

quack(Duck.new)  # Prints: 'Quack!'
quack(Dog.new)   # Prints: 'Woof!'

quack関数は、渡された引数のオブジェクトがquackメソッドを持っている前提で定義されている。このように振る舞いに着目したプログラミング方法をダックタイピングと言う。渡された引数は、Duckクラスでなくとも良い。Dogクラスのインスタンスでも、それがquackメソッドを持っていれば問題ない。

おひたしおひたし

ダックタイピングを使ってDIP

DIP = SOLIDのD。 依存関係逆転の原則(Dependency Inversion Principle)。

概要:

  1. 高レベルのモジュールは低レベルのモジュールに依存すべきではない。両方とも抽象に依存すべきである。
  2. 抽象は詳細に依存すべきではない。詳細が抽象に依存すべきである。
class XMLParser
  def parse
    puts 'Parsing XML...'
    # XML parsing logic here
  end
end

class JSONParser
  def parse
    puts 'Parsing JSON...'
    # JSON parsing logic here
  end
end

Documentクラスを作成。このクラスは具体的なパーサーに依存せず、どんな種類のパーサーでも受け入れることができる。

class Document
  def initialize(parser)
    @parser = parser
  end

  def parse
    @parser.parse
  end
end

このようにすると、Documentクラスは具体的なパーサークラス(XMLParserJSONParser)に依存せず、代わりにparseメソッドを持つ任意のオブジェクト(これが抽象に該当)に依存する。

使用例:

xml_parser = XMLParser.new
document1 = Document.new(xml_parser)
document1.parse
# Output: Parsing XML...

json_parser = JSONParser.new
document2 = Document.new(json_parser)
document2.parse
# Output: Parsing JSON...

このように、Documentクラスは具体的なクラスに依存せず、代わりに抽象(ここではparseメソッドを持つオブジェクト)に依存する。これにより、新たな種類のパーサーを追加するときもDocumentクラスは変更する必要がなく、コードの柔軟性と再利用性が高まる。

おひたしおひたし

モジュールを使ってインターフェースを模倣する

これとかが参考になる。

モジュールを使ってインターフェースを模倣することができる。"契約"の形でメソッドを定義(契約を守らないクラスがあった場合エラーを出す)し、そのモジュールを具体的なクラスでincludeしメソッドを再定義する。

ダックタイピングに加えて、インターフェースを模倣しDIPを実現してみる。

まず、インターフェースを表すモジュールを定義する:

module Parser
  def parse
    raise NotImplementedError, 'You must implement the parse method'
  end
end

次に、このParserモジュールを各具体的なクラスでincludeする:

class XMLParser
  include Parser

  def parse
    puts 'Parsing XML...'
    # XML parsing logic here
  end
end

class JSONParser
  include Parser

  def parse
    puts 'Parsing JSON...'
    # JSON parsing logic here
  end
end

このようにすることで、各具体的なパーサークラスはParserモジュールの契約(parseメソッドを実装する)を満たすことが期待される。

最後に、依存関係を持つクラス(ここではDocumentクラス)を作成する。このクラスは具体的なパーサークラスに依存せず、Parserモジュールの契約を満たす任意のオブジェクトに依存する:

class Document
  def initialize(parser)
    unless parser.is_a?(Parser)
      raise ArgumentError, 'Invalid parser'
    end

    @parser = parser
  end

  def parse
    @parser.parse
  end
end

ここでは、初期化時にパーサーオブジェクトがParserモジュールの契約を満たしていることをチェックする。これにより、Documentクラスのparseメソッドは、パーサーオブジェクトがParserモジュールのparseメソッドを実装していることを確信できる。

個人的感想→ビルドする前にコンパイラに怒られるって言うのがインターフェースのメリットだと思う。模倣したインターフェースでは、仮に変なメソッドを定義していたとしても、実際にデプロイするまで気づかないというデメリットがある。不具合としてのエラーではなく仕様としてのエラーを出すというのも冗長な気がする・・・。

おひたしおひたし

ブロックは、さまざまなデータを持つ引数として用いられることがある。定義する関数は&blockで引数を受け取る

おひたしおひたし

スーパークラスで、下位クラスのクラスメソッドを呼び出すと、スーパークラスに書かれたコードが実行されたあとクラスメソッドが実行される。これが結構わかりにくい上に、実務のコードで結構見かけたりする。。。

下記ChatGPT先生に生成してもらった例

====================================================

モジュールを使ってメソッドを共有し、別のクラスでそのメソッドを使用する例を示します。具体的には、クラスが持つ特定の属性の値をキャッシュし、それを取得するクラスメソッドを定義します。

まず、以下のようなモジュールを定義します。

module Cachable
  extend ActiveSupport::Concern

  class_methods do
    def cache_attribute(attribute)
      @cached_attribute = attribute
    end

    def get_cached
      @cached_attribute
    end
  end
end

ここで、cache_attributeはクラス変数@cached_attributeに特定の属性を設定するクラスメソッドで、get_cachedはその設定した属性を取得するメソッドです。

次に、このCachableモジュールをインクルードするクラスを定義します。

class MyClass
  include Cachable
  cache_attribute :some_attribute
end

ここで、MyClassクラスがロードされると同時に、cache_attribute :some_attributeが実行され、@cached_attribute:some_attributeが設定されます。

そして、以下のようにget_cachedメソッドを使用することで、設定した属性を取得することができます。

puts MyClass.get_cached  # => :some_attribute

こうした方法で、モジュールを使用してクラスに振る舞いを追加し、その振る舞いをそのクラスの一部として利用することができます。

このスクラップは2023/08/24にクローズされました