■イベント Sansan Builders Box 2019 https://jp.corp-sansan.com/sbb2019/
■登壇概要 タイトル:Sansanアーキテクチャ史
登壇者: プロダクト開発部 荒川聖悟
▼Sansan Builders Box https://buildersbox.corp-sansan.com/
Sansanアーキテクチャ史
View Slide
荒川 聖悟(Shogo Arakawa)プロダクト開発部 サーバサイドエンジニア@adsholokoad-sho-loko2018年11月 Sansan株式会社入社SansanのAPI開発を主に業務では行っている。好きな領域は低レイヤ。
Sansan Builders BoxAgenda- Sansanとは?- Sansanのアーキテクチャの歴史> v1/v2/v3の変遷> それぞれの学び
Sansan Builders BoxSansanとは?AI×人によるデータ入力データベース化マルチデバイスで活用他システムとの連携お客様AIなどのテクノロジーとオペレーターによる多重入力を活用し、高い入力精度を実現名刺をスキャン4321Sansanスキャナ スマートフォンアプリ
Sansan Builders BoxSansanとは?12年間の歴史のあるアプリケーション
Sansan Builders BoxSansanとは?
v1
Sansan Builders Boxv1 :各レイヤの役割BLLDALWebFormsビジネスロジック担当のレイヤBLLと対応するUIのレイヤSQLなどのデータアクセスを担当するレイヤ
Sansan Builders Boxv1 : コードの雰囲気( BLL )BLLDALWebForms // CardInformationBll.cspublic 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(); // ロールバック処理などは省略}}
Sansan Builders Boxv1 : コードの雰囲気( BLL )BLLDALWebForms // CardInformationBll.cspublic 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(); // ロールバック処理などは省略}}トランザクション管理
Sansan Builders Boxv1 : コードの雰囲気( BLL )BLLDALWebForms // CardInformationBll.cspublic 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で対応
Sansan Builders Boxv1 : コードの雰囲気( DAL )BLLDALWebForms // CardInformationDal.cspublic 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());}手動でクエリ結合
Sansan Builders Boxv1の良い点- とにかくシンプルに書き下せる- 良くも悪くもほとんど共通化を意識していなかった- 画面と業務ロジックの結びつきが明確- 1対1で対応するので当たり前
Sansan Builders Boxv1の悪いところ- 画面駆動アーキテクチャになりがち- 業務ロジックの使いまわしが効かなくて辛い- 各レイヤの責務があいまい- 雰囲気でレイヤを使っていて実装に統一性が取れず辛い
v2
Sansan Builders Boxv2 : 概要DomainModelRepositoryService ビジネスロジックを担当するレイヤモデルとO/Rマッピングを担当するレイヤデータアクセスの抽象化レイヤ
Sansan Builders Boxv2 : 概要DomainModelRepositoryService ビジネスロジックを担当するレイヤモデルとO/Rマッピングを担当するレイヤデータアクセスの抽象化レイヤ用語的にはいわゆるDDDライクなアーキテクチャ
Sansan Builders Boxv2 : アーキテクチャ図ICardDeleteService(Interface)ServiceDomainModelRepositoryCardDeleteServiceCardCardRepositoryICardRepository(Interface)
Sansan Builders Boxv2 : コードの雰囲気(Service)DomainModelRepositoryService // CardDeleteServcie.cspublic 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.Success, Target = target };}}
Sansan Builders Boxv2 : コードの雰囲気(Service)DomainModelRepositoryService // CardDeleteServcie.cspublic 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.Success, Target = target };}}O/R マッピング
Sansan Builders Boxv2 : コードの雰囲気(Service)DomainModelRepositoryService // CardDeleteServcie.cspublic 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.Success, Target = target };}}Repositoryが抽象化!
Sansan Builders Boxv2 : コードの雰囲気(Domain Model)DomainModelRepositoryService // Card.cs[TableName("trn_card")]public class Card{[ColumnName("id", ColumnType.VarChar, 10)]public string Id { get; set; }...}テーブル名を指定カラムの情報を指定
Sansan Builders Boxv2 : コードの雰囲気(Repository)DomainModelRepositoryService 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";
Sansan Builders Boxv2 : コードの雰囲気(Repository)DomainModelRepositoryService 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メソッド内の処理
Sansan Builders Boxv2 : コードの雰囲気(Repository)ファイルの肥大化 モジュールの肥大化× = これじゃない感….
その結果…
DDDの導入に失敗したBy 社内のエンジニア※あくまでも個人の感想です
Sansan Builders Boxv2の課題と学び- Repositoryの肥大化と実装難易度の高さ- Domain ModelごとにRepositoryができる- 一応レイヤごとに分割したが、業務ごとに分割できずに、肥大化する- 「共通化」の認識を共通化させることが出来なかった- フロントエンド固有のロジックが共通されたりする(実はAPI専用メソッドなど)- エンジニア全員に思想と実装を浸透させることはさらに難しい- とにかく遅い!パフォーマンス出ない!- 発行されるクエリが多い&謎すぎてチューニング作業が難航
Sansan Builders Boxv2の良かった点- 画面駆動からは脱却できた- 手探りながらも業務ロジックを共通化できつつあった- まれに効率的(ほとんど聞いたことはない)
v3
Sansan Builders Boxv3 : アーキテクチャ図UIDomainControllerApplicationCardDeleteServiceICardDeleteDataAccessorCardQueryService Controller QueryServiceAPIWebCardDeleteDataAccessorICardDeleteServiceInfrastructure
Sansan Builders Boxv3 : アーキテクチャ図DomainControllerApplicationCardDeleteServiceICardDeleteDataAccessorCardQueryService Controller QueryServiceAPIWebCardDeleteDataAccessorICardDeleteServiceInfrastructureフロントエンド固有のクエリを発行(基本SELECT文のみ)UI
Sansan Builders Boxv3 : アーキテクチャ図DomainControllerApplicationCardDeleteServiceICardDeleteDataAccessorCardQueryService Controller QueryServiceAPIWebCardDeleteDataAccessorICardDeleteServiceInfrastructure1対1で対応UI
Sansan Builders Boxv3 : アーキテクチャ図DomainControllerApplicationCardDeleteServiceICardDeleteDataAccessorCardQueryService Controller QueryServiceAPIWebCardDeleteDataAccessorICardDeleteServiceInfrastructure①共通のクエリを発行するUI
Sansan Builders Boxv3 : アーキテクチャ図DomainControllerApplicationCardDeleteServiceICardDeleteDataAccessorCardQueryService Controller QueryServiceAPIWebCardDeleteDataAccessorICardDeleteServiceInfrastructure②テクノロジ依存を隠蔽化UI
Sansan Builders Boxv3 : アーキテクチャ図(業務レイヤでの分割)UI ControllerApplicationQueryServicecard company adminxxxService xxxService xxxService
Sansan Builders Boxv3 : コードの雰囲気( Application )DomainInfrastructureApplication // BizCardService.cspublic 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;}
Sansan Builders Boxv3 : コードの雰囲気( Application )DomainInfrastructureApplication // BizCardService.cspublic 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;}トランザクション管理
Sansan Builders Boxv3 : コードの雰囲気( Domain )DomainInfrastructureApplication// IBizCardDataAccessor.cspublic interface IBizCardDataAccessor{void DeleteCard(string cardId)}インターフェースを切る
Sansan Builders Boxv3 : コードの雰囲気( Infrastructure )DomainApplication // BizCardDeleteDataAccessor.cspublic void DeleteCard(string cardId){var sql = @"update lkweb.trn_cardset delete_flag = 1where card_id = @cardId";using (var conn = DbConnectionUtility.GetDataDbConnection(ucompanyId)){conn.Execute(sql, new { cardId });}}Infrastructure
Sansan Builders Boxv3 : コードの雰囲気( Infrastructure )DomainApplication // BizCardDeleteDataAccessor.cspublic void DeleteStack(string cardId){var sql = @"update lkweb.trn_cardset delete_flag = 1where card_id = @cardId";using (var conn = DbConnectionUtility.GetDataDbConnection(ucompanyId)){conn.Execute(sql, new { cardId });}}SQLを直で指定するInfrastructure
Sansan Builders Boxv3 : コードの雰囲気( Infrastructure )DomainApplication // BizCardDeleteDataAccessor.cspublic void DeleteCard(string cardId){var sql = @"update lkweb.trn_cardset delete_flag = 1where card_id = @cardId";using (var conn = DbConnectionUtility.GetDataDbConnection(ucompanyId)){conn.Execute(sql, new { cardId });}}実装クラスInfrastructure
Sansan Builders Boxv3の良かった点- シンプルさと共通化のバランスが取れている- 誰が見ても理解しやすい構造になっている- さらに部内のエンジニア内でもそれなりに浸透していると感じる- 無駄な抽象化は避けている- むやみにインターフェース切らない、など
Sansan Builders Boxv3の課題とこれから- モジュールの場所や実装は未だに悩むことがある- 悩むべくして悩んでるとも言える- v1 / v2共存問題
Sansan Builders Box学びのまとめ- 時代とともに流行のアーキテクチャは移り変わる- いわゆる「レガシー」もこうして生まれる- アーキテクチャの浸透は想像以上に難しい- 弊部では新入社員向けにアーキテクチャに関する講座を有志で実施している- プラットフォームとアプリケーションに合ったアーキテクチャを選ぶ
ご清聴ありがとうございました@adsholoko