ryota21silvaの技術ブログ

Funna(ふんな)の技術ブログ

これまで学んだ技術の備忘録。未来の自分が救われることを信じて

Railsの利点と欠点、RailsがDDDや大規模開発に向いていない理由

知り合いが「Railsの長所ってなんやっけ?」って話をしてたので、その辺のRailsの長所・短所とか、RailsがDDDや大規模開発に向いてない理由を自分なりに整理してみた。

間違っている点などあれば是非ご指摘頂きたいです。


※2023/07/21 追記:
本記事においてユースケースという言葉を多用していますが、文脈(コンテキスト)という言葉を使用すべきだったかもしれません🙏
DDDにおいてモデルはユースケースごとに分かれるのではなく文脈ごとに分かれるものであり、本記事でも文脈というニュアンスでユースケースという言葉を使用していましたが、適切な言葉を選ぶべきでした。
ex. 商品というモデルを販売コンテキスト、配送コンテキストに分ける(参考: 境界づけられたコンテキスト 概念編 - ドメイン駆動設計用語解説 [DDD] - little hands' lab)。


Rails(Active Record)の利点と欠点

RailsというかActive Recordの利点と欠点を話しているだけ。

  • Railsの利点
    1. RDBのテーブルとActive Recordのモデルが一対一になり直感的でわかりやすい。CRUD機能、バリデーション、ロジックなどをすべてActive Recordモデルに書けるので短期的には爆速で開発できる。
    2. これはORM全般に共通する利点なので蛇足ですが、オブジェクト(モデル)を直感的に操作するだけでDBアクセスする処理を書ける。生SQLを直接書かずに済む(どんなSQLが発行されているかは確認しないとですが)

 つまりViewやControllerなどあらゆるコードがActiveRecordと密結合になるので、例えばViewに渡っているデータの表示方法を変更したいときはActiveRecordモデルのコードをイジるだけで済んじゃったり。 短期的な開発効率が上がるので爆速でMVPをリリースできるし、スタートアップの開発に向いている。

  • Railsの欠点
    1. テーブルとActiveRecordモデルが一対一の関係にあるため、例えばUserに関するデータ・振る舞い(CRUD操作も含む)・制約などがActiveRecordのUserモデルに集約されがち。
      => Userというモデルは用途やユースケースに応じて持つべきデータ・振る舞い・制約が異なる可能性があるのに、ActiveRecordのUserクラスにあらゆるユースケースの処理を生やしていくと1つ1つのメソッドがどのユースケースで使われるべきか分かりづらくなる。例えばnameプロパティにバリデーションをかける場合もユースケースによって制約内容が異なるかもしれない。
      ActiveRecordは特定の1つのユースケースに特化したコードを書くことには向いているが、複数のユースケースに対応するコードを書く場合(テーブルとモデルが一対一とならない場合)に苦労する。

    2. Active RecordモデルがControllerやViewに渡りやすいことから、あらゆるレイヤーがActiveRecordと密結合な状態になってしまう。
      => 1とつながる内容ですが、アプリケーションが複数のユースケースを持ち始めた場合に問題が生じてくる。複数のユースケースと密結合なActiveRecordモデルのオブジェクトがあらゆるユースケースのViewやControllerに渡ってしまうと「ユースケースAではどのデータ(プロパティ)、どのメソッドを用いるのか分からない、、」ってなるし、変更漏れが発生しそう。
      そして複数のユースケースのロジックを1つのActiveRecordモデルに記述すると可読性が落ちるし責務の大きい肥大モデルが生まれてしまう。

つまり、RailsActiveRecordには

などの処理が集約されているため、複数のユースケースに考慮したコードが書き辛い

この辺りはRuby on Railsの正体と向き合い方 / What is Ruby on Rails and how to deal with it? - Speaker Deckがめっちゃ分かりやすかったです。
以下に一部引用文を抜粋。

Railsはいつ、なぜ限界を迎えるのか?
・いつ:あるModelが複数の異なるユースケースでC(R)UD操作されるようになったとき。
・なぜ:あるModelに書かれたValidations/Callbacksは特定のユースケースと密結合しているため。
・何が辛いのか:あるユースケースに特化したModelの中で、別のユースケースの事情を考慮したコードを書かなければならないこと。


じゃあRailsで複数のユースケースにどうやって対応するの?

RailsやLaravelで複数のユースケースに対応した長期的な開発を取り組むためにDDDを実践したい場合は、Active Recordモデル(Eloquentモデル)をそのままViewやControllerに返すのではなくユースケースに沿ったドメインオブジェクトを返してあげるのが良い。
そしてInfra層のリポジトリ実装クラスでは、Active Record(Eloquent)のORMのみを参照する(DDDではDomain層のRepositoryでリポジトリインターフェイスを定義し、Infrastructure層で永続化の処理を行うので)。
そして他のどのレイヤーからもActiveRecordの機能を参照しないのが良さそう?参照するならインターフェイス噛まして隠蔽するのが良いんかな。(Railsインターフェイスってどんなやったっけ)


おわりに

そもそもRailsの長所は短期的に爆速で開発できることであって、大規模開発には向いてないみたい。
でもRailsライクで生まれたCakePHPはCakeのエンティティ(DDDのエンティティとは別)と密結合になりがちやし、LaravelもEloquentモデルと密結合になりやすいから、Railsに限らん話では?と思った。

その他参考資料

【Go】Goでドメインオブジェクトをどのようにして生成すべきか

ゴール

  • ドメインルールを守ったオブジェクトであることを保証したい。
  • Goの言語使用上外部からの変更(同一パッケージ内からの変更)を完全に防ぐことは難しいが、それでもなるべくドメインオブジェクトの不変性を保ちたい。

方針その1. 構造体のフィールドをパッケージ内でのみアクセス可能にする

以下の手順で実装する。

  • 構造体のフィールドの先頭を小文字にする
  • コンストラクタを用意する
  • パッケージ内でのみアクセス可能なフィールドを変更するためのメソッドを用意する
  • Getterを用意する
type User struct {
    // フィールドを小文字にすることで、パッケージ内でしか取得・変更できないようにする
    id int
    name string
    email string
    password string
    createdAt time.Time
    updatedAt time.Time
}

// コンストラクタを用意する
func NewUser(id int, name string, email string, password string, createdAt time.Time) *User {
    return &User{
        id:        id,
        name:      name,
        email:     email,
        password:  password,
        createdAt: createdAt,
        updatedAt: createdAt,
    }
}

// 「パッケージ内でのみアクセス可能なフィールド」を変更するためのメソッド
func (u *User) ChangeName(name string) {
    u.name = name
}

// Getterを用意する
func (u *User) CreatedAt() time.Time {
    return u.createdAt 
}
// 外部packageのコード
user, err := NewUser(1, "John")
user.ChangeName("Bob") // 変更可能
user.userName = "Mike" // コンパイルエラー

デメリット

Package内では破壊的変更が可能

  • いわゆるOOPにおけるProtectedでの制限はできるが、Privateでの制限はできない。
  • 以下のようなパッケージ構成をすれば破壊的変更を防げるが、管理が大変かつDDDのパッケージ構成にそぐわないと思う。

      ├── domain
      │   ├── user
      │   │   └── user.go
      │   ├── userid
      │   │   └── user_id.go
      │   └── username
      │       └── user_name.go
    

Getterを用意するのが面倒

フィールドごとにGetterを用意する必要がある。

type User struct {
  id int
  userName string
}

func (u *User) ID() int {
  return u.id
}

func (u *User) UserName() string {
  return u.userName
}

方針その2. インターフェイスを用いることでコンストラクタの使用を強制する

インターフェイスを用いてエンティティや値オブジェクトを生成すればコンストラクタの使用を強制することができるため、ドメインルールを守ったオブジェクトであることを保証できそう。

// インターフェイス
type UserNameInterface interface {
    Get() string
}

// 実装側の構造体を先頭小文字にする
type userNameImpl struct {
    UserNameInterface  // 省略可能だがどのインターフェイスを実装しているかあえて明示している
    username string
}

// インターフェイスを満たすためのメソッド
// Goでは「インターフェースの中にある同じ名前のメソッドを全て定義すれば、自動的にインターフェイスを実装したことになる
func (i *userNameImpl) Get() string {
    return i.username
}

// コンストラクタ
func NewUserName(s string) (UserNameInterface, error) {
    if err := validateUsername(s); err != nil {
          return nil, err
    }

    return &userNameImpl{username: s}, nil
}

// バリデーション
func validateUsername(s string) error {
    s = strings.Trim(s, " ")
    c := utf8.RuneCountInString(s) // 日本語(マルチバイト)の文字列を数える
    switch {
    case c == 0:
        return fmt.Errorf("名前を入力してください")
    case c > 10:
        return fmt.Errorf("名前は10文字以内で入力してください")
    }
    return nil
}
u := UserNameInterface{} // エラー
u := userName{username: "hoge"} // エラー
u, err := NewUserName("hoge") // OK
if err != nil {
  // エラーハンドリング
}

結論

方針その2「インターフェイスを用いることでコンストラクタの使用を強制する」が良さげ。

実装例

コードの例があまり良くないかもだが、備忘録として実装したコードを書き記しておく。

  • UserBookエンティティ
package userbook

import (
    "github.com/ryota1116/stacked_books/domain/model/book"
    "time"
)

type UserBookInterface interface {
    UserId() UserIdInterface
    BookId() BookIdInterface
    Status() StatusInterface
    Memo() MemoInterface

    ChangeMemo(value *string) error
}

type userBook struct {
    userId    UserIdInterface
    bookId    BookIdInterface
    status    StatusInterface
    memo      MemoInterface
}

func NewUserBook(
    userId int,
    bookId int,
    status int,
    memo *string,
) (UserBookInterface, error) {
    s, err := NewStatus(status)
    if err != nil {
        return &userBook{}, err
    }

    m, err := NewMemo(memo)
    if err != nil {
        return &userBook{}, err
    }

    return &userBook{
        userId: NewUserId(userId),
        bookId: NewBookId(bookId),
        status: s,
        memo:   m,
    }, nil
}

func (ub *userBook) UserId() UserIdInterface {
    return ub.userId
}

func (ub *userBook) BookId() BookIdInterface {
    return ub.bookId
}

func (ub *userBook) Status() StatusInterface {
    return ub.status
}

func (ub *userBook) Memo() MemoInterface {
    return ub.memo
}

func (ub *userBook) ChangeMemo(value *string) error {
    return ub.memo.changeMemo(value)
}
  • 値オブジェクトであるMemo
package userbook

import (
    "fmt"
    "unicode/utf8"
)

const maxCount = 255 // メモの最大文字数

type MemoInterface interface {
    Value() *string
    changeMemo(value *string) error
}

type memo struct {
    value *string // NOTE: ポインタ型にすることでnilを許容している
}

func NewMemo(value *string) (MemoInterface, error) {
    if err := validate(value); err != nil {
        return nil, err
    }

    // &でポインタ型を生成
    return &memo{value}, nil
}

func (m *memo) Value() *string {
    return m.value
}

func (m *memo) changeMemo(value *string) error {
    if err := validate(value); err != nil {
        return err
    }

    m.value = value
    return nil
}

func validate(value *string) error {
    // nilではない場合、値をチェックする
    if value != nil {
        memoCount := utf8.RuneCountInString(*value)

        if memoCount > maxCount {
            return fmt.Errorf("メモは255文字以下で入力ください。")
        }
    }

    return nil
}
  • 呼び出し元はこんな感じにする。
// エンティティの取得
ub, err := userBookRepository.FindOne(id)
if err != nil {
    // エラーハンドリング
}

// エンティティの更新
value := "更新後のメモの内容"
if err := ub.ChangeMemo(&value); err != nil {
    // エラーハンドリング
}


u, err := userBookRepository.Save(ub)
if err != nil {
    // エラーハンドリング
}

2023/03/26 追記 : ChatGPTさんに考えてもらう

ChatGPTさんに何回質問しても堂々巡りになったので(私のプロンプトが悪いやんなごめんな😇)端的に回答してくれたモノだけ載せておく。 主にChatGPTさんが考えてくれたことは

  1. 構造体のフィールドを小文字にして、同一パッケージ内からのアクセスしかできないように制限する。
  2. コンストラクタを用意して、ドメインルールを保証したオブジェクトを生成する。
  3. Getter、Setterを用意する
  4. Go言語にはアクセス修飾子が無く外部からのアクセスを完全に制限することはできないので、、コーディングルールを設けてチーム内に浸透させる。

ということで「方針その1. 構造体のフィールドをパッケージ内でのみアクセス可能にする」と同じ方法でした。

参考

NoSQLの種類・特徴・良記事をまとめる

仕事でNoSQLに触れる機会がちょくちょくあるので、NoSQLの種類・特徴・良記事を簡単にまとめてみた。

NoSQLとは

特徴

  • 拡張性と分散処理に優れている
  • 大容量データの高速処理が可能
  • データの整合性が緩い

種類

  • キーバリュー型
    • キーとバリューの単純な1対1管理。
    • Dynamo,Redis
  • 列指向型(カラム指向型)

    • キーバリュー型に「列(カラム)」の概念を追加したもの。 行に付与されたキーが複数のカラムを保持しており、必要に応じてカラムを追加することができる。つまり行キーごとのカラム数を動的に増やすことが可能となる。
    • RDBMSがレコード(行)を一塊のデータとして扱うのに対して、列(カラム)方向にデータを扱う。
      • 特定の列の値をまとめて処理することに長けており、列単位での大量集計、大量更新が得意(ある列の値を一斉に更新する etc.)。
      • 一方で複雑な検索・集計、特定の行を抜き出して更新・削除したりするのは苦手。
    • Cassandra,Bigtable,HBase
      • Cassandraについてはこの辺りの記事が良かったです。

        Cassandraでは、A・P(可用性・分断耐性)を担保するのに対し、冗長構成をとったRDBMSではC・A(整合性・可用性)を担保するように設計されています。
        techblog.yahoo.co.jp

      • Cassandraの設計ポイントが分かりやすい。 qiita.com

    www.publickey1.jp

  • ドキュメント型

    • JSON」「XML」といったドキュメントでデータを管理する。1レコード内に複雑な階層関係を入れることができる。ドキュメントはユニークIDで特定できる。
    • 個々のドキュメントのデータ構造が自由なので、事前にテーブルの構造を厳密に決めておく必要がない(スキーマレス)。ある程度決めておかんと地獄になるやろうけど。
    • MongoDB, CouchDB, FireStore
  • グラフ型
    • グラフ構造を備えたデータベースで、ノード(頂点)、エッジ(辺)、プロパティ(属性)の3つの要素から構成され、ノード間の関係性を表現できる。データの構造が従来のリレーショナルではなくネットワーク状になっている場合に、格納・検索の面で威力を発揮する。
    • Neo4j, InfiniteGraph

    グラフデータベースとは何か ~ネットワーク状のデータ構造から瞬時に情報を検索するDBを解説 - アイマガジン|i Magazine|IS magazine

NoSQL図
NoSQL基礎の基礎 - Qiita

雑感

  • Dynamo、Redisは触れた機会が多いけど使っててめっちゃ速い〜てなる。あとドキュメント型はFireStoreで触れたことあったけど、Mongoがドキュメント型であること、AWSにMongo互換のサービスがあることは知らんかった。
  • 逆に列指向型(カラム指向型)のように行キーごとにカラム数を動的に増やせるデータモデルが存在することは仕事で調べるまで知らんかった。
  • とりあえず思うことは、要件定義~設計するときにRDBで何とかしようとしがちやけど、NoSQLを選択肢に入れることでハッピーになれないか、はたまたログを吐き出すだけで要件を満たせないかなど柔軟に考えていきたい。まあ経験がモノを言うんかもしれんけど、、😡

他に参考にした記事たち

【MySQL】VARCHARの型サイズによってパフォーマンスが落ちることは無さそう

テーブル定義でVARCHARの型サイズをどこまで厳密に設定するべきなのか悩んだことがある。
どうやらLENGTHの長さはパフォーマンスに影響を与えないらしい。
無理にLENGTHを短くする必要は無さそう(ただのメモ記事でごめんなさい)。

qiita.com

qiita.com

【TypeScript】Non-null assertion operator(変数の末尾に!)

  • 「この変数はnullやundefinedeではないよ〜」とコンパイラに教えてくれる。つまり、 <T | undefined | null> 型の変数xがあったとして、x! って書いたらnull および undefinedを除外した<T>を生成してくれる。
  • 実行時(JSに変換された後)にこのコードは削除される。
    • !オペレーターはあくまで型チェックをスキップするだけで、実行時に null などが入る可能性は依然としてあることに注意。
      • なのでjestを書く場合などは expect(...).not.tuBeNull() などでチェックしてあげる必要がある。

www.typescriptlang.org

【Go】Enumを扱う

Enum(列挙型)とは

列挙型とは、プログラミング言語やデータベース管理システムなどにおけるデータ型の一つで、複数の異なる定数を一つの集合として定義するもの。多くの言語では “enum” の略号で示される。 列挙型(集合型)とは - 意味をわかりやすく - IT用語辞典 e-Words

とあるように、複数の定数を1つのクラス(型)としてまとめて管理できるもの。

Javaであれば

public enum Fruits{
    ORANGE,
    APPLE,
    BANANA
};

のように書けるみたい。 引数や戻り値の型をEnumに限定できるのでプログラムの堅牢性が増すし、可読性が上がったりする。

qiita.com

www.modis.co.jp

GoでEnumを扱うには?

GoにはJavaにおけるEnum(列挙型)のような機能が無い。
そのため、const定義とその中に iota (※1)を使うことでEnumを扱うのが一般的?かもしれない。

ただし、Goは変数を初期化する際に明示的に値を代入しない場合に、デフォルトで割り振られる値が決まっているので注意。
これをZero Value(ゼロ値)(※2)といい、int型の場合は0で、string型の場合は“”で初期化されるようになっている。

※1: iotaとは定数宣言(const)内で使用される識別子のことで、0から始まる型なしの整数連番を生成してくれる。
※2: Zero Value(ゼロ値)については、ゼロ値を使おう #golang - Qiitaが分かりやすい。



この仕様から、Goでは以下のように定数の割り当てを1からにすればいい。

type Fruit int

const (
    ORANGE Fruit = iota + 1 // 1
    APPLE                   // 2
    BANANA                  // 3
)

以下のfruits := Fruits{}は明示的に値を代入していないので、Zero Valueの仕様により、fruits.Fにはint型のデフォルト値の0が入ってしまう。

そのため iota + 1としておけば、意図した挙動になる。

type Fruits struct {
    F Fruit
}

func main() {
    fruits := Fruits{} // 変数初期化時に値を代入していないので、0が割り振られる

    switch fruits.F {
    case ORANGE:
        fmt.Println(“オレンジ”) // (+1していないと、この分岐を通ってしまう)
    case APPLE:
        fmt.Println(“りんご”)
    case BANANA:
        fmt.Println(“バナナ”)
    default:
        fmt.Println(“エラー”) // この分岐を通ってくれる
    }
}

公式パッケージの例

Goの標準パッケージtimeに用意されている構造体time.Time型を返すtime.Date関数は第2引数にtime.Month型を求めており、

func Date(year int, month Month, day int, hour int, min int, sec int, nsec int, loc *Location) Time

以下のようにtime.Month型はconst定義と iota を使って表現されており、これによって引数の型をEnumであるtime.Monthに限定しようとしていることが分かる(intをベタ書きしてもコンパイル通るとは思うけど)。

(time/time.go)
// A Month specifies a month of the year (January = 1, ...).
type Month int

const (
    January Month = 1 + iota
    February
    March
    April
    May
    June
    July
    August
    September
    October
    November
    December
)

実際使う場合はこんな感じ。

time.Date(2022, time.August, 10, 12, 0, 0, 0, time.UTC)

参考記事

【Go】JSONの入れ子を構造体に変換する

■ はじめに

APIから返ってくるJSON文字列をGolangの構造体に変換したいと思います。

JSONを構造体に変換する基本形

例えば以下JSONを構造体にマッピングしたい場合

{
    "book":{
        "google_books_id": "Wx1dLwEACAAJ",
        "title": "リーダブルコード",
        "authors": ["Dustin Boswell","Trevor Foucher"],
        "description": "読んでわかるコードの重要性と方法について解説",
        "isbn_10": "4873115655",
        "isbn_13": "9784873115658",
        "page_count": 237,
        "published_year": 2012,
        "published_month": 6
    }
}

以下のように記述すればOKです。

type BookRequestParameter struct {
    GoogleBooksId string    `json:"google_books_id"`
    Title         string    `json:"title"`
        Authors   []string      `json:"authors"`
    Description   string    `json:"description"`
    Isbn_10       string    `json:"isbn_10"`
    Isbn_13       string    `json:"isbn_13"`
    PageCount     int       `json:"page_count"`
    PublishedYear   int     `json:"published_year"`
    PublishedMonth   int    `json:"published_month"`
}

JSON入れ子を構造体に変換する

以下のようなJSON入れ子を構造体にマッピングする場合

{
    "book":{
        "google_books_id": "Wx1dLwEACAAJ",
        "title": "リーダブルコード",
        "authors": ["Dustin Boswell","Trevor Foucher"],
        "description": "読んでわかるコードの重要性と方法について解説",
        "isbn_10": "4873115655",
        "isbn_13": "9784873115658",
        "page_count": 237,
        "published_year": 2012,
        "published_month": 6
    },
    "memo": {
        "body": "メモです。"
    }
}

以下のどちらかの方法で構造体をフィールドに持つ構造体を定義してあげれば良いです。

type RegisterUserBookRequestParameter struct {
    Book BookRequestParameter `json:"book"`
    Memo MemoRequestParameter `json:"memo"`
}

type BookRequestParameter struct {
    GoogleBooksId string    `json:"google_books_id"`
    Title         string    `json:"title"`
    Description   string    `json:"description"`
    Isbn_10       string    `json:"isbn_10"`
    Isbn_13       string    `json:"isbn_13"`
    PageCount     int       `json:"page_count"`
    PublishedYear   int  `json:"published_year"`
    PublishedMonth   int     `json:"published_month"`
    PublishedDate   int  `json:"published_date"`
}

type MemoRequestParameter struct {
    Body          string    `json:"body"`
}
type RegisterUserBookRequestParameterr struct {
    Book struct  {
        GoogleBooksId string    `json:"google_books_id"`
        Title         string    `json:"title"`
        Description   string    `json:"description"`
        Isbn_10       string    `json:"isbn_10"`
        Isbn_13       string    `json:"isbn_13"`
        PageCount     int       `json:"page_count"`
        PublishedYear   int  `json:"published_year"`
        PublishedMonth   int     `json:"published_month"`
        PublishedDate   int  `json:"published_date"`
    } `json:"book"`
    Memo struct {
        Body          string    `json:"body"`
    }`json:"user_book"`
}

実際にAPIを叩いて受け取ったレスポンスを構造体に変換してみる

今回はGoogle Books APIを題材にしてみます。 Google Books APIhttps://www.googleapis.com/books/v1/volumes?q=検索したい書籍名の形式でリクエストを投げられます。

以下のコードはGoogle Books APIから受け取ったJSON形式のレスポンスをResponseBodyFromGoogleBooksApi構造体に変換しています。

// ResponseBodyFromGoogleBooksApi : GoogleBooksAPIを叩いた時のJSONレスポンスを格納する構造体
type ResponseBodyFromGoogleBooksApi struct {
    Items []Item `json:"items"`
}

type Item struct {
    ID         string     `json:"id"`
    VolumeInfo VolumeInfo `json:"volumeInfo"`
}

type VolumeInfo struct {
    Title               string               `json:"title"`
    Authors             []string             `json:"authors"`
    PublishedDate       string               `json:"publishedDate"`
    Description         string               `json:"description"`
    IndustryIdentifiers []IndustryIdentifier `json:"industryIdentifiers"`
    PageCount           int                  `json:"pageCount"`
}

type IndustryIdentifier struct {
    Type       string `json:"type"`
    Identifier string `json:"identifier"`
}

type GoogleBooksApiClientInterface interface {
    SendRequest(searchWord string) (ResponseBodyFromGoogleBooksApi, error)
}


func (client googleBooksApiClient) SendRequest(searchWord string) (model.ResponseBodyFromGoogleBooksApi, error) {
  searchURL := "https://www.googleapis.com/books/v1/volumes?q=" + searchWord

  // GoogleBooksAPIを叩く
  res, err := http.Get(searchURL)
  if err != nil {
    // エラーハンドリング
  }

  defer res.Body.Close()

  body, err := ioutil.ReadAll(res.Body)
  if err != nil {
    // エラーハンドリング
  }

  // JSONエンコードされたデータをparseして、構造体の変数responseBodyFromGoogleBooksApiに格納する
  var responseBodyFromGoogleBooksApi model.ResponseBodyFromGoogleBooksApi
  if err := json.Unmarshal(body, &responseBodyFromGoogleBooksApi); err != nil {
    // エラーハンドリング
  }

  return responseBodyFromGoogleBooksApi, nil
}

JSONを構造体に変換する便利ツール

JSONを構造体の形式にマッピングするためには、json-to-goを使うと便利です。

json-to-go

【Git】git rebase -iでpush済みのコミットまとめたれ

コミットをまとめる

コミットBとCをまとめたい

$ git log -n 3                                                                                                                                                              
commit 78dbdc4439854253a4c65a23aad687488bcf2051 (HEAD -> feature/hogehoge)
Author: ryota1116
Date:   Tue Sep 21 01:27:14 2021 +0900

    コミットC

commit 185f507a6a32fd93505db1cd05b6effa23f45532
Author: ryota1116
Date:   Tue Sep 21 01:14:21 2021 +0900

    コミットB

commit f19845709c26dea827c6097279472ecc3ddda491
Author: ryota1116
Date:   Tue Sep 21 01:10:07 2021 +0900

    コミットA

git rebase -iでコミットをまとめる。

$ git rebase -i HEAD~3

pick b536dec90 コミットA
pick 7fc1e2c81 コミットB
pick 3c98d9014 コミットC

# Rebase 185f507a6..3c98d9014 onto 185f507a6 (3 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# .       create a merge commit using the original merge commit's
# .       message (or the oneline, if no original merge commit was
# .       specified). Use -c <commit> to reword the commit message.
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

コミットC(3c98d9014)を、1つ前のコミットB(7fc1e2c81)にまとめたい場合以下のように書き換える。 squash(押し潰す)されたコミットCをコミットBが拾う(pick)。

$ git rebase -i HEAD~3

pick b536dec90 コミットA
pick 7fc1e2c81 コミットB
s 3c98d9014 コミットC

:wqでファイルを保存しviを終了すると、以下の画面になる。

# This is a combination of 2 commits.
# This is the 1st commit message:

コミットB

# This is the commit message #2:

コミットC

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date:      Fri Sep 24 16:49:38 2021 +0900
#
# interactive rebase in progress; onto 185f507a6
# Last commands done (3 commands done):
#    pick 7fc1e2c81 コミットB
#    squash 3c98d9014 コミットC
# No commands remaining.
# You are currently rebasing branch 'feature/hogehoge
#
# Changes to be committed:
#       modified:   src/hogehoge.php
#       modified:   src/fuga.php
#       modified:   src/piyo.php
#

コミットメッセージをまとめてあげて、:wq

# This is a combination of 2 commits.
# This is the 1st commit message:

コミットB+

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date:      Fri Sep 24 16:49:38 2021 +0900
#
# interactive rebase in progress; onto 185f507a6
# Last commands done (3 commands done):
#    pick 7fc1e2c81 コミットB
#    squash 3c98d9014 コミットC
# No commands remaining.
# You are currently rebasing branch 'feature/hogehoge
#
# Changes to be committed:
#       modified:   src/hogehoge.php
#       modified:   src/fuga.php
#       modified:   src/piyo.php
#

以下の画面に戻る。

$ git rebase -i HEAD~3
[detached HEAD 51ffdbb8d] コミットB+
 Date: Fri Sep 24 16:49:38 2021 +0900
 3 files changed, 11 insertions(+), 3 deletions(-)
Successfully rebased and updated refs/heads/feature/hogehoge

コミットが纏まったことが分かる。

$ git log -n 3                                                                                                                                                              
commit 51ffdbb8dc3f2b5336f5394739599313c25022a0 (HEAD -> feature/hogehoge)
Author: ryota1116
Date:   Fri Sep 24 16:49:38 2021 +0900

    コミットB+

commit b536dec90d6505bcfc6ae2f3f74ba03a5eeca8b3
Author: ryota1116
Date:   Tue Sep 21 01:27:14 2021 +0900

    コミットA

commit 185f507a6a32fd93505db1cd05b6effa23f45532
Author: ryota1116
Date:   Tue Sep 21 01:14:21 2021 +0900

    コミットα

問題無ければforce push。 同じブランチで他の人が一緒に作業してたりするなら、force pushは避けましょうね。

$ git push -f

まとめたコミットを戻したい時

git logではsquash後のコミットしか見れないが、

$ git log -n 3                                                                                                                                                              
commit 51ffdbb8dc3f2b5336f5394739599313c25022a0 (HEAD -> feature/hogehoge)
Author: ryota1116
Date:   Fri Sep 24 16:49:38 2021 +0900

    コミットB+

commit b536dec90d6505bcfc6ae2f3f74ba03a5eeca8b3
Author: ryota1116
Date:   Tue Sep 21 01:27:14 2021 +0900

    コミットA

commit 185f507a6a32fd93505db1cd05b6effa23f45532
Author: ryota1116
Date:   Tue Sep 21 01:14:21 2021 +0900

    コミットα

git reflogでHEADの移動履歴を確認できる。
3c98d9014まで戻せば良さそう。

$ git reflog -n 11
78dbdc443 (HEAD -> feature/hogehoge) HEAD@{0}: rebase -i (finish): returning to refs/heads/feature/hogehoge
78dbdc443 (HEAD -> feature/hogehoge) HEAD@{1}: rebase -i (squash): コミットB+
549583fe1 HEAD@{2}: rebase -i (squash): # This is a combination of 2 commits.
b536dec90 HEAD@{3}: rebase -i (start): checkout HEAD~3
3c98d9014 HEAD@{4}: rebase -i (abort): updating HEAD
3c98d9014 HEAD@{5}: rebase -i (abort): updating HEAD
bdd5515b2 HEAD@{6}: rebase -i (squash): # This is a combination of 2 commits.
b536dec90 HEAD@{7}: rebase -i (start): checkout HEAD~3
3c98d9014 HEAD@{8}: commit: コミットC
7fc1e2c81 HEAD@{9}: commit: コミットB

git reset --hard で戻す。

$ git reset --hard 3c98d9014                                                                                                                                           
HEAD is now at 3c98d9014 コミットC

reflog見ると、HEADの位置が移動していることが分かる。

$ git reflog -n 12
3c98d9014 (HEAD -> feature/hogehoge) HEAD@{0}: reset: moving to 3c98d9014
78dbdc443 HEAD@{1}: rebase -i (finish): returning to refs/heads/feature/hogehoge
78dbdc443 HEAD@{2}: rebase -i (squash): コミットB+
549583fe1 HEAD@{2}: rebase -i (squash): # This is a combination of 2 commits.
b536dec90 HEAD@{3}: rebase -i (start): checkout HEAD~3
3c98d9014 HEAD@{4}: rebase -i (abort): updating HEAD
3c98d9014 HEAD@{5}: rebase -i (abort): updating HEAD
bdd5515b2 HEAD@{6}: rebase -i (squash): # This is a combination of 2 commits.
b536dec90 HEAD@{7}: rebase -i (start): checkout HEAD~3
3c98d9014 HEAD@{8}: commit: コミットC
7fc1e2c81 HEAD@{9}: commit: コミットB

参考記事

PHPでポリモーフィズム、インターフェイスを理解する

ポリモーフィズム多態性)とは

ポリモーフィズムとは「同名のメソッドを異なるクラス間で使用できるようにすること」を意味しています。

例えば以下のようなSoldierクラス、Wizardクラス、Warriorクラスが存在し、これらのクラスに「攻撃をするメソッド」を定義したとします。

(※この例は以下の記事を参照しています。)

<?php
class Soldier {
    private const SOLDIER_POWER = 2;
    
  // 攻撃をするメソッド
    public function slash(): int {
        return self::SOLDIER_POWER;
    }
}
<?php
class Wizard {
    private const WIZARD_POWER = 1;
    
  // 攻撃をするメソッド
    public function cast(): int {
        return self::WIZARD_POWER;
    }
}
<?php
class Warrior {
    private const WARRIOR_POWER = 3;
    
  // 攻撃をするメソッド   
    public function knuckle(): int {
        return self::WARRIOR_POWER;
    }
}

これらの各メソッドは「攻撃をする」という同じ目的を持っていますが、クラスごとに別名で定義されています。

そこでポリモーフィズムを用いてみます。
まずCharacterというスーパークラスを作成し、Characterクラスを継承する複数のサブクラスではattack()という同名のメソッドを定義してあげます。

(Character.php)

<?php
class Character {
    // この記述はポリモーフィズムとしては不完全である(後述)
    public function attack(): int {
        return 0;
    }
}
<?php
require_once 'Character.php'

class Soldier {
    private const SOLDIER_POWER = 2;
        
    public function atack(): int {
        return self::SOLDIER_POWER;
    }
}
<?php
require_once 'Character.php'

class Wizard {
    private const WIZARD_POWER = 1;
        
    public function atack(): int {
        return self::WIZARD_POWER;
    }
}
<?php
require_once 'Character.php'

class Warrior {
    private const WARRIOR_POWER = 3;
        
    public function atack(): int {
        return self::WARRIOR_POWER;
    }
}

このようにポリモーフィズム同名のメソッドで異なる挙動を実現してくれるため、同じ目的を持った機能を呼ぶために各クラスにわざわざ異なる名前のメソッドを定義する必要が無くなります。

これによってコードの書き直しが減る&クラスごとにメソッド名を覚える必要が無くなるため、記述の間違いが減るなどのメリットが得られます。大規模な開発を行う場合は特に恩恵が受けられそうです。

抽象クラス/抽象メソッド

ただし、上記の定義では「サブクラスがattack()メソッドを実装してくれることが保証されていない」ため、ポリモーフィズムとしては不完全です。

スーパークラスとしてはサブクラスがattack()メソッドを実装してくれることを期待していたとしても、サブクラスはattack()メソッドと同じ目的のメソッドを別に定義してしまう可能性があります。サブクラスがスーパークラス側の都合を理解しているとは限りません。

そこで使用するのが抽象メソッドです。
抽象メソッドとは「それ自身が機能を持たない空のメソッドのこと」です。

抽象メソッド自体は機能を持たないため、外部(サブクラス)から機能を与えてあげる必要があります。つまり抽象メソッドとは「サブクラスでオーバオライドされることを期待したメソッド」と言えます。(独習PHP第3版より)

使い方としては、まず抽象メソッドを含んだ抽象クラスを定義してあげます。

(CharacterAbstract.php)

<?php
abstract class CharacterAbstract {
    // 抽象メソッドなので中身を持たない
    protected abstract function attack(): int;
}

サブクラスでの定義は先ほどと同じ通りです。

最初の例と異なるポイントは、抽象クラスを継承したサブクラスは「全ての抽象メソッドをオーバーライドしなければならない義務を負う」という点です。
もし全てのメソッドをオーバーライドしていない場合はエラーが表示され、サブクラスをインスタンス化できなくなります。

このように抽象メソッドを使用すれば「特定のメソッドがサブクラスで実装されることを保証できる」ようになります。

インターフェイス

ただしこれでもまだ問題があります。

PHPでは多重継承ができない(複数のクラスを同時に継承できない)ため、ポリモーフィズムを実現したい全ての機能を1つの抽象クラスにまとめなければならない上、サブクラスが必要としない機能までオーバーライドする必要があるというデメリットが存在します。

例えば、

  • Soldierクラスではattack()メソッドとguard()メソッドが必要

  • Wizardクラスではattack()メソッドだけが必要

といったケースの場合、CharacterAbstractクラスにはattack()guard()という抽象メソッドを定義することになります。
そして前述の通り抽象クラスを継承したサブクラスは全ての抽象メソッドをオーバーライドしなければならない義務を負うため、Wizardクラスは抽象クラスであるCharacterAbstractクラスを継承した時点で、必要としないguard()メソッドまでオーバーライドしなければならなくなります。

こうなるとサブクラスの役割がよく分からなくなってしまう上、コードも冗長になります。

そこで、この問題を解決するために使用するものがインターフェイスです。
インターフェイスとは、

(やや強引に言えば)配下のメソッドが全て抽象メソッドであるクラスのこと(独習PHP第3版より)

と言えます。

実際にコードを書いてみます。先ほどのCharacterAbstractインターフェイスで書き換えると以下のようになります。
そしてインターフェイスでは中身を持つメソッドや、プロパティは定義できず、抽象メソッドと定数のみが定義可能です。

(CharacterInterface.php)
<?php
interface AttackInterface {
    function attack(): int;
}

interface GuardInterface {
    function guard(): int;
}

抽象クラスとの大きな違いはインターフェイスは多重継承が可能である点」です。
このインターフェイスの特徴により、以下のようにサブクラスごとに必要なメソッドだけを実装することができます。

そして、インターフェイスに含まれる全てのメソッドを実装する必要がある」という制約上のメリットも受けられます。

(Soldier.php)
<?php
require_once 'Character.php'

// AttackInterfaceとGuardInterfaceを実装している
class Soldier implements AttackInterface, GuardInterface {
    private const SOLDIER_POWER = 2;
    private const SOLDIER_DEFENCE = -1;

    public function atack(): int {
        return self::SOLDIER_POWER;
    }

    public function defence(): int {
        return self::SOLDIER_DEFENCE;
    }
}
(Wizard.php)
<?php
require_once 'Character.php'

// AttackInterfaceだけを実装している
class Wizard implements implements AttackInterface {
    private const WIZARD_POWER = 1;
        
    public function atack(): int {
        return self::WIZARD_POWER;
    }
}

またインターフェイスの機能を受け継ぐことは厳密に言えば「継承する」ではなく「実装する」と呼ぶため、こちらで覚えておくべきかと思います。またインターフェイスを実装するクラスのことを実装クラスと呼びます。

補足

また、以下のコードも見てみます。

<?php
interface AttackInterface {
    function attack(): int;

    function doubleAattack(): int;
}


class Soldier implements AttackInterface, GuardInterface {
    private const SOLDIER_POWER = 2;

    public function atack(): int {
        return self::SOLDIER_POWER;
    }

    public function doubleatack(): int {
        return self::SOLDIER_POWER * 2;
    }
}

class Wizard implements implements AttackInterface {
    private const WIZARD_POWER = 1;
        
    public function atack(): int {
        return self::WIZARD_POWER;
    }

    public function doubleatack(): int {
        return self:: WIZARD_POWER * 2;
    }
}



// 使用する側のコード
public function buttle(AttackInterface $attack) {
    return $attack->atack();
    return $attack->doubleatack();
}

実装したメソッドを使用する側のコードに注目です。
インターフェイスであるAttackInterfacebuttle()メソッドの引数として型指定して受け取ることで、buttle()メソッドの引数はAttackInterfaceを実装しているクラスのオブジェクトであることが確実となります。

そしてbuttle()メソッドで呼び出しているattack()メソッドとdoubleatack()メソッドはAttackInterfaceの抽象メソッドなので、実装クラスには必ず存在するメソッドと言えます。

したがって、使用する側(buttle()メソッド)の引数に渡された変数がSoldierクラスのオブジェクトであろうとWizardクラスのオブジェクトであろうと、使用する側のコードの記述を一切変えること無くメソッドを扱うことが可能になります。使用する側のコードを実装クラスに合わせて変更する必要はありません。

メモ

実際にインターフェイスを使う場合はこんな感じでしょうか?

<?php
class AttackGetting
{
    private $attackType

    public function __construct(string $attackType)
    {
        // Setter
        $this->attackType = $attackType;
    }

     // Getter
    public function getAttackType(): AttackInterface
    {
        if ($this->attackType === 'Soldier') {
            return new Soldier();
        } else if ($this->attackType === 'Wizard') {
           return new Wizard();
        }
    }
}
<?php

// 使用する側のコード
public function buttle(AttackInterface $attack) {
    return $attack->atack();
    return $attack->doubleatack();
}


$attack_getting = new AttackGetting('Soldier');
buttle($attack_getting->getAtacckType());

参考記事

PHPのPshSHで対話型デバッグを行う

composerを使ってインストール

$ composer g require psy/psysh:@stable                                                                                                                       

Changed current directory to /Users/funesakisuke/.composer
./composer.json has been created
Running composer update psy/psysh
Loading composer repositories with package information
Updating dependencies
Lock file operations: 14 installs, 0 updates, 0 removals
  - Locking dnoegel/php-xdg-base-dir (v0.1.1)
  - Locking nikic/php-parser (v4.10.4)

Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 14 installs, 0 updates, 0 removals
  - Installing symfony/polyfill-php80 (v1.22.0): Extracting archive
  - Installing symfony/polyfill-mbstring (v1.22.0): Extracting archive
archive
7 package suggestions were added by new dependencies, use `composer suggest` to see details.
Generating autoload files
10 packages you are using are looking for funding.
Use the `composer fund` command to find out more!

REPLとしてpsyshを使う

これでコンソールで色々試せる。 Tabを押せば関数を調べられる。

$ ./vendor/bin/psysh                                                                                                                                         
Psy Shell v0.10.6 (PHP 7.4.14 — cli) by Justin Hileman

>>> set
set_error_handler     set_file_buffer       set_time_limit        setlocale             settype
set_exception_handler set_include_path      setcookie             setrawcookie

$ ./vendor/bin/psyshではなく、psyshで利用できるようにパスを通す

# $HOME/.composer/vendor/binを追加
export PATH=“$HOME/.composer/vendor/bin:$PATH”
$ psysh                                                                                                                                            
Using local PsySH version at ~/workspace/app/cake_3_tutorial/cake_3_tutorial
Psy Shell v0.10.6 (PHP 7.4.14 — cli) by Justin Hileman


>>> function timesFive($x) {
...  $result = $x * 5;
...  return $result;
... }
>>> timesFive(10);
=> 50

cake consoleもある。php -aでいい気もした。

$ bin/cake console          
You can exit with `CTRL-C` or `exit`

Psy Shell v0.10.6 (PHP 7.4.14 — cli) by Justin Hileman
>>>
$ php -a                    
Interactive shell

php >

デバッガーとしてpsyshを使う

コードの中にeval(\Psy\sh());を記述すれば、そこがブレークポイントの位置となりデバッグを行える。

Psy Shell v0.10.6 (PHP 7.4.14 — cli-server) by Justin Hileman
From src/Controller/ArticlesController.php:60:
    58:     $tags = $this->Articles->Tags->find('list');
    59:
  > 60:     eval(\Psy\sh());
    61:
    62:     // ビューコンテキストに tags をセット
Psy Shell v0.10.6 (PHP 7.4.14 — cli-server) by Justin Hileman
From src/Controller/ArticlesController.php:60:
    58:     $tags = $this->Articles->Tags->find('list');
    59:
  > 60:     eval(\Psy\sh());
    61:
    62:     // ビューコンテキストに tags をセット

$tags
=> Cake\ORM\Query {#85
     (help): "This is a Query object, to get the results execute or iterate it.",
     sql: "SELECT tags.id AS `tags__id`, tags.title AS `tags__title` FROM tags tags",
     params: [],
     defaultTypes: [
       "tags__id" => "integer",
       "tags.id" => "integer",
       "id" => "integer",
       "tags__title" => "string",
       "tags.title" => "string",
       "title" => "string",
         ...
       .....
       .......

$article->id
=> 2

$article->title
=> "おはようだこんばんは"

$this->request->is(['post', 'put'])
=> false

コマンドオプション

Commands · bobthecow/psysh Wiki · GitHub

help
  help       Show a list of commands. Type `help [foo]` for information about [foo].      Aliases: ?
  ls         List local, instance or class variables, methods and constants.              Aliases: dir
  dump       Dump an object or primitive.
  doc        Read the documentation for an object, class, constant, method or property.   Aliases: rtfm, man
  show       Show the code for an object, class, constant, method or property.
  wtf        Show the backtrace of the most recent exception.                             Aliases: last-exception, wtf?
  whereami   Show where you are in the code.
  throw-up   Throw an exception or error out of the Psy Shell.
  timeit     Profiles with a timer.
  trace      Show the current call stack.
  buffer     Show (or clear) the contents of the code input buffer.                       Aliases: buf
  clear      Clear the Psy Shell screen.
  edit       Open an external editor. Afterwards, get produced code in input buffer.
  sudo       Evaluate PHP code, bypassing visibility restrictions.
  history    Show the Psy Shell history.                                                  Aliases: hist
  exit       End the current session and return to caller.                                Aliases: quit, q

参考