Share
Embed
分岐地獄を脱出した話: 抽象メソッド化と動的なインスタンス生成
Excel、CSV、PDFなど異なる形式・フォーマットの売上明細を取り込む中で、分岐や処理が複雑化していた売上計上システムを改善した話です。
Tomomi Nishino
March 02, 2025
Transcript
26 slides
-
© 2025 Wantedly, Inc. 分岐地獄を脱出した話: 抽象メソッド化と動的なインスタンス生成 Mar. 1 2025 -
Tomomi Nishino @keppagrun(24not) TokyoWoman.rb #1 -
© 2025 Wantedly, Inc. 自己紹介 西野 智美 Tomomi Nishino 所属:ウォンテッドリー株式会社
職業:バックエンドエンジニア @keppagurun(24not) 1998年にSES企業のITエンジニアとして就職。 昨年9月にウォンテッドリー株式会社に入社。 小学生と中学生の2児の母。 先週、三浦半島.rbに参加しました! -
© 2025 Wantedly, Inc. 今日話すこと 分岐地獄からの脱出のために 設計を見直した話 とある売上計上システムの • Ruby(Ruby
on Rails)の特徴である柔軟さの実務活用例 ◦ 昔々の実話ベースですが、内容は登壇向けに構成しています ◦ ソースコードも発表用に作り直しています -
© 2025 Wantedly, Inc. 前提条件 1. • システムの要件 • どのような実装か
• 運用して見えてきた問題点 目次 どう解決したか? 2. • 方針変更 • 実装に強固なルールを敷く • 何がどう変わったか まとめ 3. -
© 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 -
© 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 同じファイル形式でも データフォーマットは異なるため、 ファイル名で判別して それぞれのフォーマットに合ったパース処理を 実装していた。 -
© 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 -
© 2025 Wantedly, Inc. 1. 前提条件 運用して見えてきた問題点 同じファイルへの修正が発生しやすく、改修依頼が並行で走ると衝突しやすい。 D社さんの 対応しなきゃ
E社さんの 緊急対応! A社さんの ファイル名 変わってる 全部 Excel -
© 2025 Wantedly, Inc. 2. どう解決したか 方針の変更 • 中途半端に処理をまとめようとしない •
ファイル形式の単位でなく、明細のフォーマット単位で処理をまとめる • ファイル名でフォーマットを自動判別するのをやめる 保守のしやすさを優先し、ある程度強制的な実装ルールを作る -
© 2025 Wantedly, Inc. 2. どう解決したか 方針の変更 • Javaの抽象クラスのように、必ずオーバーライドが必要なメソッドを定 義する基底クラスは作れないか?
• 各明細ファイルのパース処理はその基底クラスを継承する実装にしたい • 上流では動的にクラス解決して、オーバーライドされたメソッドを呼び 出せば分岐がなくせるのでは? -
© 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を継承していないという点も注意が必要です。 -
© 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 -
© 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 を継承 しているクラスであるかを確認す る。 -
© 2025 Wantedly, Inc. その結果どうなったか 2. どう解決したか • 複雑な分岐がなくなってソースコードがシンプルになった •
1つのフォーマットにつき、1つのクラスを作る形になったので、開発案 件が複数ある時でも実装範囲が被りにくくなった • Base クラスを継承して、parse_to_attributes メソッドを実装するとい う明確な方針になったので新規加入メンバーでも対応しやすくなった