Slide 1

Slide 1 text

育てるアーキテクチャ 株式会社テレシー VPoE daisuke-sasaki(@fujidomoe) PyConJP 2025 2025.09.27 戦い抜くマイクロサービスの設計と進化戦略

Slide 2

Slide 2 text

株式会社テレシー VPoE daisuke-sasaki 略歴 株式会社テレシー 開発部 部長/VPoE ● 2020年にテレシーにJOIN、リードエンジニアとしてアプリケー ションとインフラのエンジニアリングを行う。 ● 2024年からテレシー VPoEに就任。 役割/領域 #高校野球 #少年野球 management engineering Cloud infra Back Manag ement Front @fujidomoe

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

おしらせ

Slide 7

Slide 7 text

AGENDA 背景 02 Pythonエコシステムの特徴 03 実際に遭遇したアンチパターン 04 改善の旅 05 チームでの実践的な改善のプロセス 01

Slide 8

Slide 8 text

突然ですが、皆さんの職場にエンジニアでない方が書いた Pythonコードはありませんか? 󰢧

Slide 9

Slide 9 text

とりあえず動くコード 😇

Slide 10

Slide 10 text

def process(data_df): jp_indices = data_df[data_df['region'] == 'JP'].index data_df.drop(data_df.index.difference(jp_indices), inplace=True) data_df['ctr'] = data_df['clicks'] / data_df['views'] low_perf_indices = data_df[data_df['ctr'] < 0.02].index data_df.drop(low_perf_indices, inplace=True) data_df['is_high_performer'] = True 存在保証のない列でフィルタ &中間データの生成 破壊的な操作 存在保証のない列を用いて演算

Slide 11

Slide 11 text

本日のゴールです ● 技術は「きっかけ」、文化が「ゴール」 ● アーキテクチャを育てるのはチームです。

Slide 12

Slide 12 text

このトークを聞いてもらいたい人 ● 前のスライドの全部のせ関数が、本番で動いているのを 想像して「ウッ」となった方 ● 実践的な設計パターンやアンチパターンに興味がある方 ● セルフマージ文化に課題を感じている方

Slide 13

Slide 13 text

● 開発体制(2022年頃) ○ 研究者とエンジニアは別チーム ○ それぞれのチームで複数のマイクロサービスを開発 ○ 1マイクロサービス -> 1人が担当、全てセルフマージ 本日お話する舞台の背景

Slide 14

Slide 14 text

● 開発体制(2022年頃) ○ 研究者とエンジニアは別チーム ○ それぞれのチームで複数のマイクロサービスを開発 ○ 1マイクロサービス -> 1人が担当、全てセルフマージ 本日お話する舞台の背景

Slide 15

Slide 15 text

● 開発体制(2022年頃) ○ 研究者とエンジニアは別チーム ○ それぞれのチームで複数のマイクロサービスを開発 ○ 1マイクロサービス -> 1人が担当、全てセルフマージ 本日お話する舞台の背景

Slide 16

Slide 16 text

● 開発体制(2022年頃) ○ 研究者とエンジニアは別チーム ○ それぞれのチームで複数のマイクロサービスを開発 ○ 1マイクロサービス -> 1人が担当、全てセルフマージ 本日お話する舞台の背景

Slide 17

Slide 17 text

研究職の方から本番稼働中のETLバッチを引き継いだ。 なんでETLバッチのイニシャルを研究職が書いたの?

Slide 18

Slide 18 text

研究職の方から本番稼働中のETLバッチを引き継いだ。 なんでETLバッチのイニシャルを研究職が書いたの?

Slide 19

Slide 19 text

組織で当たり前が揃っていない。レビュー文化がない。 その弾みで、 「試行錯誤中相当のコードが本番で動いている」 これを出発点に、「戦い抜くPythonマイクロサービスの設計 と進化戦略」それを契機としてチームの文化を変えていった 話をします。

Slide 20

Slide 20 text

● リアーキテクチャは、まぁ、やるよね。 ● コードレビューも、まぁ、やるよね。 ● 大事なのは推進プロセス ● 最も気を使ったのは組織のソフト面

Slide 21

Slide 21 text

● リアーキテクチャは、まぁ、やるよね。 ● コードレビューも、まぁ、やるよね。 ● 大事なのは推進プロセス ● 最も気を使ったのは組織のソフト面

Slide 22

Slide 22 text

● 今日話すこと ○ 引き継いだバッチアプリケーション ■ アンチパターンの紹介と具体的な改善 ■ チームでの実践的な改善のプロセス

Slide 23

Slide 23 text

● 今日話さないこと ● 特定フレームワーク、特定ORMの詳細解説

Slide 24

Slide 24 text

AGENDA 背景 02 Pythonエコシステムの特徴 03 実際に遭遇したアンチパターン 04 改善の旅 05 チームでの実践的な改善のプロセス 01

Slide 25

Slide 25 text

Pythonエコシステムの特徴 研究職(notエンジニア)の方が書いたコードを引き継ぐケー スがある

Slide 26

Slide 26 text

稀によくある光景 Jupyter Notebookから始まったコードが、ほとんどそのま ま本番バッチとして稼働 😇

Slide 27

Slide 27 text

稀によくある光景 ● 設計思想がバラバラ ○ あるものは「スクリプトの延長」 ○ あるものは「オブジェクト指向」 ● テストコードは存在しない。

Slide 28

Slide 28 text

AGENDA 背景 02 Pythonエコシステムの特徴 03 実際に遭遇したアンチパターン 04 改善の旅 05 チームでの実践的な改善のプロセス 01

Slide 29

Slide 29 text

前置き: 実際に稼働していたコードへの敬意 ● 紹介するコードは実際に本番で動きお金を稼いでくれた コード 💸 ● 稼いだくれたコードに感謝と敬意を持って話します

Slide 30

Slide 30 text

アンチパターン紹介 ● Python動的型付けの落とし穴 ○ kwargs編 ○ Pandas DataFrame編 ● アーキテクチャ不在 ● グローバル関数羅列

Slide 31

Slide 31 text

アンチパターン紹介 ● Python動的型付けの落とし穴 ○ kwargs編 ○ Pandas DataFrame編 ● アーキテクチャ不在 ● グローバル関数羅列

Slide 32

Slide 32 text

Pythonは動的型付け言語でさくっと動く

Slide 33

Slide 33 text

アプローチ次第でこれが罠になりがちです

Slide 34

Slide 34 text

この辞書には、何のキーが入ってるんだっけ 🤔

Slide 35

Slide 35 text

kwargsとは? def create_profile(name, **options): profile = {"name": name} # デフォルト値を設定 profile["age"] = options.get("age", "未設定") profile["email"] = options.get("email", "未設定") profile["location"] = options.get("location", "未設定") return profile

Slide 36

Slide 36 text

kwargs編① def extract_data(user_id, **kwargs): start = kwargs.get('start_date', '2025-09-27') if kwargs.get('inclued_deleted'): pass

Slide 37

Slide 37 text

kwargs編① def extract_data(user_id, **kwargs): start = kwargs.get('start_date', '2025-09-27') if kwargs.get('inclued_deleted'): pass 😱s/inclued/include 😱常にFalseになるが気づかない

Slide 38

Slide 38 text

kwargs編② # extract.py def extract_data(user_id, **kwargs): start_date = kwargs.get("start_date", '2025-09-27') end_date = kwargs.get("end_date", datetime.now()) include_deleted = kwargs.get('include_deleted', False) batch_size = kwargs.get('batch_size', 1000) retry_count = kwargs.get('retry_count', 3) # さらに続く... # transform.py def transform_data(data, **kwargs): normalize = kwargs.get('normalize', True) remove_duplicates = kwargs.get('remove_duplicates', True) # まだまだ続く...

Slide 39

Slide 39 text

kwargs編② # extract.py def extract_data(user_id, **kwargs): start_date = kwargs.get("start_date", '2025-09-27') end_date = kwargs.get("end_date", datetime.now()) include_deleted = kwargs.get('include_deleted', False) batch_size = kwargs.get('batch_size', 1000) retry_count = kwargs.get('retry_count', 3) # さらに続く... 😱 どんなパラメータを渡せるの? 😱 利用可能なオプション覚えておくの?

Slide 40

Slide 40 text

アンチパターン紹介 ● Python動的型付けの落とし穴 ○ kwargs編 ○ Pandas DataFrame編 ● アーキテクチャ不在 ● グローバル関数羅列

Slide 41

Slide 41 text

Pandas DataFrameとは? Pythonで表形式のデータ操作 を柔軟に扱うためのライブラ リです。

Slide 42

Slide 42 text

Pandas DataFrameを利用した例 import pandas as pd from sqlalchemy import create_engine db_engine = create_engine("省略") # 1. SQLクエリを準備 sql_query = "SELECT user_id, product_name, price FROM sales_data WHERE price > 5000;" # 2. SQLを実行し、結果を直接DataFrameに読み込む df = pd.read_sql(sql_query, db_engine) # 3. 取得したDataFrameを表示 print(df) # 例:価格(price)が10000より大きいデータを抽出 high_price_df = df[df['price'] > 10000]

Slide 43

Slide 43 text

Pandas Dataframe編 def process(self, target_df: pd.DataFrame, source_df: pd.DataFrame) -> pd.DataFrame: time_total_df = (target_df.groupby("datehour")[["count"]] .sum().reset_index().rename(columns={"count":"total_count"}) ) prorating_target_df = pd.merge(target_df, time_total_df, on="datehour", how="left") prorating_target_df = pd.merge( prorating_target_df, source_df.rename(columns={"count": "source_count"}), on="datehour", how="outer", ) df = prorating_target_df[["datehour","prefecture","prorated"]].rename(columns={"prorated": "count"}) # この後手続き処理が100行ほど続く...

Slide 44

Slide 44 text

Pandas Dataframe編 def process(self, target_df: pd.DataFrame, source_df: pd.DataFrame) -> pd.DataFrame: time_total_df = (target_df.groupby("datehour")[["count"]] .sum().reset_index().rename(columns={"count":"total_count"}) ) prorating_target_df = pd.merge( prorating_target_df, source_df.rename(columns={"count": "source_count"}), on="datehour", how="outer", ) df = prorating_target_df[["datehour","prefecture","prorated"]].rename(columns={"prorated": "count"}) # この後手続き処理が100行ほど続く... 😇これはこれで気になる

Slide 45

Slide 45 text

Pandas Dataframe編 def process(self, target_df: pd.DataFrame, source_df: pd.DataFrame) -> pd.DataFrame: time_total_df = (target_df.groupby("datehour")[["count"]] .sum().reset_index().rename(columns={"count":"total_count"}) ) # この後手続き処理が100行ほど続く... 😱引数も戻り値もDataFrame

Slide 46

Slide 46 text

Pandas Dataframe編 time_total_df = (target_df.groupby("datehour")[["count"]] .sum().reset_index().rename(columns={"count":"total_count"}) ) prorating_target_df = pd.merge(target_df, time_total_df, on="datehour", how="left") prorating_target_df = pd.merge( prorating_target_df, source_df.rename(columns={"count": "source_count"}), on="datehour", how="outer", ) df = prorating_target_df[["datehour","prefecture","prorated"]].rename(columns={"prorated": "count"}) メソッドの中身全体として 😱 存在保証のないカラムにアクセス 😱 各操作がDataFrameの構造を変更

Slide 47

Slide 47 text

アンチパターン紹介 ● Python動的型付けの落とし穴 ○ kwargs編 ○ Pandas DataFrame編 ● アーキテクチャ不在 ● グローバル関数羅列

Slide 48

Slide 48 text

アーキテクチャ不在(グローバル関数羅列) class RDBConnection: def __init__(self): self.con = pymysql.connect( "省略") def execute_read_query (self, query: str): return result def find_report_by_date_query (date: str) -> str: q = f"""SELECT report_id FROM ...""" return q def find_report_by_date (date: str) -> list[int]: db = RDBConnection() q = find_report_by_date_query (date) res = db.execute_read_query (q) return [int(r[0]) for r in res] def find_report_by_id_query (id: int) -> list[int]: # 省略 def find_report_by_id_query (id: int) -> str: # 省略 # find_report_by_dateと同じようなパターンが沢山存在する ....

Slide 49

Slide 49 text

アーキテクチャ不在(グローバル関数羅列) def find_report_by_date (date: str) -> list[int]: db = RDBConnection() q = find_report_by_date_query (date) res = db.execute_read_query (q) return [int(r[0]) for r in res] 毎回コネクション ビジネスロジック(データ変換)、SQL生成、データ入出力

Slide 50

Slide 50 text

アーキテクチャ不在(グローバル関数羅列) def find_report_by_date_query(date: str) -> str: q = f"""SELECT report_id FROM ...""" return q def find_report_by_date(date: str) -> list[int]: db = RDBConnection() q = find_report_by_date_query(date) res = db.execute_read_query(q) return [int(r[0]) for r in res] 必ずセット

Slide 51

Slide 51 text

AGENDA 背景 02 Pythonエコシステムの特徴 03 実際に遭遇したアンチパターン 04 改善の旅 05 チームでの実践的な改善のプロセス 01

Slide 52

Slide 52 text

Step by Stepアプローチ ● Step1 : 型ヒントを追加してみる ● Step2 : Pydanticで1つの関数の引数を整理 ● Step3 : レイヤー分離を試す ● Step4 : チーム全体で改善の輪をひろげる

Slide 53

Slide 53 text

Step by Stepアプローチ ● Step1 : 型ヒントを追加してみる(割愛) ● Step2 : Pydanticで1つの関数の引数を整理 ● Step3 : レイヤー分離を試す ● Step4 : チーム全体で改善の輪をひろげる

Slide 54

Slide 54 text

アンチパターンのおさらい ● Python動的型付けの落とし穴 ■ kwargs編 ■ DataFrame編

Slide 55

Slide 55 text

Pydanticによる秩序の導入 ● Pythonのデータ検証と シリアライゼーションの ためのライブラリ。 ● 型ヒントを活用してラン タイムでのデータ検証を 行い、データの整合性を 保証。

Slide 56

Slide 56 text

Pydanticによる秩序の導入 ● Pythonのデータ検証と シリアライゼーションの ためのライブラリ。 ● 型ヒントを活用してラン タイムでのデータ検証を 行い、データの整合性を 保証。

Slide 57

Slide 57 text

before # extract.py def extract_data(user_id, **kwargs): start_date = kwargs.get("start_date", '2025-09-27') end_date = kwargs.get("end_date", datetime.now()) include_deleted = kwargs.get('include_deleted', False) batch_size = kwargs.get('batch_size', 1000) retry_count = kwargs.get('retry_count', 3) # さらに続く... 😱 どんなパラメータを渡せるの? 😱 利用可能なオプション覚えておくの?

Slide 58

Slide 58 text

After from pydantic import BaseModel, Field from datetime import datetime class Param(BaseModel): user_id: int start_date: datetime = Field(default=datetime(2025, 9, 27)) end_date: datetime = Field(default_factory=datetime.now) include_deleted: bool = False batch_size: int = Field(default=1000, gt=0, le=10000) retry_count: int = Field(default=3, ge=1, le=5) def extract_data(param: Param): # 何が渡ってくる型が明確 😀 # IDEの補完が効く! 😀 # validな値が渡ってくる 😀

Slide 59

Slide 59 text

ipythonで動かしてみる

Slide 60

Slide 60 text

正常想定の値によるインスタンス生成 : 生成成功 In [1]: from pydantic import BaseModel, Field ...: from datetime import datetime ...: class Param(BaseModel): ...: user_id: int ...: start_date: datetime = Field(default=datetime(2025, 9, 27)) ...: end_date: datetime = Field(default_factory=datetime.now) ...: include_deleted: bool = False ...: batch_size: int = Field(default=1000, gt=0, le=10000) ...: retry_count: int = Field(default=3, ge=1, le=5) In [2]: p = Param(user_id=123, start_date="2025-09-27T14:50:00") In [3]: p Out[3]: Param(user_id=123, start_date=datetime.datetime(2025, 9, 27, 14, 50), end_date=datetime.datetime(2025, 9, 14, 18, 10, 6, 756829), include_deleted=False, batch_size=1000, retry_count=3) 成功

Slide 61

Slide 61 text

不正な値によるインスタンス生成 : 生成失敗 ---->1 param = Param( # 不正な値を渡すと 2 user_id="not_a_number" , # ❌ ValidationError 3 batch_size=100000) # ❌ batch_size must be <= 10000 File /usr/local/lib/python3.12/site-packages/pydantic/main.py:253, in BaseModel. __init__(self, **data) --> 253 validated_self = self.__pydantic_validator__.validate_python(data, self_instance =self) 254 if self is not validated_self: 255 warnings.warn( "省略", stacklevel=2) ValidationError : 2 validation errors for Param user_id Input should be a valid integer , unable to parse string as an integer [ type=int_parsing , input_value= 'not_a_number' , input_type= str] For further information visit https://errors.pydantic.dev /2.11/v/int_parsing batch_size "省略” 例外

Slide 62

Slide 62 text

IDEの補完

Slide 63

Slide 63 text

kwargs版

Slide 64

Slide 64 text

Pydantic版

Slide 65

Slide 65 text

ちなみに、イミュータブル(変更不可能)にできます

Slide 66

Slide 66 text

Pydantic デフォルト設定 In [1]: from pydantic import BaseModel, ConfigDict In [2]: class User(BaseModel): ...: id: int ...: name: str In [3]: u = User(id=123, name="PyCon2025") In [4]: u.name="hoge" In [5]: u Out[5]: User(id=123, name='hoge') 項目値変更可能 インスタンス生成成功

Slide 67

Slide 67 text

Pydantic イミュータブル設定 In [1]: from pydantic import BaseModel, ConfigDict ...: class ImmutableModel(BaseModel): ...: model_config = ConfigDict(frozen=True) # イミュータブル In [2]:class User(ImmutableModel): ...: id: int ...: name: str In [3]: u = User(id=123, name="PyCon2025") In [4]: u.name = "hoge" --------------------------------------------------------------------------- ValidationError Traceback (most recent call last) Cell In[4], line 1 ----> 1 u.name = "hoge" インスタンス生成成功 項目値変更すると例外

Slide 68

Slide 68 text

Python標準ライブラリのdataclass (型検証文脈での比較)

Slide 69

Slide 69 text

型ヒントのみ In [1]: from dataclasses import dataclass ...: @dataclass ...: class Param: ...: name: str ...: age: int ...: In [2]: p = Param(name="PyCon2025", age="15歳") In [3]: p Out[3]: Param(name='PyCon2025', age='15歳') ”15歳”という文字列がそのまま格納 🤔

Slide 70

Slide 70 text

他の選択肢はあるの?

Slide 71

Slide 71 text

他の選択肢はあるの? import pandas as pd import pandera as pa # 「name」と「age」カラムが必須であるスキーマを定義 schema = pa.DataFrameSchema({ "name": pa.Column(str), # ageは0以上の整数 "age": pa.Column(int, pa.Check.ge(0)) }) invalid_df = pd.DataFrame({ "name": ["Taro", "Jiro"] # "age"カラムが欠けている! }) try: schema.validate(invalid_df) except pa.errors.SchemaError as e: print(e)

Slide 72

Slide 72 text

Step by Stepアプローチ ● Step1 : 型ヒントを追加してみる(割愛) ● Step2 : Pydanticで1つの関数の引数を整理 ● Step3 : レイヤー分離を試す ● Step4 : チーム全体で改善の輪をひろげる

Slide 73

Slide 73 text

アンチパターンのおさらい ● アーキテクチャ不在 ● グローバル関数羅列 😱 認知負荷が高い 😱 変更の影響範囲がわかりにくい 😱 テストを書きにくい

Slide 74

Slide 74 text

アンチパターンのおさらい ● アーキテクチャ不在 ● グローバル関数羅列 😱 認知負荷が高い 😱 変更の影響範囲がわかりにくい 😱 テストを書きにくい

Slide 75

Slide 75 text

アンチパターンのおさらい ● アーキテクチャ不在 ● グローバル関数羅列 😱 認知負荷が高い 😱 変更の影響範囲がわかりにくい 😱 テストを書きにくい

Slide 76

Slide 76 text

アンチパターンのおさらい ● アーキテクチャ不在 ● グローバル関数羅列 😱 認知負荷が高い 😱 変更の影響範囲がわかりにくい 😱 テストを書きにくい

Slide 77

Slide 77 text

● リポジトリパターン ● データアクセスロジックをビジネスロジックから分 離する設計パターン。 ● データストア(データベース、API、ファイルシステ ムなど)へのアクセスを抽象化し、ドメインモデル とデータソースの間に仲介層を設けることで、アプ リケーションの保守性と拡張性を向上させます。 リポジトリパターンによる関心の分離

Slide 78

Slide 78 text

● リポジトリパターン ● データアクセスロジックをビジネスロジックから分 離する設計パターン。 ● データストア(データベース、API、ファイルシステ ムなど)へのアクセスを抽象化し、ドメインモデル とデータソースの間に仲介層を設けることで、アプ リケーションの保守性と拡張性を向上させます。 リポジトリパターンによる関心の分離

Slide 79

Slide 79 text

リポジトリパターンによる関心の分離 抽象定義 具象実装 ビジネスロジック

Slide 80

Slide 80 text

class IReportRepository(ABC): @abstractmethod def find_by_report_id(self, report_id: int) -> Report | None: pass class ReportRepository(IReportRepository): def __init__(self, session: Session): self._session = session # DB接続は外部から注入 def find_by_report_id(self, report_id: int) -> Report | None: x = self._session.query(ReportDTO).filter_by(id=report_id).one() #try except省略 return Report(report_id=x.id, report_name=x.report_name, expired_at=x.expired_at) class ReportUseCase: def __init__(self, repository: IReportRepository): self._repository = repository def find_report(self, report_id: int): report = self._repository.find_by_report_id(report_id) if not report: return None ... 省略 ... 抽象基底クラスを作成する仕組み 継承先にメソッド実装を強制

Slide 81

Slide 81 text

class IReportRepository(ABC): @abstractmethod def find_by_report_id(self, report_id: int) -> Report | None: pass class ReportRepository(IReportRepository): def __init__(self, session: Session): self._session = session # DB接続は外部から注入 def find_by_report_id(self, report_id: int) -> Report | None: x = self._session.query(ReportDTO).filter_by(id=report_id).one() #try except省略 return Report(report_id=x.id, report_name=x.report_name, expired_at=x.expired_at) class ReportUseCase: def __init__(self, repository: IReportRepository): self._repository = repository def find_report(self, report_id: int): report = self._repository.find_by_report_id(report_id) if not report: return None ... 省略 ... 継承元の@abstractmethod由来で実装を強制

Slide 82

Slide 82 text

before ## before: 混沌 query.py (1000行超え) ├── find_report_by_date_query() ├── find_report_by_date() ├── find_report_by_id_query() ├── find_report_by_id() └── ... (延々と続く)

Slide 83

Slide 83 text

after ## After: レイヤー構造 ├── cli # エントリーポイント ├── domain │ ├── entity │ └── repository # interfaceのみ ├── infra # DB接続など │ └── mysql │ ├── model │ └── repository # interfaceの実装

Slide 84

Slide 84 text

● 一貫性のあるデータアクセス ● 関心の分離 ● テストの容易性 リポジトリパターンの目的と利点

Slide 85

Slide 85 text

● 一貫性のあるデータアクセス ● 関心の分離 ● テストの容易性 リポジトリパターンの目的と利点

Slide 86

Slide 86 text

● 一貫性のあるデータアクセス ● 関心の分離 ● テストの容易性 リポジトリパターンの目的と利点

Slide 87

Slide 87 text

● 一貫性のあるデータアクセス ● 関心の分離 ● テストの容易性 リポジトリパターンの目的と利点

Slide 88

Slide 88 text

Step by Stepアプローチ ● Step1 : 型ヒントを追加してみる(割愛) ● Step2 : Pydanticで1つの関数の引数を整理 ● Step3 : レイヤー分離を試す ● Step4 : チーム全体で改善の輪をひろげる

Slide 89

Slide 89 text

チームの巻き込み方

Slide 90

Slide 90 text

● 1マイクロサービス -> 1人が担当 ● コードレビューの文化なし 運用体制のおさらい

Slide 91

Slide 91 text

①敬意から始める ● 既存コードは「ビジネスに貢献してきた資産」 ● 動いているコードへの感謝を忘れない ● 批判ではなく「さらに良くする」スタンス

Slide 92

Slide 92 text

“あらゆる人間関係の衝突は、謙虚・尊敬・信頼の欠如によるものだ。”

Slide 93

Slide 93 text

● 改善ポイントを公開の場で議論 ○ Slack/Teamsのpublic channel or GitHub ○ 定例でのアジェンダ化 ○ 段階的な合意形成 ■ Step1 : 課題の共有 ■ Step2 : 解決策の提案 ■ Step3 : 小さく試す ②透明性のある進め方

Slide 94

Slide 94 text

③チーム全体の成長機会に ● アーキテクチャ共有会の開催 ○ Before/Afterの比較 ○ 改善の意図を説明 ○ 質問・議論の時間を十分に ○ ペアプロ・モブプロを取り入れるなど

Slide 95

Slide 95 text

小さく始めて大きく育てる

Slide 96

Slide 96 text

Step by Stepアプローチ(おさらい) ● Step1 : 型ヒントを追加してみる ● Step2 : Pydanticで1つの関数の引数を整理 ● Step3 : レイヤー分離を試す ● Step4 : チーム全体で改善の輪をひろげる

Slide 97

Slide 97 text

まとめ

Slide 98

Slide 98 text

ご清聴ありがとうございました 技術は「きっかけ」、文化が「ゴール」 アーキテクチャを育てるのはチームです。