Wiz テックブログ

Wizは、最新のIoTやICTサービスをお客様に届ける「ITの総合商社」です。

TypeScript InterfaceとType Aliasについて

f:id:yukiji_03:20210805141123j:plain

はじめに

皆様こんにちは、フロントエンドの松本です。

本日はTypeScriptのInterfaceType Aliasについてお話していきたいと思います。

オブジェクトの宣言時などはどちらを使えばいいのか、頭を悩ませるトピックの一つでもあると思います。

それでは早速、両者の具体的な違いをみていきましょう。

型の宣言

// Interface
interface Book {
    title: string
    page: number
}

// Type Alias
type Book = {
    page: number
}

Interfaceは構造を定義するものなのでType Aliasと違って=が不要になっており、見た目としてはほとんど一緒です。

また、 Type Aliasは右辺に任意の型を指定できるという点で汎用的です。

Interfaceの場合、ブロックスコープでなければならないので、次のようなType AliasInterfaceに書き直す方法はありません。

type A = number
type B = A | string

宣言のマージ

同名のInterfaceは 型がマージされます(宣言のマージ)

interface Book {
    title: string
}

// Bookに page: number をマージ
interface Book {
    page: number
}

const book: Book = {
    title: 'TypeScript',
    page: 360
}

これをType Alias に書き直すとエラーが起こります。

type Book = {
    title: string
}

type Book = {
    page: number
}

// //Duplicate identifier 'Book'

拡張性

Interface は拡張性が高いです。

extendsを使うことで、継承したサブインターフェースを作成できます。

interface Movie {
    title: string
    time: number
}

//Movieを継承
interface Drama extends Movie {
    season: number
}

const kaigaiDrama: Drama = {
    title: 'The Walking Dead',
    time: 60,
    season: 10
}

また、InterfaceType Aliasをextendsすることも出来ます。

// Type Alias
type Movie {
    title: string
    time: number
}

//Movieを継承
interface Drama extends Movie {
    season: number
}

const kaigaiDrama: Drama = {
    title: 'The Walking Dead',
    time: 60,
    season: 10
}

Type Aliasは拡張こそできないものの、交差型を利用して既存のタイプを組み合わせることができます。

type Rice = {
    kind: '白米' | '玄米'
    gram: number
}

type Egg = {
    size: 'S' | 'M' | 'L'
}

type TamagokakeGohan = Rice & Egg

const tkg: TamagokakeGohan = {
    kind: '白米',
    gram: 250,
    size: 'L'
}

まとめ

Type AliasInterfaceはほとんど同じことができ、違いは小さなものです。

Type Aliasは型に名前をつけるもの、Interfaceは構造を定義するものです。

例えばオブジェクトの宣言時などはInterfaceを使用するなどして、ケースバイケースで使い分けると良いと思います。

個人的にはType Aliasで書いた方が安心できる派です。

Interfaceは宣言のマージにより、意図しないところで型が変化する恐れがあるので)

最後になりますが、Wizではエンジニアを募集中です。

興味のある方は是非覗いてみてください↓

careers.012grp.co.jp

NextAuth.jsでカスタムログインページを実装する方法

f:id:mt_816:20210804192049p:plain

こんにちは、フロントエンドエンジニアの松尾です。

最近はNext.jsが盛り上がっており、エコシステムも充実してきています。

その中に、NextAuth.jsという認証機能を簡単に実装できるライブラリがあり非常に便利です。

next-auth.js.org

本記事では、NextAuth.jsを使用したカスタムログインページの実装方法について説明したいと思います。

NextAuth.jsとは?

NextAuth.jsとは、Next.jsに簡単に認証機能を実装できるライブラリです。

アカウント登録も必要なく、ライブラリをインストールするだけで実装できます。

また、メールアドレス認証に加えて、GoogleTwitterFacebookといったサービスのOAuth認証を組み込むことが可能です。

NextAuth.jsでログインする場合、以下のようにNextAuth.js専用のログインページに遷移する形となっており、自由にページをカスタマイズできません。

f:id:mt_816:20210803152037p:plain

認証機能を特定のページに組み込みたい場合も多いのではないでしょうか?

そこで、カスタムログインページの実装方法について説明したいと思います。

実装方法

まず初めに、next-authをインストールしてください。

npm install next-auth

次に、NextAuth.jsのAPIルートを作成します。

/pages/api/auth/[...nextauth].jsを作成し、以下を記述してください。

import NextAuth from 'next-auth'
import Providers from 'next-auth/providers'

// NextAuth.js関数に渡すオプション
const options = {
  providers: [
    Providers.Credentials({
      // NextAuthの認証関数。credentialsにログイン情報が格納される。
      authorize: async credentials => {
        if (
          // ログインID・パスワードは環境変数にて設定する。
          credentials.login === process.env.NEXT_PUBLIC_LOGIN_ID &&
          credentials.password === process.env.NEXT_PUBLIC_PASSWORD
        ) {
          // ログイン成功後ユーザー情報を返却する。値はsessionに格納される。
          return Promise.resolve({ name: 'admin' })
        } else {
          // ログイン失敗後認証を拒否し、エラーメッセージを返却する。
          return Promise.resolve(null)
        }
      },
    }),
  ],
  // ログインページを指定する。今回はトップページのため'/'を指定。
  pages: {
    signIn: '/',
  },
}

export default (req, res) => NextAuth(req, res, options)

最後に、認証機能を実装するページに以下を記述します。

import { useRouter } from 'next/router'
import { getCsrfToken, useSession, signOut } from 'next-auth/client'

const LoginPage = ({ csrfToken }) => {
  const { error } = useRouter().query
  const [ session ] = useSession()

  return (
    <div>
      <h1>カスタムログインページ</h1>
      {session ? (
        // ログイン状態の場合。ユーザー名、ログアウトボタンを表示。
        <>
          <div>ユーザー:{session.user?.name}</div>
          <button onClick={signOut}>ログアウト</button>
        </>
      ) : (
        // ログアウト状態の場合。入力フォームを表示。
        <form method='post' action='/api/auth/callback/credentials'>
          <input name='csrfToken' type='hidden' defaultValue={csrfToken} />
          <label>
            <div>ログインID</div>
            <input name='login' />
          </label>
          <label>
            <div>パスワード</div>
            <input name='password' type='password' />
          </label>
          <div>
            <button type='submit'>ログイン</button>
          </div>
          {/* ログイン失敗後、エラーメッセージを表示。*/}
          {error && <div>ログインID又はパスワードが間違っています。</div>}
        </form>
      )}
    </div>
  )
}

// POSTリクエスト(サインイン・サインアウトなど)に必要なCSRFトークンを返却する。
export const getServerSideProps = async context => {
  return {
    props: {
      csrfToken: await getCsrfToken(context),
    },
  }
}

export default LoginPage

ログインに成功すると、useSession関数のsessionにユーザー情報が格納されます。

以下のように、sessionにユーザー情報が含まれているか否かで表示を切り替えています。

▼ ログイン前

f:id:mt_816:20210804110407p:plain

▼ ログイン後

f:id:mt_816:20210804110414p:plain

また、認証失敗時に関しては、URLパラメータのerrorを参照し、エラーメッセージを表示させています。

▼ ログイン失敗時

f:id:mt_816:20210804135835p:plain

まとめ

以上NextAuth.jsを使用したカスタムログインページ実装についての説明でした。

今回は簡易的な認証機能の例でしたが、NextAuth.jsは、GoogleTwitterFacebookといったサービスの認証機能の組み込みや、データベース・メール送信サービスとの連携も可能となっています。

NextAuth.jsは無料で使用できますので、興味ある方は是非お試しください!

最後になりますが、Wizではエンジニアを募集中です。

興味のある方は是非覗いてみてください↓

careers.012grp.co.jp

npm packageの公開の手順

f:id:thunder_fury:20210727170909p:plain

はじめに

皆さんこんにちは、フロントエンドエンジニアのWooです。⚡️🌪

npmモジュール(npm パッケージ)を作成しデプロイしてオープンソースとして使えるところまでやってみたいと思います。

npm (Node Package Manager)

開始する前に、簡単にnpmについて調べてみたいと思います。

npmの役割はパッケージ管理ツールです。 ソフトウェアを管理し、インストールをサポートしてくれます。 この記事を書いた時点では公開のパッケージ数約1,688,642個以上のnpmパッケージが公開されており現在もまだnpmのパッケージが増えています。

npm インストール

Node.jsをインストールするとnpmも同時にインストールがされます。 下記のリンクでNode.jsのインストールが簡単にできます。

nodejs.org

package.json

package.jsonを生成するためにはプロジェクトのフォルダからnpm initのコマンドを叩く必要があります。 Package.jsonは開発に必要な情報などを記載する場所であり、必要なパッケージをインストールする時に自動で記載をしてくれます。

package-lock.json

package-lock.jsonは各パッケージの依存関係の管理をするファイルです。

npm initを実行した時には生成されませんが、このnpmパッケージをインストールしたり、変更、削除などの操作を行うと生成されます。

npmユーザー登録

npm公式ページで登録することができます。 お名前とメールアドレス、パスワードを入力した後、加入したメールアドレスに本人認証メールが届くので承認したらnpmパッケージをデプロイすることができます。

www.npmjs.com

npmパッケージ開発

npm 初期化

プロジェクトのフォルダ(リポジトリ)を作成してコマンドを叩きます。

npm init -y

package.jsonが生成されるので下記のように設定を行います。

{
  "name": "@[npmid]/[パッケージ名]", // <-追加
  "version": "1.0.0",
  "description": "どんなパッケージのかの説明", // <-追加
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": { // <-追加
    "type": "git",
    "url": "リポジトリのURL" 
  },
  "bugs": { // <-追加
    "url": "リポジトリのissuesのURL"
  },
  "homepage": "リポジトリのreadmeURL", // <-追加
  "keywords": [
    "キーワード", // <-追加
    "keyword" // <-追加
  ],
  "author": "開発者", // <-追加
  "license": "MIT" // <-変更
}

注意点としてはlicenseの設定ですが、自分はMITによくします。 MITとは、「コードは好きに使っても良いけど、公開されたコードを使って何か問題が起きた場合、作者は責任とらないよ」というlicenseで、 オープンソースにはMITライセンスをよく使うらしいです。 他にもいろいろなlicenseがありますので調べてみてください。


npmパッケージ作成

rootにindex.jsを作成します。
下記は簡単挨拶文を出力する簡単なサンプルコードです。

export class Test {
  constructor(name) {
    this.name = name
  }
  action() {
    return `Hello ${this.name}`
  }
}

その他最低限必要なファイルを追加してモジュールのアップロードの準備を完了します。

root/
├── index.js
├── .gitignore
├── .npmignore
├── README.md
├── LICENSE
└── package.json

npmログイン

ログインはディレクトリのrootからコマンドを叩きます。

> npm login

Username: 登録したID
Password: 
Email: (this IS public) 登録したメールアドレス
Logged in as youngsoohan on https://registry.npmjs.org/.

ログインが成功されたらアクセストークンが発行されコマンドでデプロイすることができます。 トークンはマイページで確認することができます。

npm デプロイ

デプロイは上記のpackage.jsonに記載したnameがパッケージ名になります。
約1,688,642パッケージの中から被らないパッケージ名で公開しなければいけません。

そのため@useridを頭につけると被ることはないので一般的には@useridをつけてデプロイをすることが多いです。

例)@npmid/modalのような感じです。

 "name": "@[npmid]/[パッケージ名]"

バージョンも設定する必要があり、npm init -yの時、defaultで1.0.0が記されます。
自分は正式のリリースとして問題なければ1.0.0でアップを行い、その前までの開発段階では0.0.1から設定して公開するようにしてます。
今回はサンプルとして公開し後ほど削除するので1.0.0で公開しました。

 "version": "1.0.0",

公開コマンド(下記は無料プランのコマンドです。)

npm publish --access public

npmのマイページに接続したらちゃんと公開されています!

f:id:thunder_fury:20210728140437p:plain

npmのデプロイ完了しましたー🔥

公開したパッケージ使ってみる

npm i @npmid/test
import { Test } '@npmid/test'

const test = new Test(`npm`)
console.log(test.action())

f:id:thunder_fury:20210728180556p:plain

npmにちゃんと挨拶をしていますので問題なく公開はされているようです。

npm パッケージ削除

注意点としては最後に消したいバージョンを書かないといけないのと、時間過ぎてしまうと自分で消すことができずnpmに問い合わせをしないといけないようなので必要でなければすぐ消しましょう⚡️。

npm unpublish @userid/test@1.0.0

最後に

今回の内容はサンプルとしての内容ですが実際にはフォームのバリデーションのパッケージを開発し公開しました。 初めて開発をして色々調べながらですがとても勉強になりました。
実際公開したパッケージをどんどんプロジェクトに導入し工数削減を目指して行きたいと思っております。


最後になりますが、Wizではエンジニアを募集中です!

興味のある方は是非覗いてみてください!↓

careers.012grp.co.jp

【Laravel】ローカルスコープから考える要求と意図

こんにちは。バックエンドエンジニアの河内です。

LaravelのEloquent ORMにクエリスコープという機能があります。 その中に、ローカルスコープという機能があって、それを使うと頻繁に利用するクエリ条件を1箇所にアクセスしやすい形でまとめることができます。

このローカルスコープを使うと、コードが非常に読み下しやすくなるんですね。それはなぜなんだろう、というお話です。

ローカルスコープ使い方

公式からの抜粋ですが、たとえば「人気のあるアクティブユーザー」だけをDBから取得したい場合、以下のように「人気のある」スコープ(=scopePopular)と「アクティブ」スコープ(=scopeActive)を用意します。

ここで想定されている仕様については、「人気のある」とは「票数が100より大きい」であり、「アクティブ」とは「アクティブフラグが立っている」…と読み取れます。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * Scope a query to only include popular users.
     *
     * @param  \Illuminate\Database\Eloquent\Builder  $query
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopePopular($query)
    {
        return $query->where('votes', '>', 100);
    }

    /**
     * Scope a query to only include active users.
     *
     * @param  \Illuminate\Database\Eloquent\Builder  $query
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeActive($query)
    {
        return $query->where('active', 1);
    }
}

これらのスコープを利用するには、以下のように書きます(チェーンできます)。利用時に、scopeプレフィックスは不要で、scopeを取り去ったあとの単語を小文字で呼び出します。

  • scopePopular → popular
  • scopeActive → active

ですね。

<?php$users = User::popular()->active();

逆に、スコープを利用しないと以下のような同じような条件を何度も書くことになります。

<?php
    :
    $users = User::where('votes', '>', 100)->where('active', 1);

たとえば、「人気」の基準が、「票数が200より大きい」という仕様に変更になった場合、個別に書いて回ったソースコードを書き換える旅が始まってしまいます。

処理をまとめるのはプログラムの基本ですが、スコープで条件が書かれていると、読み下しやすいのもポイントです。

要求と意図

なぜ読み下しやすいのでしょうか。それは、たとえばscopePopularについていえば、「票数が100より大きい」ユーザーを取得するという論理的な「要求」が、「人気のある」という「意図」である、という位置付けに変換されたから…だと考えます。

ここでの「要求」と「意図」という単語は、スケールする要求を支える仕様の「意図」と「直交性」という記事の文脈から援用しました。

この「要求」と「意図」、わりと順不同でどちらが発端にもなるのかな、と思っています。

意図発

たとえば、「人気のある」ユーザーだけをトップページに表示したい、という「意図」発で依頼がやってくることも往々にしてあるかと思います。

この場合、「人気のある」とはどういうことなのか…ということを依頼者(DDD*1の文脈ではドメインエキスパート)と協議して「要求」を煮詰めることになります。

要求発

逆に、「要求」発の場合、まさに「意図」を見出だす必要があります。これは依頼者が「意図」を意識的に持っていることもあれば持っていないこともあり、持っていなかった場合、プログラマ側が抽出する必要があります。

「サイトのトップページに『票数が100より大きい』ユーザーの降順に表示したいんだよね」という要求があったときに、「つまり、『人気のある』ユーザーですよね」という意図を見出だし名付ける…ローカルスコープのメソッド名はまさに、その意図についての名付けの好例なのだと思います。

まとめ

Laravelのローカルスコープは、「要求」を「意図」に変換しその「意図」に名付けをおこなっている行為でした。

このローカルスコープの例に限らず、およそプログラミング自体がそういうものなのだ、ということは、DDDなどの設計思想に触れるにつれ痛感します。

また、「意図」の名前は、客観的な「要求」と違い、主観的なより生き生きとしたものになってくるかと思います*2

そういう「意図」で編まれたプログラムは見通しのよいものになるのだと思っています。

最後に

Wizではエンジニアを募集しております。 興味のある方、ぜひご覧下さい。

careers.012grp.co.jp

*1:参照記事にもDDDの話が登場しますね

*2:リーダブルコードでいえば「カラフル」な単語で表現できないか、ということになるのかと思います

Puppeteerを使ったツールの構成について

こんにちは、フロントエンドエンジニアの高橋です。

先日Puppeteerを使った業務効率化のツールを作成しました。 その時参考にさせていただいた記事と一緒にその構成についてまとめたいと思います!

Puppeteerをこれから触る方に少しでもお役に立てればと思います…!

何を作るのか

あまりオープンにできないのですが、webでの操作を半自動化するツールです! 作りたいものの流れとしては以下の通りです。

  • 必要な情報をフォームで入力
  • データをどこかにためておく
  • 入力した値を元にPuppeteerを動かす
  • 結果をSlackで通知する

では構成についてまとめていきたいと思います。

構成

早速ですが、以下のような構成になっております!

アーキテクチャ

順番に何をやっているかをまとめていきます。

①フォームからGASを動かす

1

フォーム作成

まずは必要情報を書き込むためのフォームを作成します。

Google Formを使うことも考えましたが、データを加工する必要がありました。 次のGAS上やCloudFunctions上でもできますが処理が複雑になってしまうため、送信前に加工することにしました。

個人的にReactが好きなのでReactで作成し、フォームはReact Hook Formで作成しました。工数削減のためにCSSは書かずChakra UIを使用しています。

React Hook Form と Chakra UIを組み合わせると本当に爆速でフォームが出来上がります。実は公式でもサンプルが載っていますので気になる方はぜひご確認ください!

GAS連携

おなじみのGASでAPIを公開します。 詳細は以下の記事を参照ください!

①で参考にした/使用したサイト

React Hook Form formbuilder

Chakra UI公式

今から10分ではじめる Google Apps Script(GAS) で Web API公開

②GASからスプレッドシートに保存

2

GASのSpreadsheetAppクラスを使用してSpreadsheetを更新します。

// シート取得
const ss = SpreadsheetApp.openById(SpreadsheetApp.getActiveSpreadsheet().getId());
// dataというシートを取得
const dataSheet = ss.getSheetByName("data");

// データ(res)をシートに入れる
await dataSheet.appendRow([res]);
②で参考にしたサイト

Google Apps Script で Spreadsheet にアクセスする方法まとめ

Spreadsheet-app

③GASからCloudFunctionsに保存している関数を呼び出す

3

こちらもAPI連携できますので、上記のシート追加の後にCloudFunctionsのAPIを呼び出します。

  const url = 'CloudFunctionsのAPI URL'
  const result = await UrlFetchApp.fetch(url, option)

GAS上ではHTTPリクエスト用にUrlFetchAppというClassが用意されているので、そちらを使用します。

③で参考にしたサイト

url-fetch-app

④⑤CloudFunctionsをVPCアクセスコネクタ経由で動かす

4-7

関数を用意する

一番大変な部分ですが、全てを語ると尽きないので以下の参照記事をご確認いただければと思います!

VPCアクセスコネクタ設定をする

VPCとはVirtual Private Cloudの略で、プライベート仮想ネットワーク空間のことです。アクセスコネクタを設定をすることでVPCに接続することができます。

こちらを使用した経緯は、IPアドレスの固定のためです。 今回Puppeteerで操作するサイトがIPアドレスで制限をかけていたりするので、こちらの設定が必須でした。(これに気づけずかなり時間を浪費しました。。)

料金もある程度かかってしまいますので、事前に料金も試算しておいた方がいいかと思います!

④⑤で参考にしたサイト
  • Puppeteer

公式

バージョンによってエラーが出る

ヘッドレスモード時のみエラーが出てしまう時

imageUploadについて

  • Cloud Functions

CloudFunctions実行環境

  • VPCアクセスコネクタ設定

公式 料金について

VPCアクセスコネクタ 設定方法

⑥⑦結果(成功 or 失敗)を返す

最後にSlackで結果を通知する必要があるので、関数内で処理を分ける必要があります。 今回はtry~catchで実装しました。

 try {
    const browser = await puppeteer.launch(options)
    const page = await browser.newPage()
    :
    await browser.close()
    // 完了時の処理
  }catch(e){
    //例外が発生した場合の処理
  }
⑥⑦で参考にしたサイト

Pupeteerを使うときは try~finallyでbrowserをcloseする。

⑧結果をSlackで通知

8

最後にSlackとの連携をしていきます。 登録の流れは下記の記事を参考にしていただければと思います。

関数内からSlackへメッセージを送るには@slack/web-apiというライブラリが便利なのでそちらを使用します。

以下のように書けばメッセージを送ることができます。

const {WebClient} = require("@slack/web-api");
:

const slackClient = new WebClient('slackのtoken');
const slackParams = {
      channel: '#チャンネル名',
      text: 'テキスト'
};
await slackClient.chat.postMessage(slackParamsFinish);

かなり簡単に書くことができました! こちらを先程のtry~catchに反映させていきます。

 try {
    const browser = await puppeteer.launch(options)
    const page = await browser.newPage()
    await browser.close()
    // 処理完了メッセージ送る
    const slackParamsFinish = {
     channel: '#チャンネル名',
     text: 'テキスト'
    };
    await slackClient.chat.postMessage(slackParamsFinish);

  }catch(e){
    //例外が発生した場合の処理
    const slackParamsError = {
     channel: '#チャンネル名',
     text: 'テキスト'
    };
    await slackClient.chat.postMessage(slackParamsError);
  }

こちらでSlackへの連携も完了です!

⑧で参考にしたサイト

Slack APIと連携する

Slack APIを使用してメッセージを送信する

最後に

難しいツールではなかったのですが、初挑戦の部分も多く所々で苦戦していました。相談に乗っていただいたメンバーには感謝です。 構成に関しても改善できる点があると思うので、何か思うことがあればコメントいただけますと幸いです…!

現在Wizではエンジニアを募集中です。 興味のある方はぜひ覗いてみてください!↓

careers.012grp.co.jp

リモート環境におけるエンジニアの新人研修と新人メンターの話

f:id:kdm012:20210716114128j:plain

こんにちは、フロントエンドエンジニア小玉です。

本記事は、今年度4月〜6月までリモート環境下で行われたフロントエンドエンジニア新人研修について、
その内容と振り返りに加えて、エンジニア歴約1年半の自分がメンターとして教育に携わった所感を記したいと思います。

研修概要

研修目的JavaScriptを伴わないサイトの運用ができるようなレベルになる』
研修対象:新卒社員1名(メンティー
研修実施者:メンター2人(うち一人が小玉) オブザーバー1名
研修期間:2021年4月〜6月
研修場所:oVice(オンラインオフィス)or GoogleMeet

研修内容

新卒社員が入社してからの大まかな研修の流れとして、以下のようになっています。

  1. 会社概要研修
  2. 技術研修
  3. OJT

会社概要研修

こちらでは自分が所属している部署(エンジニア組織)が会社においてどのような役割を担っているのかを理解すること、また社会人としての基礎的なマナーを身につけることを目的としています。

技術研修

以下のような内容を座学と実践的な課題を交えて研修して行きます。

□Webの基礎知識

  • Webページが表示されるまでの仕組み

□開発環境について

□フロントエンドの基礎知識

また、技術研修の中ではスキルの習得に加えてフロントエンドとしての心構えや振る舞いについても研修します。
エンジニアとして仕事をしていると、どうしてもわからない課題やエラーに直面し、思ったように作業が進まないことがあるかと思います。そんな時に上司やチームのメンバーに報告・連絡・相談することの大切さや時間管理の重要性などを研修します。

OJT

メンティーに対しメンターが一人付き、メンティーは実際の案件をメンターと一緒にこなしていきます。
その中では、他人の書いたコードを読み解くことができるようになることや、 複数人で1つのプロジェクトを運用できるようになることが求められます。

研修の振り返り

リモート環境下での研修

今回実施した研修では一度も実際にあって研修を行うことはなく、全てオンラインでの研修となり、
その中で以下のような点に注意するべきだと感じました。

  • 雑談の時間を積極的に取る
  • 日々の振り返りシートなどを活用する

【雑談の時間を積極的に取る】
顔の見えない研修となると、淡々と座学や実習を進めがちになってしまいます。
メンティーがどのようなリアクションをとっているのかは声からでしか判断できないため、
仮に課題に詰まってしまった時にメンティーが質問のしやすい雰囲気を作る必要があります。

私たちの研修の中では朝イチから研修にすぐ入るわけではなく、
仕事と関係のない雑談をしてから研修に入るようにしていました。

【日々の振り返りシートなどを活用する】
雑談を取り入れることに通ずる部分はありますが、
メンティーの表情や仕草から課題への理解度は測りきれないのが正直なところです。
そこはメンティー本人にアウトプットしてもらう仕組みが必要です。

私たちは、日々の研修後にメンティーに振り返りシートを記載してもらい、それに対しメンターがコメントを返すようにしていました。そこから次の研修時不足分を補うようなカリキュラムに変更するという柔軟な対応ができたかと思います。

メンター制度について

今回の研修はメンティー1人に対し、メンターが2人付く体制で実施しました。 初めて教育をする側の立場になり感じたこととして、『ティーチング < コーチングの意識』が重要だと感じました。
どのような意図でコードを書いたのか、何に悩んでいるのか、どうしたらできるのか、しっかりヒアリングし、すぐに回答を与えるのではなく、考える方法や何かヒントを与えることでメンティーのさらなる理解が望めると思いました。

まとめと今後について

リモート環境下での研修内容と、その振り返りに加えメンターをやってみた所感をまとめてみました。
メンティーとの密なコミュニケーションを取り心理的安全性を担保した研修が実施できるよう今後も施策を考えて行きたいと思います。
また、新卒社員を研修カリキュラムを事前に作成し研修をしっかり行うのが組織として今回が初めてだったこともありメンティーやメンターの評価制度も改善する余地があります。 引き続きみんながハッピーになれる研修、組織にできるようがんばります。

今回あげた研修内容とは別に、新人教育の一環としてリアルタイムコーディング大会を開いたりもしています。
詳細はこちら↓

tech.012grp.co.jp

Wizではエンジニアを募集しています。興味のある方、ぜひご覧ください。

careers.012grp.co.jp

Laravel×クリーンアーキテクチャ

どうもバックエンドエンジニアの小室です。

最近クリーンアーキテクチャについて理解が以前に比べ深まってきました。

まとめがてら、簡単なユーザー作成機能をクリーンアーキテクチャを使い実装していきたいと思います。

まずクリーンアーキテクチャといえば

f:id:shuto_komuro:20210706151155j:plain
クリーンアーキテクチャ
お馴染みの円の図です。

レイヤーの説明

・Enterprise Business Rules

ビジネスロジックを表現するレイヤーになります。DDDによる設計が最も影響し、

DDDでいうEntity, Value Object, Domain Serviceが該当します。

・Application Business Rules

ビジネスロジックを組み合わせ、ソフトウェアは何ができるのか(UseCase)を表現するレイヤーになります。

・Interface Adapters

入力、永続化、出力を司るレイヤーになります。

・Frameworks & Drivers

フレームワークやDBなど、詳細な技術についてを置くためのレイヤーです。 このレイヤーでは、Interface Adapterが理解できる形へ変換される必要があります。

レイヤー間の依存方向

f:id:shuto_komuro:20210709001032p:plain
依存方向

この矢印の意味は依存方向を表します。 依存方向は上位レベルのレイヤーに向かっていきます。

f:id:shuto_komuro:20210709002232p:plain
flow of control

大事なのはここ、リクエストからレスポンスまでの流れを表しています。 もっと詳しく表したが図こちら。

f:id:shuto_komuro:20210709002909j:plain
flow of control

クリーンアーキテクチャのメリット

フレームワーク独立

アーキテクチャは、ソフトウェアのライブラリに依存しない。

フレームワークを道具として使うことを可能にし、システムはフレームワークの限定された制約に縛られない。

・テスト可能

ビジネスルールは、UI、データベース、ウェブサーバー、その他外部の要素なしにテストできる。

・UI独立

ビジネスルールの変更なしに、UIを置き換えられる。

・データベース独立

OracleあるいはSQL Serverを、Mongo, BigTable, CouchDBあるいは他のものと交換することができる。

ビジネスルールは、データベースに拘束されない。

・外部機能独立

ビジネスルールは、単に外側についてなにも知らない。

原文: Clean Coder Blog

クリーンアーキテクチャにおいて大事(コア)な考え

重要なものを些細なものに依存させない。

重要なもの=ビジネスルール

ビジネスルールは図でいう

・赤色の層(Entity: Application Business Rules)

・黄色の層(UseCase: Enterprise Business Rules)

の2層の部分です。

些細なもの = DB, UI, フレームワークなど

重要なものを些細なものに依存させないために「依存関係の逆転」を行う。

※「依存関係の逆転」後述してます。

実装コード

ルーティング

<?php

Route::post('/createUser', [\App\Http\Controllers\UserController::class, 'createUser']);

Controller

<?php

namespace App\Http\Controllers;

use App\Packages\Domain\User\Dto\UserInputData;
use App\Http\Requests\UserCreateRequest;
use App\Packages\UseCase\User\CreateUserUseCaseInterface;

class UserController extends Controller
{
    /**
     * @param UserCreateRequest $request
     * @param CreateUserUseCaseInterface $userCreateUseCase
     * @return \Illuminate\Http\JsonResponse
     */
    public function createUser(
        UserCreateRequest $request,
        //②Input Boundary<I>
        CreateUserUseCaseInterface $userCreateUseCase
    )
    {
        //①Input Data<DS>
        $userInputData = new UserInputData($request['name'], $request['tel']);
        //③UseCase Interactor
        $userOutputDataResponse = $userCreateUseCase->handle($userInputData);

        return response()->json([
            'uuid' => $userOutputDataResponse->getUuid()
            ],200);
    }
}

①でリクエスデータをアプリケーション用Input Dataに変換します。

②インターフェースクラスを定義してメソッドインジェクション(DI)をしています。

DIについては Laravelで始める依存性の注入(DI) - Qiita

環境変数に応じてRepositoryをInMemoryに切り替えたりもできます。

<?php

namespace App\Providers;

class AppServiceProvider extends ServiceProvider
{
    public function register()
    {
        if(app()->environment() == 'local') {
            $this->app->bind(
                'App\Packages\Infrastructure\User\RepositoryInterface',
                'App\Packages\Infrastructure\User\InMemoryRepository'
            );
            $this->app->bind(
                'App\Packages\UseCase\User\CreateUserUseCaseInterface',
                'App\Packages\UseCase\User\CreateUserInteractor'
            );
        }else{
            $this->app->bind(
                'App\Packages\Infrastructure\User\RepositoryInterface',
                'App\Packages\Infrastructure\User\UserRepository'
            );
        }
    }

③UseCase InteractorにInput Dataを渡します。

Input Data<DS>

<?php

namespace App\Packages\Domain\User\Dto;

class UserInputData
{
    private $name;
    private $tel;

    public function __construct($name, $tel)
    {
        $this->name = $name;
        $this->tel = $tel;
    }

    /**
     * @return mixed
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * @return mixed
     */
    public function getTel()
    {
        return $this->tel;
    }
}

?>

いわゆるDTOってやつです。 ※ <DS>はData Structure

Input Boundary

<?php

namespace App\Packages\UseCase\User;

use App\Packages\Domain\User\Dto\UserInputData;

interface CreateUserUseCaseInterface
{
    public function handle(UserInputData $userInputData);
}

?>

Use Caseのインターフェースクラスになります。

実装クラス(UseCase Interactor)で必要となるメソッド名を定義します。

DIを使うことで、Controller層はUseCase層の実際の実装クラス(UseCase Interactor)ではなく、

インターフェースクラス(Input Boundary)に依存することになり、Use Case層への依存度を緩和します。

※ <I>はInterface

UseCase Interactor

<?php
namespace App\Packages\UseCase\User;

use App\Packages\Domain\User\User;
use App\Packages\Domain\User\Dto\UserInputData;
use App\Packages\Domain\User\Dto\UserOutputData;
use App\Packages\Domain\User\VO\Name;
use App\Packages\Domain\User\VO\Tel;
use App\Packages\Infrastructure\User\RepositoryInterface as UserRepositoryInterface;

class CreateUserInteractor implements CreateUserUseCaseInterface
{
    private $userRepository;

    //①Data Access Interface<I>
    public function __construct(UserRepositoryInterface $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    /**
     * @param UserInputData $userInputData
     * @return UserOutputData
     * @throws \Exception
     */
    public function handle(UserInputData $userInputData)
    {
        $uuid = uniqid("user_");
        //②Entity
        $user = new User(
            $uuid,
            new Name($userInputData->getName()),
            new Tel($userInputData->getTel()));
        //③Data Access
        $this->userRepository->save($user);
        //本来はPresenterを用意するが、MVCFWの都合上OutPutDataをレスポンス
        $userOutputData = new UserOutputData($uuid);
        return $userOutputData;
    }
}

?>

UseCase Interactorではアプリケーションロジックを実装します。

①Data Access InterfaceはRepositoryのインターフェースクラスになります。

実装クラスで必要となるメソッドを定義します。

②User Entiryのオブジェクトを生成しています。

③User EntiryのオブジェクトをRepositoryに渡し、保存します。

Entity

<?php

namespace App\Packages\Domain\User;

use App\Packages\Domain\User\VO\Name;
use App\Packages\Domain\User\VO\Tel;

class User
{
    private $uuid;
    private $name;
    private $tel;

    public function __construct(
        $uuid,
        Name $name,
        Tel $tel
    )
    {
        $this->uuid = $uuid;
        $this->name = $name;
        $this->tel = $tel;
    }

    /**
     * @return string
     */
    public function getUuid() :string
    {
        return $this->uuid;
    }

    /**
     * @return Name
     */
    public function getName() :Name
    {
        return $this->name;
    }

    /**
     * @return Tel
     */
    public function getTel() :Tel
    {
        return $this->tel;
    }

    /**
     * @param Name $name
     */
    public function changeName(Name $name)
    {
        $this->name = $name;
    }
}

?>

エンティティはビジネスルールをカプセル化したオブジェクトです。

クリーンアーキテクチャでいうEntityはDDDでいうEntityとは定義幅が異なります。

DDDドメインモデルはEntityとValue Objectとドメインサービスを含んだものを指します。

クリーンアーキテクチャのEntityは、DDDドメインモデルにあたります。

ドメイン駆動設計のエンティティと比べるとその定義幅はかなり広いです。

UseCase層で受け取ったデータをもとにEntityのオブジェクトを作成しています。

EntityのValueObject

<?php

namespace App\Packages\Domain\User\VO;

use Exception;

class Name
{
    private $value;

    public function __construct(string $value)
    {
        if(strlen($value) < 3) throw new Exception();
        if(strlen($value) > 20) throw new Exception();
        $this->value = $value;
    }

    /**
     * @return string
     */
    public function getValue() :string
    {
        return $this->value;
    }
}

?>

User Entiryを構成するValue Objectになります。

DataAccessInterface

<?php

namespace App\Packages\Infrastructure\User;

use App\Packages\Domain\User\User;

interface RepositoryInterface
{
    /**
     * @param User $user
     * @return mixed
     */
    public function save(User $user);

    /**
     * @param $userId
     * @return mixed
     */
    public function find($userId);
}

?>

Repositoryのインターフェースクラスを定義しています。

DataAccess

<?php

namespace App\Packages\Infrastructure\User;

use App\Packages\Domain\User\User;
use App\Models\User as UserModel;

class UserRepository implements RepositoryInterface
{
    /**
     * @param User $user
     * @return mixed|void
     */
    public function save(User $user)
    {
        $userModel = new UserModel();
        $userModel->uuid = $user->getUuid();
        $userModel->name = $user->getName();
        $userModel->tel = $user->getTel();
        $userModel->save();
    }

    public function find($userId)
    {
        $userModel = UserModel::findOrFail($userId);
        ...
    }
}
?>

Repositoryクラス(Data Access)を定義しています。

Repository Interface(Data Access Interface<I>)を継承しています。

DIにより、Data AccessはUseCase層であるData Access Interface<I>に依存することになります。

InMemoryRepository

<?php

namespace App\Packages\Infrastructure\User;

use App\Packages\Domain\User\User;

class InMemoryRepository implements RepositoryInterface
{
    protected $data;

    public function __construct()
    {
        $this->data = collect();
    }

    public function all()
    {
        return $this->data;
    }

    public function save(User $user)
    {
        $this->data->push([
            'name' => $user->getName(),
            'tel' => $user->getTel()
            ]);
    }

    public function find($userId){}
}

DIにより、ロジックが疎結合になります。

特定のインフラストラクチャに依存しないようにロジックを記述できます。

インメモリに切り替えることも容易になります。

まとめ

俯瞰してみると、重要なビジネスルールであるEntity層とUseCase層はフレームワークを変えたとしても、

特に変更はなくそのまま使うことができる様な形になり、フレームワークに依存していないと、見て取れます。

UIや、データベースなどの変化しやすいレイヤーと

ビジネスロジックという変化しづらいレイヤーを分離することができました。

最後に

Wizではエンジニアを募集しております。

興味のある方、ぜひご覧下さい。

careers.012grp.co.jp