エリック・エヴァンスのドメイン駆動設計を読みました①

難解と言われながら名著とされている「エリック・エヴァンスのドメイン駆動設計」を読みました。

備忘録として自分なりの解釈を入れたりしてメモを残しておきたいと思います。
間違いもあると思うのでご指摘ください。
この記事では3部まで記載しています。

第1部 ドメインモデルを機能させる
ドメインモデルとは何かですが、それぞれドメインとモデルを以下のように認識しています。

ドメイン
プログラム、システムが関心を持つ領域や対象

モデル
物や事象、概念などを現実にあるものや現実で行っていることをベースに必要な部分を厳選して抽象化すること

モデルの必要な部分というのがドメインによって変化するので、ドメインによって精錬されたモデルがドメインモデル。

ドメインモデルを機能させるために…
・知識をかみ砕く
大量の情報から重要な情報を抜き出す、重要な言葉を見つけ出す(1人で行うのはNG) 抜き出すためには何が重要な情報かを見極めるためにその分野の学習が必要になる

・コミュニケーションと言語の使い方
チームで誤解のないように共通の言葉(ユビキタス言語)を使いながらモデル化を行う。
もし名前がなければ付ける。
その名前の物が何をするのか、チーム全体で共有する。
それが共有できないとコードの共有をするのも難しくなる。

・モデルと実装を結びつける
モデル駆動設計を行う。手続き型ではない。

モデル駆動設計
ソフトウェア要素のサブセットがモデル要素と密接に対応している設計。
相互に一致した状態を保つ。

第2部 モデル駆動設計の構成要素
・ドメイン駆動設計がアーキテクチャに求めること
レイヤードアーキテクチャなどドメイン層が分離したもの

・ドメインモデルの構成要素
ドメインモデルはエンティティと値オブジェクトとドメインサービスから構成される。

エンティティ…同一性(IDなどの識別子を持つ)を持つオブジェクト
値オブジェクト…エンティティとは逆で同一性がないオブジェクト。
同じ色で同じマジックが2つあったらどちらを使うのか気にしない

※同じモノでも視点や誰が使うかによって、同一性をもつかどうかは変わるので注意

ドメインサービス…エンティティや値オブジェクトとして扱うと不自然なもの
エンティティと値オブジェクトもドメインサービスとして扱うこともできるので節度を持って扱う

優れたサービスは以下の3点

  1. 操作がドメインの概念に関係しており、その概念がエンティティや値オブジェクトの自然な一部ではない。
  2. ドメインモデルの他の要素の観点からインターフェースが定義されている
  3. 操作に状態がない

第3部 より深い洞察へ向かうリファクタリング
リファクタリングはソフトウェア開発者にとっては良く知られている言葉で、機能の変更をしないようにソフトウェアの再設計を行うこと。
マーチン・ファウラーの著書である「リファクタリング」のような、ざっくり言うとコードをきれいにするのも重要だが、ドメインモデルの場合は、いわゆるクックブックのやり方を当てはめるだけでは済まない。
クックブックを当てはめることは良いことではあるが、当てはめてドメインモデルが改悪されてしまうと意味がなくなる。
適切なドメインモデルを念頭に創造力を持つこと、試行錯誤を繰り返すことがまず第一で、それが外れない範囲でパターンを適用する。

・ブレイクスルー
ブレイクスルーは起こすものではなくて結果的に起こるもの 継続的なリファクタリングをすることによって、コードやモデルが整えられる。
改良するたびに、開発者の視界は明確になってくる
視界が明確になったことにより、洞察のブレイクスルーをもたらす可能性が作り出される

ブレイクスルーの舞台を整えるために必要なこと

  1. 知識を噛み砕き、強固なユビキタス言語を育成するのに集中すること
  2. 重要なドメインの概念を探求して、それをモデルで明示すること
  3. 設計をよりしなやかになるように改良すること
  4. モデルを蒸留すること

・概念を掘り出す

ドメインエキスパートの使う言葉に耳を傾ける 以下のモデルにとって有益になりうる概念を示す手掛かりを見逃さないようにする。

  1. 何か複雑なものを簡潔に述べている用語がないだろうか?
  2. ドメインエキスパートに言葉の選び方を(たぶん、角が立たないように)正されていないか?
  3. あなたが特定のフレーズを使った時に、ドメインエキスパートたちの困惑した表情が消えることはないか?

制約やプロセスなどをモデル概念とすると設計が鋭くなる場合がある。

・より深い洞察へ向かうリファクタリング 重点的に取り組むべきこと

  1. ドメインに馴染む
  2. 常に物事に対して違う見方をする
  3. ドメインエキスパートとの会話を途切れさせない

リファクタリングを行う際はコードが整然としていることで安心しない。 ドメインモデルが適切かを常に考える必要がある。

モデルが適切でなかった場合、良いモデルについて探求する必要がある。 探求するには、より長い時間がかかり、より多くの人の参加が必要となる。 ドメインエキスパートや元々の開発者など、より多くの人とミーティングを行う。

ソフトウェアはユーザーのためだけのものではなく開発者のためのもの。

より深い洞察へ向かうリファクタリングは継続的なプロセス。 暗黙的な概念が認識されて明示的になる。 設計の一部はよりしなやかになる。 そして深いモデルへと突き進みまたリファクタリングを繰り返していく。

FullCalendarを使う

最近、弊社製品ODINに機能アップデートがあり、配送予定をカレンダーで管理できるという機能が追加されました。
ここで使われている FullCalendar について、どのようなことができて、どのように使うのか紹介します。

できるもの

カレンダー上に予定を表示します。
日付欄や予定がクリックされたときには、FullCalendar独自のイベントが発生し、その予定に関連した処理をさせることができます。

カレンダーの表示

<div id=”calendar”>にカレンダーを描画するという処理を行っています。

<div id="calendar"></div>

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fullcalendar@5.5.0/main.min.css">
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@5.5.0/main.min.js"></script>
<script type="text/javascript">

    document.addEventListener('DOMContentLoaded', function () {

        let calendarEl = document.getElementById('calendar');
        let calendar = new FullCalendar.Calendar(calendarEl, {
            initialView: 'dayGridMonth'
        });

        calendar.render();

    });

</script>

FullCalendarでは予定の表示方法が複数提供されていて、1つ1つの表示方法を「view」としています。
initialViewに「dayDridMonth」というviewを指定することで下のような見た目になります。

予定の表示とコールバック関数の設定

let calendar = new FullCalendar.Calendar(calendarEl, {

        initialView: 'dayGridMonth',

        events: [
           {
                title: '予定1',
                start: '2021-09-15 10:00:00',
                end: '2021-09-15 12:00:00',
           },
           {
               title: '予定2',
               start: '2021-09-17 21:00:00',
               end: '2021-09-18 02:00:00'
           },
           {
               title: '予定3',
               start: '2021-09-20',
               allDay: true
           }
       ],

       dateClick: function (jsEvent) {
           alert('日付がクリックされました。\n' + jsEvent.dateStr);
       },

       eventClick: function (jsEvent) {
           alert(jsEvent.event.title+'がクリックされました。\n' + jsEvent.event.startStr);
       }
});
日付のクリック
予定のクリック

このクリックイベントを使えば、ユーザーに予定を登録・編集させることができます。

また、今回は「events」オプションに直接予定を記入していますが、JSONを返すスクリプトのURLを指定することでも、予定を設定できます。

document.addEventListener('DOMContentLoaded', function () {

   let calendarEl = document.getElementById('calendar')
   let calendar = new FullCalendar.Calendar(calendarEl, {

       initialView: 'dayGridMonth',

       events: '/getEvents.php',

       dateClick: function (jsEvent) {
            alert('日付がクリックされました。\n' + jsEvent.dateStr);
       },

       eventClick: function (jsEvent) {
           alert(jsEvent.event.title+'がクリックされました。\n' + jsEvent.event.startStr);
       }

});

Lambda 不要なAMIを定期削除する

開発中、テスト用の環境にデプロイするたびにAMIを自動取得していたら、不要なAMIが山のようになってしまったので掃除をする。

以前作成したこちらの記事のLambdaを元に、不要なAMIを定期的に削除するようにしてみます。
Lambda 不要なAMIとスナップショットをまとめて削除する

従来の方法(手動実行用)

元のLambda関数では下記のように、削除対象のAMIを手打ちして、eventから取得していました。

{
    "delete_amis": [
        "ami-0123456789abcdef1",
        "ami-0123456789abcdef2",
        "ami-0123456789abcdef3"
    ]
}
def lambda_handler(event, context):
    delete_amis = event['delete_amis']
    print(delete_amis)

    for delete_ami in delete_amis:
        # Delete AMI.
        unregister_ami(delete_ami)
    
        # Delete Snapshots.
        delete_related_snapshots(delete_ami)
    
    return

今回の方法(自動実行用)

  1. 現在所有しているAMIの一覧から、適当なフィルターをかけつつ一部を取得します。
    • Filtersを指定しなかったり、Filters=[{'Name': 'name', 'Values': ['*']}]のように全指定すると、describe_images()からのレスポンスが非常に遅くなるので注意。
  2. AMIの作成日部分を取得して、一週間以上古いものというざっくり基準で削除対象のリストに放り込みます。
    (リスト内包表記となぜか実行時間が変わらなかったので、可読性がヤバいことになるのを避けるためにappendで書いています)
  3. 削除対象となったAMI-IDのリストを元に、以前作成したAMI+Snapshotの削除処理を実行します。
def get_old_ami():
    # Describe target AMIs.
    try:
        response = ec2.describe_images(
            Filters=[{'Name': 'name', 'Values': ['delete-sample*']}]
        )
        if response.get('ResponseMetadata', {}).get('HTTPStatusCode', -1) != 200:
            raise('# Cannot describe images!')
        images = response.get('Images')
        print('# All delete-sample images:', len(images))
    except ClientError as e:
        print(e.response['Error']['Code'])
        print(e.response['Error']['Message'])
        logging.error("# Describe images error: %s", e)
        return
    
    seven_days = 604800
    old_images = []
    epoc_now = int(time.time())
    print('# epoc now:', epoc_now)
    
    # Those older than 7 days are subject to deletion.
    for image in images:
        creation_date = image.get('CreationDate')
        datetime = dt.strptime(creation_date, "%Y-%m-%dT%H:%M:%S.000Z")
        epoc_datetime = int(dt.timestamp(datetime))

        if epoc_now - epoc_datetime > seven_days:
            old_images.append(image.get('ImageId'))

    return old_images
def lambda_handler(event, context):
    # Get the AMI to be deleted.
    delete_amis = get_old_ami()
    print('# Target AMIs:', len(delete_amis))
    print(delete_amis)

    for delete_ami in delete_amis:
        # Delete AMI.
        unregister_ami(delete_ami)
    
        # Delete Snapshots.
        delete_related_snapshots(delete_ami)
    
    return

定期実行

トリガーにEventBridgeを指定し、cron式でスケジュールを設定しました。
JSTで「火~土 AM00:00」に実行したい場合、下記のように指定します。

cron(0 15 ? * MON-FRI *)

Kusanagiセットアップ

EC2でKusanagiを実行するにあたって変更等を行った箇所 早いと噂のKusanagiを検証用にインスタンスを作成したので、その備忘録

前提kusanagiAMIでのインスタンスの起動が出来ており、SSH接続できる状態

  1. 管理者権限で実行
    • sudo su -

  1. Kusanagiをセットアップ
    • セットアップに関しては、公式からわかりやすい手順が記載されている。 こちらを参照

  1. Kusanagiをプロビジョニング
    • これに関しても、公式からわかりやすい記事が用意されているので
      こちらを参照
    • ※プロビジョニングをやり直したい時は、下記コマンドで削除して再試行すればOK
      • kusanagi remove -y [プロファイル名]

  1. phpiniを変更(アップロード最大容量をUP)
    • /etc/php7.d/php.iniにある下記2つの設定を変更
      • post_max_size = 16M
      • upload_max_filesize = 16M
    • 設定の変更をkusanagiに読み込ませる
      • kusanagi php7

  1. All-in-One WP Migration プラグインを入れる
    • ブラウザでWP管理画面にアクセスして、プラグインを導入
    • ※FTPを選択して、kusanagiパスワードを入力する
    • ※このままでは、ファイルサイズが大きかったりするとファイルをアップロードできない可能性が高い

  1. NginxのPOST許容サイズを変更
    • 下記2つのファイルを変更する
      • /etc/nginx/conf.d/[プロファイル名]_http.conf
      • /etc/nginx/conf.d/[プロファイル名]_ssl.conf
    • 変更内容
      • client_max_body_size = 200M
    • Nginxを再起動
      • systemctl restart nginx

  1. ディレクトリの所有者を変更
    • chown -R httpd:www wp-content/plugins/all-in-one-wp-migration/storage
    • chown -R httpd:www wp-content/ai1wm-backups

  1. All-in-One WP Migrationでバックアップファイルをアップロードして完成
    • ファイルサイズが大きいと、時間がかかったりします….

  1. まとめ
    • 少しやることが多いけど、All-in-One WP Migrationのおかげでかなり楽に移行できる。
    • 画像たっぷりのサイトは、Kusanagiに移行してもあまり意味が無いかも…

  1. 参考サイト

初めて、MarkDown形式で投稿してみました✨

Lambdaでレイヤーを追加する方法


Lambdaって便利ですよね…AWSを利用していて何か便利にしたい!と思ったときに必ずと言っていいほどに活躍してくれています。そんなLambda関数をもっと便利に使うために、「レイヤー」と呼ばれる機能があります。


1.レイヤーとは

公式ドキュメントでは、こんなことが書かれています。


Lambdaレイヤーは、追加のコードまたはデータを含むことができる .zip ファイルアーカイブです。レイヤーには、ライブラリ、 カスタムランタイム 、データ、または設定ファイルを含めることができます。レイヤーを使用すると、コードの共有と責任の分離を促進し、ビジネスロジックの記述をより迅速に繰り返すことができます。
レイヤーは、 .zip ファイルアーカイブとしてデプロイされた Lambda関数でのみ使用できます。コンテナイメージとして定義された関数では、コンテナイメージの作成時に、優先ランタイムとすべてのコード依存関係をパッケージ化します。詳細については、AWS コンピューティングブログの「コンテナイメージの Lambda レイヤーと拡張機能の使用

」を参照してください。

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/configuration-layers.html

ざっくりとまとめると、pipなどのライブラリをLambda関数間で共有できたりします。
レイヤーを使う前は、個々の関数で必要なライブラリをpipして、それをひとまとめにしてzipでアップロードしていました。
ですが、このレイヤーを利用することで、あの関数で使ったライブラリが便利だからこっちでも使いたい!となっても、その関数にレイヤーを追加するだけで利用できるようになります。


2.レイヤー追加

1.左横のメニューから「レイヤー」を選択

2.右上の「レイヤーの追加」を選択

3.名前と説明を記入してzipファイルをアップロード、ランタイムを選択し右下の「作成」をクリック

この時に気を付けたいのが、「ファイル名」です。pipしたライブラリ群をzipします。そしてそのファイル名は、「python.zip」としてください。そうしないとレイヤーを追加できても関数で、うまく動作しません。

4.完成!


3.最後に

これで、一つの関数でしか使えなかったpipも、他の関数でも利用できるようになりました。

最後にもう一度だけ、「アップロードするzipファイルの名前は、python.zipにしましょう」