Sansanアーキテクチャ史 / A Brief History of Sansan's Architecture

13d936e697fe0f4fa96f926d0a712f6c?s=47 Sansan
October 23, 2019

Sansanアーキテクチャ史 / A Brief History of Sansan's Architecture

■イベント
Sansan Builders Box 2019
https://jp.corp-sansan.com/sbb2019/

■登壇概要 

タイトル:Sansanアーキテクチャ史

登壇者: 
プロダクト開発部 荒川聖悟

▼Sansan Builders Box 

https://buildersbox.corp-sansan.com/

13d936e697fe0f4fa96f926d0a712f6c?s=128

Sansan

October 23, 2019
Tweet

Transcript

  1. Sansanアーキテクチャ史

  2. 荒川 聖悟(Shogo Arakawa) プロダクト開発部 サーバサイドエンジニア @adsholoko ad-sho-loko 2018年11月 Sansan株式会社入社 SansanのAPI開発を主に業務では行ってい

    る。好きな領域は低レイヤ。
  3. Sansan Builders Box Agenda - Sansanとは? - Sansanのアーキテクチャの歴史 > v1/v2/v3の変遷

    > それぞれの学び
  4. Sansan Builders Box Sansanとは? AI×人によるデータ入力 データベース化 マルチデバイスで活用 他システムとの連携 お客様 AIなどのテクノロジーとオペレーターに

    よる多重入力を活用し、高い入力精度を実現 名刺をスキャン 4 3 2 1 Sansanスキャナ スマートフォンアプリ
  5. Sansan Builders Box Sansanとは? 12年間の歴史のあるアプリケーション

  6. Sansan Builders Box Sansanとは?

  7. v1

  8. Sansan Builders Box v1 :各レイヤの役割 BLL DAL WebForms ビジネスロジック担当のレイヤ BLLと対応するUIのレイヤ

    SQLなどのデータアクセスを担当するレイヤ
  9. Sansan Builders Box v1 : コードの雰囲気( BLL ) BLL DAL

    WebForms // CardInformationBll.cs public void DeleteCard(string cardId) { try { connLK.OpenConnection(); connLK.BeginTransaction(); CardInformationDal dal = new CardInformationDal(); DataTable dt = dal.GetCard(cardId); if (dal.DeleteStack(dt.Rows[0]) { throw new NotExpectedResultException(message); } connLK.Commit(); // ロールバック処理などは省略 } }
  10. Sansan Builders Box v1 : コードの雰囲気( BLL ) BLL DAL

    WebForms // CardInformationBll.cs public void DeleteBizCard(string cardId) { try { connLK.OpenConnection(); connLK.BeginTransaction(); CardInformationDal dal = new CardInformationDal(); DataTable dt = dal.GetCard(cardId); if (dal.DeleteStack(dt.Rows[0]) { throw new NotExpectedResultException(message); } connLK.Commit(); // ロールバック処理などは省略 } } トランザクション管理
  11. Sansan Builders Box v1 : コードの雰囲気( BLL ) BLL DAL

    WebForms // CardInformationBll.cs public void DeleteBizCard(string cardId) { try { connLK.OpenConnection(); connLK.BeginTransaction(); CardInformationDal dal = new CardInformationDal(); DataTable dt = dal.GetCard(cardId); if (dal.DeleteStack(dt.Rows[0]) { throw new NotExpectedResultException(message); } connLK.Commit(); // ロールバック処理などは省略 } } DALと1対1で対応
  12. Sansan Builders Box v1 : コードの雰囲気( DAL ) BLL DAL

    WebForms // CardInformationDal.cs public void DeleteStack(string cardId) { var sql = new StringBuilder(); sql.AppendFormat("update {0} c", Const.TableName); sql.AppendFormat(" set delete_flag = :{0}", Const.Delete); sql.AppendFormat(" where c.{0} = :{1}", Const.CardId, cardId); return DBHelper.ExecuteQuery(sql.ToString()); } 手動でクエリ結合
  13. Sansan Builders Box v1の良い点 - とにかくシンプルに書き下せる - 良くも悪くもほとんど共通化を意識していなかった - 画面と業務ロジックの結びつきが明確

    - 1対1で対応するので当たり前
  14. Sansan Builders Box v1の悪いところ - 画面駆動アーキテクチャになりがち - 業務ロジックの使いまわしが効かなくて辛い - 各レイヤの責務があいまい

    - 雰囲気でレイヤを使っていて実装に統一性が取れず辛い
  15. v2

  16. Sansan Builders Box v2 : 概要 Domain Model Repository Service

    ビジネスロジックを担当するレイヤ モデルとO/Rマッピングを担当するレイヤ データアクセスの抽象化レイヤ
  17. Sansan Builders Box v2 : 概要 Domain Model Repository Service

    ビジネスロジックを担当するレイヤ モデルとO/Rマッピングを担当するレイヤ データアクセスの抽象化レイヤ 用語的には いわゆるDDDライクな アーキテクチャ
  18. Sansan Builders Box v2 : アーキテクチャ図 ICardDeleteService (Interface) Service Domain

    Model Repository CardDelete Service Card CardRepository ICardRepository (Interface)
  19. Sansan Builders Box v2 : コードの雰囲気(Service) Domain Model Repository Service

    // CardDeleteServcie.cs public CardDeleteResult Delete(string cardId) { try { var target = StackRepository .Where((stack, card) => card.Id == cardId) .Select((stack, card) => new object[] { cardId }) .SingleOrDefault(); Transaction.Begin(); target.Delete(); CardRepository.Save(new[] { target }); Transaction.Commit(); return new CardDeleteResult { Status = CardDeleteResult.DeleteStatus.Suc cess, Target = target }; } }
  20. Sansan Builders Box v2 : コードの雰囲気(Service) Domain Model Repository Service

    // CardDeleteServcie.cs public CardDeleteResult Delete(string cardId) { try { var target = CardRepository .Where((stack, card) => card.Id == cardId) .Select((stack, card) => new object[] { cardId }) .SingleOrDefault(); Transaction.Begin(); target.Delete(); CardRepository.Save(new[] { target }); Transaction.Commit(); return new CardDeleteResult { Status = CardDeleteResult.DeleteStatus.Suc cess, Target = target }; } } O/R マッピング
  21. Sansan Builders Box v2 : コードの雰囲気(Service) Domain Model Repository Service

    // CardDeleteServcie.cs public CardDeleteResult Delete(string cardId) { try { var target = CardRepository .Where((stack, card) => card.Id == cardId) .Select((stack, card) => new object[] { cardId }) .SingleOrDefault(); Transaction.Begin(); target.Delete(); CardRepository.Save(new[] { target }); Transaction.Commit(); return new CardDeleteResult { Status = CardDeleteResult.DeleteStatus.Suc cess, Target = target }; } } Repositoryが抽象化!
  22. Sansan Builders Box v2 : コードの雰囲気(Domain Model) Domain Model Repository

    Service // Card.cs [TableName("trn_card")] public class Card { [ColumnName("id", ColumnType.VarChar, 10)] public string Id { get; set; } ... } テーブル名を指定 カラムの情報を指定
  23. Sansan Builders Box v2 : コードの雰囲気(Repository) Domain Model Repository Service

    foreach (var custom in CustomUpdate) { if (custom is string) { var customString = (string)custom; switch (customString) { case "Address.PostalCode": addressSetClause.AppendFormat("{0}postal_code = :POSTAL_CODE ", comma); var postalCodeParam = DbParameterFactory.Create(card.Address.PostalCode); postalCodeParam.ParameterName = ":POSTAL_CODE"; paramList.Add(postalCodeParam); if (string.IsNullOrEmpty(comma)) comma = ","; break; case "Address.Prefecture": addressSetClause.AppendFormat("{0}address_prefecture = :ADDRESS_PREFECTURE ", comma); var prefectureParam = DbParameterFactory.Create(card.Address.Prefecture); prefectureParam.ParameterName = ":ADDRESS_PREFECTURE"; paramList.Add(prefectureParam); if (string.IsNullOrEmpty(comma)) comma = ","; break; case "Address.City": addressSetClause.AppendFormat("{0}address_city = :ADDRESS_CITY ", comma); var cityParam = DbParameterFactory.Create(card.Address.City); cityParam.ParameterName = ":ADDRESS_CITY"; paramList.Add(cityParam); if (string.IsNullOrEmpty(comma)) comma = ","; break; case "Address.Street": addressSetClause.AppendFormat("{0}address_street = :ADDRESS_STREET ", comma); var streetParam = DbParameterFactory.Create(card.Address.Street); streetParam.ParameterName = ":ADDRESS_STREET"; paramList.Add(streetParam); if (string.IsNullOrEmpty(comma)) comma = ","; break; case "Address.Building": addressSetClause.AppendFormat("{0}address_building = :ADDRESS_BUILDING ", comma); var buildingParam = DbParameterFactory.Create(card.Address.Building); buildingParam.ParameterName = ":ADDRESS_BUILDING";
  24. Sansan Builders Box v2 : コードの雰囲気(Repository) Domain Model Repository Service

    foreach (var custom in CustomUpdate) { if (custom is string) { var customString = (string)custom; switch (customString) { case "Address.PostalCode": addressSetClause.AppendFormat("{0}postal_code = :POSTAL_CODE ", comma); var postalCodeParam = DbParameterFactory.Create(card.Address.PostalCode); postalCodeParam.ParameterName = ":POSTAL_CODE"; paramList.Add(postalCodeParam); if (string.IsNullOrEmpty(comma)) comma = ","; break; case "Address.Prefecture": addressSetClause.AppendFormat("{0}address_prefecture = :ADDRESS_PREFECTURE ", comma); var prefectureParam = DbParameterFactory.Create(card.Address.Prefecture); prefectureParam.ParameterName = ":ADDRESS_PREFECTURE"; paramList.Add(prefectureParam); if (string.IsNullOrEmpty(comma)) comma = ","; break; case "Address.City": addressSetClause.AppendFormat("{0}address_city = :ADDRESS_CITY ", comma); var cityParam = DbParameterFactory.Create(card.Address.City); cityParam.ParameterName = ":ADDRESS_CITY"; paramList.Add(cityParam); if (string.IsNullOrEmpty(comma)) comma = ","; break; case "Address.Street": addressSetClause.AppendFormat("{0}address_street = :ADDRESS_STREET ", comma); var streetParam = DbParameterFactory.Create(card.Address.Street); streetParam.ParameterName = ":ADDRESS_STREET"; paramList.Add(streetParam); if (string.IsNullOrEmpty(comma)) comma = ","; break; case "Address.Building": addressSetClause.AppendFormat("{0}address_building = :ADDRESS_BUILDING ", comma); var buildingParam = DbParameterFactory.Create(card.Address.Building); buildingParam.ParameterName = ":ADDRESS_BUILDING"; リポジトリが”超”肥大化 ※ 1メソッド内の処理
  25. Sansan Builders Box v2 : コードの雰囲気(Repository) ファイルの肥大化 モジュールの肥大化 × =

    これじゃない感….
  26. その結果…

  27. DDDの導入に失敗した By 社内のエンジニア ※あくまでも個人の感想です

  28. Sansan Builders Box v2の課題と学び - Repositoryの肥大化と実装難易度の高さ - Domain ModelごとにRepositoryができる -

    一応レイヤごとに分割したが、業務ごとに分割できずに、肥大化する - 「共通化」の認識を共通化させることが出来なかった - フロントエンド固有のロジックが共通されたりする(実はAPI専用メソッドなど) - エンジニア全員に思想と実装を浸透させることはさらに難しい - とにかく遅い!パフォーマンス出ない! - 発行されるクエリが多い&謎すぎてチューニング作業が難航
  29. Sansan Builders Box v2の良かった点 - 画面駆動からは脱却できた - 手探りながらも業務ロジックを共通化できつつあった - まれに効率的(ほとんど聞いたことはない)

  30. v3

  31. Sansan Builders Box v3 : アーキテクチャ図 UI Domain Controller Application

    CardDelete Service ICardDelete DataAccessor Card QueryService Controller QueryService API Web CardDelete DataAccessor ICardDelete Service Infrastructure
  32. Sansan Builders Box v3 : アーキテクチャ図 Domain Controller Application CardDelete

    Service ICardDelete DataAccessor Card QueryService Controller QueryService API Web CardDelete DataAccessor ICardDelete Service Infrastructure フロントエンド固有のクエリを発行 (基本SELECT文のみ) UI
  33. Sansan Builders Box v3 : アーキテクチャ図 Domain Controller Application CardDelete

    Service ICardDelete DataAccessor Card QueryService Controller QueryService API Web CardDelete DataAccessor ICardDelete Service Infrastructure 1対1で対応 UI
  34. Sansan Builders Box v3 : アーキテクチャ図 Domain Controller Application CardDelete

    Service ICardDelete DataAccessor Card QueryService Controller QueryService API Web CardDelete DataAccessor ICardDelete Service Infrastructure ①共通のクエリを発行する UI
  35. Sansan Builders Box v3 : アーキテクチャ図 Domain Controller Application CardDelete

    Service ICardDelete DataAccessor Card QueryService Controller QueryService API Web CardDelete DataAccessor ICardDelete Service Infrastructure ②テクノロジ依存を隠蔽化 UI
  36. Sansan Builders Box v3 : アーキテクチャ図(業務レイヤでの分割) UI Controller Application QueryService

    card company admin xxxService xxxService xxxService
  37. Sansan Builders Box v3 : コードの雰囲気( Application ) Domain Infrastructure

    Application // BizCardService.cs public DeleteResult DeleteCard(string cardId) { var card = CardDataAccessor.GetDeleteTargetCard(cardId); if (card == null) { return DeleteResult.NotFound; } using (var tx = TransactionScopeFactory.Create()) { CardDataAccessor.DeleteStack(card.Id); tx.Complete(); } return DeleteResult.Success; }
  38. Sansan Builders Box v3 : コードの雰囲気( Application ) Domain Infrastructure

    Application // BizCardService.cs public DeleteResult DeleteCard(string cardId) { var card = CardDataAccessor.GetDeleteTargetCard(cardId); if (card == null) { return DeleteResult.NotFound; } using (var tx = TransactionScopeFactory.Create()) { CardDataAccessor.DeleteStack(card.Id); tx.Complete(); } return DeleteResult.Success; } トランザクション管理
  39. Sansan Builders Box v3 : コードの雰囲気( Domain ) Domain Infrastructure

    Application // IBizCardDataAccessor.cs public interface IBizCardDataAccessor { void DeleteCard(string cardId) } インターフェースを切る
  40. Sansan Builders Box v3 : コードの雰囲気( Infrastructure ) Domain Application

    // BizCardDeleteDataAccessor.cs public void DeleteCard(string cardId) { var sql = @" update lkweb.trn_card set delete_flag = 1 where card_id = @cardId"; using (var conn = DbConnectionUtility.GetDataDbConnection(ucompanyId)) { conn.Execute(sql, new { cardId }); } } Infrastructure
  41. Sansan Builders Box v3 : コードの雰囲気( Infrastructure ) Domain Application

    // BizCardDeleteDataAccessor.cs public void DeleteStack(string cardId) { var sql = @" update lkweb.trn_card set delete_flag = 1 where card_id = @cardId"; using (var conn = DbConnectionUtility.GetDataDbConnection(ucompanyId)) { conn.Execute(sql, new { cardId }); } } SQLを直で指定する Infrastructure
  42. Sansan Builders Box v3 : コードの雰囲気( Infrastructure ) Domain Application

    // BizCardDeleteDataAccessor.cs public void DeleteCard(string cardId) { var sql = @" update lkweb.trn_card set delete_flag = 1 where card_id = @cardId"; using (var conn = DbConnectionUtility.GetDataDbConnection(ucompanyId)) { conn.Execute(sql, new { cardId }); } } 実装クラス Infrastructure
  43. Sansan Builders Box v3の良かった点 - シンプルさと共通化のバランスが取れている - 誰が見ても理解しやすい構造になっている - さらに部内のエンジニア内でもそれなりに浸透していると感じる

    - 無駄な抽象化は避けている - むやみにインターフェース切らない、など
  44. Sansan Builders Box v3の課題とこれから - モジュールの場所や実装は未だに悩むことがある - 悩むべくして悩んでるとも言える - v1

    / v2共存問題
  45. Sansan Builders Box 学びのまとめ - 時代とともに流行のアーキテクチャは移り変わる - いわゆる「レガシー」もこうして生まれる - アーキテクチャの浸透は想像以上に難しい

    - 弊部では新入社員向けにアーキテクチャに関する講座を有志で実施している - プラットフォームとアプリケーションに合ったアーキテクチャを選ぶ
  46. ご清聴ありがとうございました @adsholoko

  47. None