Slide 1

Slide 1 text

Sansanアーキテクチャ史

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

Sansan Builders Box Agenda - Sansanとは? - Sansanのアーキテクチャの歴史 > v1/v2/v3の変遷 > それぞれの学び

Slide 4

Slide 4 text

Sansan Builders Box Sansanとは? AI×人によるデータ入力 データベース化 マルチデバイスで活用 他システムとの連携 お客様 AIなどのテクノロジーとオペレーターに よる多重入力を活用し、高い入力精度を実現 名刺をスキャン 4 3 2 1 Sansanスキャナ スマートフォンアプリ

Slide 5

Slide 5 text

Sansan Builders Box Sansanとは? 12年間の歴史のあるアプリケーション

Slide 6

Slide 6 text

Sansan Builders Box Sansanとは?

Slide 7

Slide 7 text

v1

Slide 8

Slide 8 text

Sansan Builders Box v1 :各レイヤの役割 BLL DAL WebForms ビジネスロジック担当のレイヤ BLLと対応するUIのレイヤ SQLなどのデータアクセスを担当するレイヤ

Slide 9

Slide 9 text

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(); // ロールバック処理などは省略 } }

Slide 10

Slide 10 text

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(); // ロールバック処理などは省略 } } トランザクション管理

Slide 11

Slide 11 text

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で対応

Slide 12

Slide 12 text

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()); } 手動でクエリ結合

Slide 13

Slide 13 text

Sansan Builders Box v1の良い点 - とにかくシンプルに書き下せる - 良くも悪くもほとんど共通化を意識していなかった - 画面と業務ロジックの結びつきが明確 - 1対1で対応するので当たり前

Slide 14

Slide 14 text

Sansan Builders Box v1の悪いところ - 画面駆動アーキテクチャになりがち - 業務ロジックの使いまわしが効かなくて辛い - 各レイヤの責務があいまい - 雰囲気でレイヤを使っていて実装に統一性が取れず辛い

Slide 15

Slide 15 text

v2

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

Sansan Builders Box v2 : アーキテクチャ図 ICardDeleteService (Interface) Service Domain Model Repository CardDelete Service Card CardRepository ICardRepository (Interface)

Slide 19

Slide 19 text

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 }; } }

Slide 20

Slide 20 text

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 マッピング

Slide 21

Slide 21 text

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が抽象化!

Slide 22

Slide 22 text

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; } ... } テーブル名を指定 カラムの情報を指定

Slide 23

Slide 23 text

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";

Slide 24

Slide 24 text

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メソッド内の処理

Slide 25

Slide 25 text

Sansan Builders Box v2 : コードの雰囲気(Repository) ファイルの肥大化 モジュールの肥大化 × = これじゃない感….

Slide 26

Slide 26 text

その結果…

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

Sansan Builders Box v2の課題と学び - Repositoryの肥大化と実装難易度の高さ - Domain ModelごとにRepositoryができる - 一応レイヤごとに分割したが、業務ごとに分割できずに、肥大化する - 「共通化」の認識を共通化させることが出来なかった - フロントエンド固有のロジックが共通されたりする(実はAPI専用メソッドなど) - エンジニア全員に思想と実装を浸透させることはさらに難しい - とにかく遅い!パフォーマンス出ない! - 発行されるクエリが多い&謎すぎてチューニング作業が難航

Slide 29

Slide 29 text

Sansan Builders Box v2の良かった点 - 画面駆動からは脱却できた - 手探りながらも業務ロジックを共通化できつつあった - まれに効率的(ほとんど聞いたことはない)

Slide 30

Slide 30 text

v3

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

Sansan Builders Box v3 : アーキテクチャ図 Domain Controller Application CardDelete Service ICardDelete DataAccessor Card QueryService Controller QueryService API Web CardDelete DataAccessor ICardDelete Service Infrastructure ②テクノロジ依存を隠蔽化 UI

Slide 36

Slide 36 text

Sansan Builders Box v3 : アーキテクチャ図(業務レイヤでの分割) UI Controller Application QueryService card company admin xxxService xxxService xxxService

Slide 37

Slide 37 text

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; }

Slide 38

Slide 38 text

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; } トランザクション管理

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

Sansan Builders Box v3の良かった点 - シンプルさと共通化のバランスが取れている - 誰が見ても理解しやすい構造になっている - さらに部内のエンジニア内でもそれなりに浸透していると感じる - 無駄な抽象化は避けている - むやみにインターフェース切らない、など

Slide 44

Slide 44 text

Sansan Builders Box v3の課題とこれから - モジュールの場所や実装は未だに悩むことがある - 悩むべくして悩んでるとも言える - v1 / v2共存問題

Slide 45

Slide 45 text

Sansan Builders Box 学びのまとめ - 時代とともに流行のアーキテクチャは移り変わる - いわゆる「レガシー」もこうして生まれる - アーキテクチャの浸透は想像以上に難しい - 弊部では新入社員向けにアーキテクチャに関する講座を有志で実施している - プラットフォームとアプリケーションに合ったアーキテクチャを選ぶ

Slide 46

Slide 46 text

ご清聴ありがとうございました @adsholoko

Slide 47

Slide 47 text

No content