アジェンダ
- 自己紹介
- 背景
- この記事に書いてあること
- 抽象的について考える
- フレームワーク開発
- まとめ
自己紹介
はじめまして、Anyflow株式会社 で CTO / サーバサイドエンジニアをしている古内と申します。
普段使っている言語は Python で、Django フレームワークとかも使ってお仕事をしています。
Django では以下の感じのコードを書いています。
https://github.com/furuuchitakahiro/django_othello
Anyflow では iPaaS という分野に取り組んでおります。
iPaaS とは integration Platform as a Service の略で、具体的には Slack と スプレッドシートを連携のような SaaS 間連携のプラットフォームです。
背景
Anyflow では様々な SaaS を連携させることがサーバサイドエンジニアの主なお仕事です。
例えば、Slack でメッセージを送信したり、スプレッドシートから値を読み込むコードを実装します。
ですが、Slack とスプレッドシートは全く異なるシステムで動いていてそれぞれの SaaS にあった連携するコードを実装する必要があります。
もっと掘り下げると Slack でメッセージ送信、ファイル送信、ユーザー情報取得、... と 1 つの SaaS でもたくさんの実装があります。
これらすべてを連携させるとなるととてもじゃありませんがオレオレスクリプトじゃデスマーチ待ったなしです。
この課題を解決すべく Anyflow では SaaS 連携フレームワークを開発しました。
その取り組んできたことをまとめます。
この記事に書いてあること
実装力の向上の参考になるような内容を心がけています。
本記事では抽象・具体を捉えながらダックタイピング実装デモンストレーションをしています。
内容は例を出しながら、シンプルにわかりやすいものから始め、徐々に応用にしていく順番で解説しています。
フレームワークを開発することがなくても普段使っている Django のようなフレームワークへの理解が少しできると思います。
抽象的について考える
みなさん突然ですが 「抽象的」 について取り組んだ・考えたことはありますか?
私は Anyflow を作る以前はあまり深くは考えていませんでした。
まずは、わかりやすく 人間 を題材に解説します。
「田中太郎は人間です。」
これを抽象と具体で考えると
「田中太郎 [ 具体 ] は 人間 [ 抽象 ] です。」
になります。このレベルはかんたんですね。ついでなので Python 化しましょう。
class Human:
def __init__(self, name: str):
self.name = name
if __name__ == '__main__':
taro = Human(name='田中太郎')
print(taro.name)
# > '田中太郎'
めっちゃかんたんですね。
これだけでは抽象化の恩恵がわからないので掘り下げます。
人間ならば名前に加えて年齢もあり、挨拶という行動もできますし、家族がいます。
これらを踏まえて再び Python 化します。
from typing import Iterable, List
class Human:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
def greeting(self) -> str:
return f'私は {self.name} と申します。年齢は {self.age} 歳です。'
class Family:
def __init__(self, members: Iterable[Human]):
self.members = list(members)
def all_greeting(self) -> List[str]:
return [member.greeting() for member in self.members]
if __name__ == '__main__':
taro = Human(name='田中一郎', age=20)
jiro = Human(name='田中次郎', age=19)
saburo = Human(name='田中三郎', age=18)
tanaka_family = Family(members=[taro, jiro, saburo])
print(tanaka_family.all_greeting())
# > [
# '私は 田中一郎 と申します。年齢は 20 歳です。',
# '私は 田中次郎 と申します。年齢は 19 歳です。',
# '私は 田中三郎 と申します。年齢は 18 歳です。'
# ]
いきなりコードが長くなりましたね。
見るべきポイントは Family クラスです。
Family クラスは Human クラスインスタンスの配列をコンストラクタに取ります。
all_greeting メソッドが動作するロジックは members がリストで Human クラスインスタンスであることが前提です。
視点を変えると members は Human クラスインスタンスの配列である の条件だけ満たせば大丈夫と捉えることができます。( いわゆるジェネリクス )
これは Human クラスという人間らしさをまとめた抽象概念と Family クラスという人間の集合である家族という抽象概念を組み合わせた結果です。
極端な話この実装は 人間ならばどんな名前、年齢でもよく、たとえ 100 個あってもそれが人間ならば家族として機能する ということです。
具体的なことには何も触れずただ、人間の特徴、家族の特徴にフォーカスして実装しています。
抽象的とは物事の共通項 ( らしさ ) の定義であることがわかってきました。
![image1.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F193294%2F5f7e7ad1-2e41-e1b9-0cec-237d71ac060a.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=f115f330db90bad741e468bc724c855a)
フレームワーク開発
ここからがフレームワークを開発するにあたって重要な事です。
何を解説するかというといわゆる ダックタイピング です。
Wikipedia では もしもそれがアヒルのように歩き、アヒルのように鳴くのなら、それはアヒルに違いない と表現されています。
こちらの解説は 出身地にフォーカスした FaceBook のようなものを題材に以下の機能を作ります。
- 名前
- 年齢
- 出身地
- 出身地の名物の定形質問への回答
コンセプトは 自身の出身地の名物をどう考えているかをプロフィールに持つ FaceBook 的なやつです。これを説明のために 「出身地 FB」 と名付けます。
それでは Python コードにしてみましょう。
構成は以下です。
- main.py ( メイン処理 )
- mods ( モジュールディレクトリ )
- human.py ( 人間 )
- questions.py ( 質問 )
- prefectures.py ( 都道府県 )
まずは questions.py で質問を実装します。
from typing import ClassVar
class BaseQuestion:
QUEASTION: ClassVar[str] = ''
def __init__(self, answer: str):
self.answer = answer
@property
def to_qa(self) -> str:
return f'Q. {self.__class__.QUEASTION}\nA. {self.answer}'
class SeaQuestion(BaseQuestion):
QUEASTION: ClassVar[str] = '出身地の海はきれいですか?'
class FujisanQuestion(BaseQuestion):
QUEASTION: ClassVar[str] = '富士山はきれいですか?'
BaseQuestion が質問らしさについて定義しています。
質問らしさとは質問文という概念と回答を持つことです。
この BaseQuestion に対して具体的な質問を定義しているのが SeaQuestion や FujisanQuestion などです。ここで大切なのは回答についてはまだ触れていないことです。
質問とは質問文と回答文と捉え、その上で具体的で定型的な質問文があり、それについての回答文が複数ある という構造を実装しています。
ポイントなのが BaseQuestion を継承すればすべて等しく質問として取り扱えるという点です。
すべての質問には質問文、回答文をまとめて文字列で吐き出せる機能が付きます。( to_qa メソッド )
つまり、BaseQuestion クラスが質問フレームワークの役割を果たします。
BaseQuestion を継承してたくさんの質問を作っていくことが出身地 FB のお仕事になっていきます。
続いて、prefectures.py で都道府県を実装します。
from typing import ClassVar, Tuple, Iterable
from . import questions
class BasePrefecture:
NAME: ClassVar[str] = ''
QUESTIONS: ClassVar[Tuple[questions.BaseQuestion, ...]] = tuple()
def __init__(self, qa_list: Iterable[questions.BaseQuestion]):
self.qa_list = list(qa_list)
@classmethod
def get_questions(cls) -> Tuple[str, ...]:
return tuple(question.QUEASTION for question in cls.QUESTIONS)
class Tokyo(BasePrefecture):
NAME: ClassVar[str] = '東京'
QUESTIONS: ClassVar[Tuple[questions.BaseQuestion, ...]] = (
questions.SeaQuestion
)
class Sizuoka(BasePrefecture):
NAME: ClassVar[str] = '静岡'
QUESTIONS: ClassVar[Tuple[questions.BaseQuestion, ...]] = (
questions.SeaQuestion,
questions.FujisanQuestion
)
class Yamanashi(BasePrefecture):
NAME: ClassVar[str] = '山梨'
QUESTIONS: ClassVar[Tuple[questions.BaseQuestion, ...]] = (
questions.FujisanQuestion
)
こちらも質問と同様の構造ですね。
BasePrefecture クラスで都道府県には名前と質問を持つことだけを定義します。
定義した BasePrefecture クラスを継承して Tokyo クラスなどを定義します。
この時に Tokyo クラスらしさ ( 名前と質問一覧 ) を設定します。
一番具体的な状態は出身地とその質問と回答のセットがある状態です。
BasePrefecture クラスが都道府県フレームワークの役割を果たします。
最後に human.py で人間の実装です。
from . import prefectures
class Human:
def __init__(
self, name: str, age: int, prefecture: prefectures.BasePrefecture
):
self.name = name
self.age = age
self.prefecture = prefecture
def greeting(self) -> str:
qa_list_str = '\n'.join([qa.to_qa for qa in self.prefecture.qa_list])
return (
f'私は {self.name} と申します。年齢は {self.age} 歳です。'
'\nQ&A'
f'\n{qa_list_str}'
)
Human クラスはほとんど変わらないですね。
抽象的について考えるからほとんど変わっていません。
追加点として prefecture ( 都道府県 ) をコンストラクタに追加されて、greeting ( 自己紹介 ) にその都道府県の質問と回答が追加されているだけです。
使い方の main.py 実装はこちら
from mods import questions, prefectures
from mods.human import Human
if __name__ == '__main__':
tokyo = prefectures.Tokyo(qa_list=[
questions.SeaQuestion(answer='ちょっと汚いです。')
])
taro = Human(name='田中太郎', age=20, prefecture=tokyo)
print(taro.greeting())
# > 私は 田中太郎 と申します。年齢は 20 歳です。
# Q&A
# Q. 出身地の海はきれいですか?
# A. ちょっと汚いです。
shizuoka = prefectures.Sizuoka(qa_list=[
questions.SeaQuestion(answer='きれいです。'),
questions.FujisanQuestion(answer='大きいです。')
])
jiro = Human(name='田中次郎', age=22, prefecture=shizuoka)
print(jiro.greeting())
# > 私は 田中次郎 と申します。年齢は 22 歳です。
# Q&A
# Q. 出身地の海はきれいですか?
# A. きれいです。
# Q. 富士山はきれいですか?
# A. 大きいです。
yamanashi = prefectures.Yamanashi(qa_list=[
questions.FujisanQuestion(answer='大きいです。')
])
saburo = Human(name='田中三郎', age=24, prefecture=yamanashi)
print(saburo.greeting())
# > 私は 田中三郎 と申します。年齢は 24 歳です。
# Q&A
# Q. 富士山はきれいですか?
# A. 大きいです。
実装のまとめ
めちゃくちゃミニマム過ぎてフレームワークには見えないかもしれませんが本質的な部分については触れられていると思います。
ダックタイピングの良いところはコードの見通しがしやすい点です。
そのクラスの持つ責任の粒度 ( 抽象度 ) をうまく切り分ければ、解決したい課題を明快に表すことができますし、拡張性 ( 多様性 ) を維持することができます。
この出身地 FB の基盤コードは BaseQuestion, BasePrefecture, Human クラスの 3 つだけです。
そのうち BaseQuestion と BasePrefecture を継承して出身地 FBサービスが拡張されていきます。
![image2.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F193294%2F105ad69e-b867-04c2-5bd7-a796bd61ced3.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=5b8b407dc42d891bf072fa65c13c0618)
みなさん Web 開発とかをしているとフレームワークを使って〇〇モデルとかを作っていると思います。
もちろんこの時、フレームワークのモデルクラスを継承して宣言していますよね?
フレームワークのモデルクラスは読込、作成、更新、削除についての動作を定義しているだけであってカラムなどについては何も触れていませんよね。
カラムについては皆さんが作る〇〇モデルクラスで定義しているかと思います。
フレームワークのモデルクラスとみなさんが定義する〇〇モデルの関係がダックタイピングのそれです。
今回のデモンストレーションでは事業特化フレームワークでした。
より抽象的な事 ( モデル・ビュー・コントローラ )を本記事のような内容に沿って実装すると Web アプリ開発で汎用的なフレームワークになります。
まとめ
本当は Python には ABC とか、ドキュメントのための docstring、型ヒントや DI 等もっとフレームワークを開発する際にお伝えしたい内容が山のようにあるのですが、1 冊の本が書けてしまう文量なので今回は主に抽象と具体を考えてダッグタイピングに落とし込むデモンストレーションにしました。
Anyflow ではこの記事の内容の 1 万倍は濃い内容が詰まっています!
開発経緯や Airflow を利用して失敗したりたくさんお話したいことがあります!
ロジック、設計が好きすぎる人・興味がある人はぜひお声がけください!
スタートアップでエンジニアリングだけでなくサービス立ち上げにも興味がある人もぜひ!
Anyflow めっちゃ勢いあります!
B Dash Camp 優勝しました!
https://jp.techcrunch.com/2019/10/31/b-dash-camp-2019-fall-pitch-arena/
Incubate Camp 優勝しました!
https://jp.techcrunch.com/2019/09/14/incubate-camp-12th/
採用ページ: https://open.talentio.com/1/c/anyflow/requisitions/detail/13828
もっとカジュアルに興味がある人は https://www.facebook.com/takahiro.furuuchi.37 に DM ください!