Upgrade to Pro — share decks privately, control downloads, hide ads and more …

分岐地獄を脱出した話: 抽象メソッド化と動的なインスタンス生成

Tomomi Nishino
March 02, 2025
82

分岐地獄を脱出した話: 抽象メソッド化と動的なインスタンス生成

Excel、CSV、PDFなど異なる形式・フォーマットの売上明細を取り込む中で、分岐や処理が複雑化していた売上計上システムを改善した話です。

Tomomi Nishino

March 02, 2025
Tweet

Transcript

  1. © 2025 Wantedly, Inc. 自己紹介 西野 智美 Tomomi Nishino 所属:ウォンテッドリー株式会社

    職業:バックエンドエンジニア @keppagurun(24not) 1998年にSES企業のITエンジニアとして就職。 昨年9月にウォンテッドリー株式会社に入社。 小学生と中学生の2児の母。 先週、三浦半島.rbに参加しました!
  2. © 2025 Wantedly, Inc. 今日話すこと 分岐地獄からの脱出のために 設計を見直した話 とある売上計上システムの • Ruby(Ruby

    on Rails)の特徴である柔軟さの実務活用例 ◦ 昔々の実話ベースですが、内容は登壇向けに構成しています ◦ ソースコードも発表用に作り直しています
  3. © 2025 Wantedly, Inc. 前提条件 1. • システムの要件 • どのような実装か

    • 運用して見えてきた問題点 目次 どう解決したか? 2. • 方針変更 • 実装に強固なルールを敷く • 何がどう変わったか まとめ 3.
  4. © 2025 Wantedly, Inc. 1. 前提条件 どのような実装か 明細ファイルの取り込み処理は、 フロントエンドからバックエンドに 販売サイトIDとファイルパスが渡される。

    ファイルパスからファイルの拡張子を抽出し ファイル形式ごとに処理を分けていた。 class SalesReportReader class << self def perform(sales_site_id, filepath) sales_site = SalesSite.find(sales_site_id) # ファイル形式ごとに parse 処理を分ける ext = File.extname(filepath).downcase case ext when ".tsv" parser = Parser::TsvFileParser.new(filepath) when ".csv" parser = Parser::CsvFileParser.new(filepath) when ".xlsx" parser = Parser::ExcelFileParser.new(filepath) else raise "Unsupported file format." end attr = parser.to_attributes attr[:sales_site_id] = sales_site.id # 売上マスタに登録 SalesImport.create!(attr) end end end sales_report_reader.rb
  5. © 2025 Wantedly, Inc. 1. 前提条件 どのような実装か module Parser class

    ExcelFileParser def initialize(filepath) @filename = filepath end def to_attributes base = File.basename(@filename, ”.xlsx”) if /\A◯◯株式会社様_\d{4}年\d{2}月\z/ =~ base to_attributes_a elsif /\A\d{6}_sales_statement\z/ =~ base to_attributes_c else raise "Unsupported file format." end end def to_attributes_a # ここはA社売上明細のパース処理 end def to_attributes_c # ここはC社売上明細のパース処理 end end end excel_file_parser.rb 同じファイル形式でも データフォーマットは異なるため、 ファイル名で判別して それぞれのフォーマットに合ったパース処理を 実装していた。
  6. © 2025 Wantedly, Inc. 1. 前提条件 運用して見えてきた問題点 明細ファイルの種類が増えると分岐が増える。 直感的に修正箇所がわかりにくくなる。 def

    to_attributes base = File.basename(@filename, ”.xlsx”) if /\A◯◯株式会社様_\d{4}年\d{2}月\z/ =~ base || /\A\d{8}_Service_E_MEISAI\z/ =~ base to_attributes_a elsif /\A\d{6}_sales_statement\z/ =~ base to_attributes_c elsif /\A◯◯株式会社_販売レポート\z/ =~ base to_attributes_d elsif /\ASalesSiteF-ご利用明細\z/ =~ base to_attributes_f else raise "Unsupported file format." end end def to_attributes base = File.basename(@filename, ”.tsv”) if /\A\d{6}-sales-report\z/ =~ base to_attributes_b elsif /\A◯◯株式会社様御中_売上報告\z/ =~ base to_attributes_g else raise "Unsupported file format." end end excel_file_parser.rb tsv_file_parser.rb
  7. © 2025 Wantedly, Inc. 2. どう解決したか 方針の変更 • 中途半端に処理をまとめようとしない •

    ファイル形式の単位でなく、明細のフォーマット単位で処理をまとめる • ファイル名でフォーマットを自動判別するのをやめる 保守のしやすさを優先し、ある程度強制的な実装ルールを作る
  8. © 2025 Wantedly, Inc. 2. どう解決したか 方針の変更 • Javaの抽象クラスのように、必ずオーバーライドが必要なメソッドを定 義する基底クラスは作れないか?

    • 各明細ファイルのパース処理はその基底クラスを継承する実装にしたい • 上流では動的にクラス解決して、オーバーライドされたメソッドを呼び 出せば分岐がなくせるのでは?
  9. © 2025 Wantedly, Inc. 実装に強制的なルールを敷く① 抽象メソッドの考え方を取り入れて、メソッドの実装を強制化する。 2. どう解決したか Rubyには抽象クラスやメソッドは用意されていないが、工夫で似たようなことは できる。

    基底クラスの抽象メソッド化したい メソッドに、Error(※)を仕込む。 module Reader class Base def initialize(filepath) @filepath = filepath end def parse_to_attributes raise NotImplementedError end end end base.rb これにより、オーバーライドされず にメソッドが呼び出されるとエラー が発生するようになる。 ※サンプルコードではNotImplementedErrorを使用していますが、Rubyにおいては本来抽象メソッドが実装されていない ことを示す用途として使うものではないようです。StandardErrorを継承していないという点も注意が必要です。
  10. © 2025 Wantedly, Inc. 実装に強制的なルールを敷く② クラス名をデータベースで管理して、constantize で動的なクラス解決を行う。 2. どう解決したか 先の手順で作った基底クラスを継承した子クラスを、販売サイトテーブルで管理す

    るように変更。 id name reader 1 販売サイトA Reader::SiteAReader 2 販売サイトB Reader::SiteBReader 3 販売サイトC Reader::SiteCReader 4 販売サイトD Reader::SiteDReader 5 販売サイトE Reader::SiteAReader
  11. © 2025 Wantedly, Inc. 実装に強制的なルールを敷く② クラス名をデータベースで管理して、constantize で動的なクラス解決を行う。 2. どう解決したか その後、抽象メソッド化した

    parse_to_attributes を実行。 class SalesReportReader class << self def perform(sales_site_id, filepath) sales_site = SalesSite.find(sales_site_id) klass = sales_site.reader.constantize raise "Invalid class." unless klass < Reader::Base attr = klass.new(filepath).parse_to_attributes attr[:sales_site_id] = sales_site.id # 売上マスタに登録 SalesImport.create!(attr) end end end sales_report_reader.rb constantizeで販売サイトテーブル から取得したreaderをクラス定数に して、klass < Reader::Base で、 それが正しく Reader:: Base を継承 しているクラスであるかを確認す る。
  12. © 2025 Wantedly, Inc. その結果どうなったか 2. どう解決したか • 複雑な分岐がなくなってソースコードがシンプルになった •

    1つのフォーマットにつき、1つのクラスを作る形になったので、開発案 件が複数ある時でも実装範囲が被りにくくなった • Base クラスを継承して、parse_to_attributes メソッドを実装するとい う明確な方針になったので新規加入メンバーでも対応しやすくなった