[Rails]STI(단일 테이블 상속) 및 메타 프로그래밍 DRY

개요


복제 코드가 증가하기 쉬운 샘플 응용의 디자인을 예로 들다
STI(단일 테이블 상속)와 메타프로그램 설계를 사용하여 DRY(중복 제외)를 시도해 봅니다.

제재


사용자가 유지하고 있는 곡을 분류별로 관리하는 프로그램입니다.
사용자 페이지에서 등록 곡을 카테고리별로 나열할 수 있습니다(더 나아가 CRUD).
그런 인상이네요.
kidachi_さん
あなたの登録曲一覧
  Rock
    ほげRock
    ふがRock
  Pops
    未登録です。    
  Jazz
    ふーJazz
    ばーJazz
아무 생각 없이 하면 록/pops/jazz 각자의 모형, 보기, 컨트롤러에
유사한 기술이나 복제품이 늘어날 것 같으십니까?
그렇다면 이를 막기 위해서는 먼저 STI부터 시작해야 한다.
(※ 추서)
사실 상술한 요건일 뿐이라면
user 테이블, 뮤직 테이블, genre 테이블만 준비되어 있습니다.user has_many genres through musics의assosiation에서도 실현 가능
(원래 록/pops/jazz 모형을 준비할 필요가 없다).
실제로'앞으로 각 genre에 여러 가지 특유의 처리를 추가하고 싶다'고 가정하면
각genre의 개별 모델을 준비하는 것을 전제로 한다.
일이 좀 있는 예가 좋지 않은 것 같아서 죄송합니다.

STI(Single Table Inheritance/단일 테이블 상속)는


상속
수퍼 클래스와 비공통 프로젝트의 하위 클래스를 사용하여
코드의 중복을 방지하는 구조이지만 주의하십시오
STI도db(표) 디자인에 적용됩니다.
단일 테이블 상속 이름은 그대로 유지됩니다.

구체적 예


방금 전의 악곡 관리 응용 탁자를 대략적으로 설계해 보았다
나는 다음과 같은 인상이 있을 것이라고 생각한다.

괜찮은데 모델/테이블에 중복된 메시지가 있어요.
늘어난 것 같은데.

따라서 단일 테이블 상속


이렇게

슈퍼클래스를 준비해서 계승 메커니즘을 사용한다는 개념과 똑같다.
언뜻 보기에는 별다른 것이 없는 것 같지만, 사실은
  • rock,pops,jazz(etc)표는 실제 존재하지 않습니다
  • 모든 데이터를 뮤직 테이블에 저장
  • 이런 특징이 있다.
    아무렇지 않은 듯 뮤직 테이블에'type'이라는 열이 추가됐습니다.
    록/pops/jazz 같은 애들 정보가 여기 있을 거야.
    또 록 테이블에서만 얻을 수 있는 정보를 원한다면
    사전에 그것을 뮤직으로 정의했다.

    모델 설치


    STI를 사용하더라도 모델은 일반적인 상속 형식으로만 이루어진다.
    music.rb
    class Music < ActiveRecord::Base
      belongs_to :user
      validates :name, presence: true
      validates :artist, presence: true
    end
    
    rock.rb
    class Rock < Music
    end
    
    주의해야 할 것은
    아이가 베이스가 아니라 부모 뮤직을 물려받았어요.
    대충 그렇겠지.
    그나저나 부모님이 설정한 관계와validates는 모두
    자반으로 물려받을 수도 있고.
    STI에 관해서는 여기까지입니다.
    결론, STI를 이용하여db를 생성할 때
  • 모표(이번은 뮤직)에 type열을 설정합니다.
  • 자식 중의 특정한 열이 필요하면 부모에게도 남겨준다
  • (이후 보통 계승의 형식으로 모형을 제작한다)
  • 그냥 이렇게 됐으면 좋겠어요.

    Cntroller 설치 및 메타프로그램 설계


    users/show로 사용자의 곡을 일람하는 방법입니다.
    즉 show에 @rock/@pops/@jazz 세 개를 설정하고 싶다는 것이다.
    여기서 원 프로그래밍을 활용해 보자.
    users_controller.rb
    class UsersController < ApplicationController
    
      GENRE = [
          'rock',
          'pops',
          'jazz'
      ]
    
      def show
        @user = User.find(current_user.id)
        set_music_by_genre
      end
    
      private
        def set_music_by_genre
          @music_list = Array.new
          GENRE.each do |music|
            key = "@#{music}"
            if @user.send(music).nil?    # 1
              music = music.gsub(/\b\w/) { |s| s.upcase }    # 2
              value = self.class.const_get(music).new    # 3
            else
              value = @user.send(music)    # 4
            end
            instance_variable_set(key, value)    # 5
            @music_list << instance_variable_get(key)    # 6
          end
        end
    end
    

    set_music_by_genre 설명


    1. if @user.send(music).nil?


    send를 사용하여 @user에 대한 메시지를 동적으로 수신하고 있습니다.
    즉, @user입니다.rock/@user.pops/@user.동적 생성 jazz
    여기, @user rock/pops/jazz 각자의 데이터가 있는지 여부
    , 기존 데이터를 저장한 상태에서 호출(#2, 3)
    저장하지 않으면 새 객체가 생성됩니다(#4).

    2. music.gsub(/\b\w/) { |s| s.upcase }


    동적으로 Rock/Pops/Jazz 문자열을 생성합니다.
    #3 을 사용합니다.

    3. self.class.const_get(music).new


    self.class.const_get(str)에서 문자열(str)부터 시작
    클래스나 모듈의 상수를 얻을 수 있습니다.
    즉, #2를 생성하는 "Rock"/"Pops"/"Jazz"
    각각 new인 클래스로 변환(Rock/Pops/Jazz).

    4. @user.send(music)


    #1과 마찬가지로 동적으로@user 메시지를 보냅니다.

    5. instance_variable_set(key, value)


    인스턴스 변수를 동적으로 생성합니다.
    @rock/@pops/@jazz는 각각 기존 대상이나 새 대상을 설정합니다.

    6. @media_list << instance_variable_get(key)


    뷰에서 사용하기 위해
    [
      #<Rock id: nil, type: "Rock", created_at: nil, updated_at: nil>, 
      #<Pops id: nil, type: "Pops", created_at: nil, updated_at: nil>, 
      #<Jazz id: nil, type: "Jazz", created_at: nil, updated_at: nil>
    ]
    
    이 대상을 포함하는 그룹을 준비하십시오.

    View


    views/users/show.html.erb
      <% @music_list.each do |music| %>
        <% if music.try(:id).nil? %>
          <!-- 新規登録フォーム -->
        <% else %>
           <!-- 登録済み情報の表示 -->
        <% end %>
      <% end %>
    
    show 설정@music_list을 바탕으로 instancevariable_get 누르기()
    동적으로 대상을 가져오고 대상에 따라 디스플레이 처리를 합니다.

    Controller


    새 등록 형식에서 버려진 데이터에 따라create를 하는 곳.
    여기도 셀프야.class.const_get ()에 대응하는 하위 클래스
    클래스를 동적으로 설정하고 저장합니다(Rock/Pops/Jazz).
    music_controller.rb
    class MusicController < ApplicationController
      before_action :set_music_info
      before_action :music_params
    
      def create
        const_name = @music_name.gsub(/\b\w/) { |s| s.upcase }
        # サブクラスごとのオブジェクトを初期化
        @music = self.class.const_get(const_name)
        @music.new(music_params)
        respond_to do |format|
          if @music.save!
            ~
          end
        end
      end
    
      private
        # strong_parameters
        def music_params
          params.require(@music_name).permit(:user_id, :name, :email, :password)
        end
    
    end
    
    
    rock_controller.rb
    class RockController < MusicController
      private
        set_music_info
          @music_name = "rock"
        end
    end
    
    STI를 사용해서 그런지 뮤직 테이블, 뮤직이 준비되어 있습니다.controller에서
    모든 하위 클래스(rock/pops/jazz)의 save 처리도 매우 직관적으로 진행된다.
    ※ 개인적으로 STI는 특별한 구조라기보다는

    관계를 계승하고자 하는 모델에 맞는 표 인터페이스


    내 생각엔 뭐가 아닐까?

    완성


    그러면 show 페이지의 악곡 일람과 악곡이 새로 등록된 논리를 사용할 수 있습니다.
    이런 금속 골격을 준비해 뮤직의 종류를 늘릴 때
    hoge_controller.rb와 GENRE 리스트에 새로운 종류를 추가하면 됩니다.
    (당연히 controller/model 파일이나routes.rb 추기를 준비해야 하며, 별도로 준비해야 한다)
    classic_controller.rb
    class ClassicController < MusicController
      private
        set_music_info
          @music_name = "classic"
        end
    end
    
    users_controller.rb
    class UsersController < ApplicationController
    
      GENRE = [
          'rock',
          'pops',
          'jazz',
          'classic'
      ]
    
    ※ GENRE 배열은 설정 파일로 외부화하는 것이 좋습니다
    이런 느낌.
    길어지니까 그만하자
    다른 논리 부분도 마찬가지로 복제 코드를 없앨 수 있다.

    참고 자료


    Rails에서 단일 테이블 상속(Single Table Inheritance)
    http://blog.matake.jp/archives/railssingle_table_inherit
    const_get (Module)
    http://ref.xaio.jp/ruby/classes/module/const_get
    instance_variable_set/get (Object)
    http://ref.xaio.jp/ruby/classes/object/instance_variable_set
    http://ref.xaio.jp/ruby/classes/object/instance_variable_get

    최후


    효율적이고 더 좋은 디자인/쓰기 방법이 있다면 꼭 가르쳐 주십시오!

    좋은 웹페이지 즐겨찾기