くま's Tech系Blog

基本的には技術で学んだことを書き留めようと思います。雑談もやるかもね!

プログラムの計算量について

今回はプログラムの計算量について触れていこうと思います。

計算量とは、大まかにはプログラムの計算効率を測る指標のようなものです。

プログラムの処理には計算や、繰り返し、比較などの様々な処理がありますが、これらの処理はデータがどれぐらい増えたらどれぐらいの割合で実行時間が増えていくのかを表したりします。

計算量には2つの指標があります。

計算量の指標

ここでは、計算量の指標を見ていきます。

時間計算量

時間計算量アルゴリズムを実行するときに消費する時間やステップ数がどれくらい必要なのかを指標とするものです。

計算量というとこの後に説明する空間計算量よりも時間計算量の方が一般的です。

時間計算量

空間計算量アルゴリズムを実行するときに消費するアルゴリズムの処理に必要な空間的領域の大きさがどれくらい必要なのかを指標とするものです。 ビット、バイト、ワードなどを測るものです。

ただし、空間計算量は端末によって変化したり、パソコンのメモリは大幅に増えているので比較対象として定量化するのは難しくなっています。 なので、計算量というとまずは時間計算量のことだと認識した方がいいかもしれないです。

オーダー記法

ここからは計算量を具体的にはどうやって測るのかについて説明しようと思います。

計算量は一般に、オーダー記法(ビッグオー記法)と呼ばれるものを使用することで、どれぐらい計算量が必要になるのかというのを表すことができます。

ただし、プログラムの処理は実行マシンやメモリ容量、プログラミング言語などによって影響されるため、厳密な計算量は計れません。 なので、オーダー記法というのはあくまでも指標と思ってください。

そして、オーダー記法は O(1)やO(N2)などの表記で表されます。

ここで出てくる(1)や(N2)はデータの個数を表しています。

例えば 、O(N)は入力が2倍になれば計算量も2倍になるという意味です。 for文で繰り返す場合などが当てはまります。
O(N2)は入力が2倍になれば計算量は4倍になるという意味です。 2重for文で繰り返す場合などが当てはまります。

次の順で性能が良いとされています。(O(1)の方が性能がいい)

O(1) > O(logN) > O(N) > O(N2) > O(N3)

計算量を求める

では、実際に計算量を求める方法についてです。 ここでは、アルゴリズムの基本構造の単位で計算量を求めます。

順次

順次構造は今の処理が終わってから次の処理に進むという構造です。 プログラミングでは基本的に上の行から下の行まで順番に処理されます。

命令が順次に並んでいる場合は、合計ステップ数は、ステップ数を足したものになります。
計算量は、入力サイズの増減に関わらず一定なので、O(1)と表します。

反復

反復は繰り返し処理のことです。計算量は複数のパターンがあります。

for (int i = 0; i < n; i++) { // nは入力サイズとする
    count++; // n回実行される
}

この場合には、合計ステップ数は入力サイズになり、計算量はO(N)と表します。

for (int i = 0; i < n; i += 3) { // nは入力サイズとする
    count++; // n/3回実行される
}

この場合には、合計ステップ数はn/3になり、n/3回ループするので、計算量はn/3なので、O(N)と表します。 ここでなぜ計算量がO(N)になるのかは後で説明します。

for (int i = 1; i <= n; i = i * 2) {
    count++; // logn回実行
}

この場合には、合計ステップ数はlognになり、計算量はO(logn)と表します。

このように処理の条件が変わると計算量も変わります。

分岐

分岐とはif文のことです。 分岐の場合にはより計算量の大きい方に分岐するものとして、ステップ数を求めます。

if (count > 0) {
    // A処理
} else {
    // B処理
}

例えば、上記のような分岐があったとします。

A処理とB処理でステップ数を比較して大きい方が分岐でのステップ数となります。 なので、計算量も比較して大きい方となります。

これらのステップ数や計算量を使用して合計を求めます。

計算量のルール

計算量を求める際には、ルールがあるので注意してください。

①最大次数の高事項以外を除く

計算量を求める際は、低次項を無視して最大次数の項だけを計算量として表します。 例えば、O(N2+N)という計算量があった場合、低次項を除いて、O(N2)が計算量となります。

f(x)=x2+10xを考えるとxが10あたりからx2の値が大半を占めることになります。 つまり、入力サイズが大きくなるにつれ、低次項の重要性が低くなっていくので、最大次数の項だけを計算量とします。

②定数係数は無視する

そして、定数係数は無視します。 例えば、O(2N)やO(3N)は定数倍の違いしかないため、係数部分を無視して、いずれもO(N)として扱います。

反復の際に、計算量がn/3なのをO(N)と表していたのはこのルールに基づいているからです。

まとめ

計算量に触れましたが、まだまだ導入部分だと思っています。 しかし、ここを理解すると実コードでどのような処理を書くべきか、このコードは改善した方がいいなどの判断基準を持つことができます。

参照

yukimasablog.com

qiita.com

凝集度について

今回は凝集度(cohesion) について記載しようと思います。

凝集度とはプログラムのひとつの関数・メソッド・クラスなどのモジュールの中に含まれる機能の純粋度を表す尺度です。 1つの関数の中でいくつもの機能が混ざり合っているよりも、機能と関数が1対1になっている方が上手く機能分割されて理解しやすく、修正の影響範囲は確実に減ります。 つまり、凝集度が高いということはメンテナンス性が高くなります。

ここからは凝集度の度合いとその分類を段階別に記載していきます。 上から順に望ましいモジュールになります。

機能的凝集度

機能的凝集度(Functional cohesion)はモジュールがある処理を1つだけ実行する場合です。

イメージは、ケーキ作りの道具をすべてキッチンの1つの専用引き出しに収納している場合です。 ボウルや木製のスプーン、ケーキ型はすべて、ケーキ作りという同じシングルタスクに貢献するため、まとめられています。

function getEquipmentForCake() {
    return  ケーキ作り用の道具(ボウルやスプーンなど);
}

逐次的凝集度

逐次的凝集度(Sequential cohesion)はモジュール内の処理が、受け渡されたデータを加工を施し結果を返却する処理であり、かつ順番に連鎖できる関係性でまとまっている場合です。

1つのモジュールの中に2つ以上の機能が含まれているが、2つの機能は関連性が高く、その順に処理していくことに意味があるため、1つの関数の中に収まっている状態になります。

例えば、コーヒーを飲むまでの流れを考えてみてください。 豆を挽くまではコーヒーを淹れることはできないです。 つまり、豆を挽くプロセスのアウトプットは、コーヒーを淹れるプロセスのインプットです。 したがって、豆を挽くこととコーヒーを淹れることは互いに密接な関係にあると結論づけることができます。

function drinkCoffee() {
    豆を挽く;
    コーヒーを淹れる;
}

連絡的(通信的)凝集度

連絡的凝集度(Communicational cohesion)はモジュール内の処理が同じデータを使用するモジュールです。 ただし、データの共有を除いて処理の関連がないモジュールです。

例えば、同種のレコードの情報を操作するルーチンを集めたモジュールが該当するので、次のような処理が当てはまるかと思われます。

function searchBook() {
    本のタイトルを検索;
    本の値段を検索;
    本の著者を検索;
}

同じ本の情報を検索していますが、処理の関連がないです。

時間的凝集度

時間的凝集度(Temporal cohesion)は同じようなタイミングで発生する、関係ない機能を集めたモジュールです。 実行順序を入れ替えても問題ないという特徴があります。

function prepareToSleep() {
    テレビを消す;
    洗顔をする;
    歯を磨く;
}

手続的凝集度

手続的凝集度(Procedural cohesion)は論理的に似たようなことをするものを集めたモジュール、もしくはお互いに関係ない機能の一つを実行するモジュールです。 時間的凝集に非常に似ており、違いは順序に意味があります。より関連性の高い処理が関数に実装されており、時間的凝集よりは凝集度が高いです。
ただし、機能的には関係ない関数が 1 つの関数にまとめられてしまっているのが問題です。

function buySomething() {
    財布を出す;
    残高を確認;
    支払う;
}

論理的凝集度

論理的凝集度(Logical cohesion)は論理的に似たようなことをするものを集めたモジュール、もしくはお互いに関係ない機能の一つを実行するモジュールです。 例えば、関連のない処理をフラグで切り替える関数などが当てはまります。

function transport() {
    if (isSunny) {
        徒歩で移動;
    } else {
       車で移動;
    }
}

論理的凝集の関数そのものを削除してしまう。もしくは、フラグ値によって処理を切り替える関数は削除可能な場合があるので検討する必要があります。

暗号的(偶発的)凝集度

暗号的凝集度(Coincidental cohesion)は作為に処理が集められた関数.関数の中に複数の処理が存在し、それらの処理はまったく関係がないモジュールです。 最も凝集度が低いため本当に必要なのか、改善する方法を検討した方が良いモジュールです。

function doAnything() {
    ケーキを焼く;
    散歩をする;
    お風呂に入る;
}

まとめ

凝集度の度合いについて記載しました。

機能的凝集度もしくは、逐次的凝集度になるように意識してモジュールを作れれば良いかなと思います。(機能的凝集度にするのが難しいパターンもあるので)
逆に暗号的(偶発的)凝集度や論理的凝集度になっている場合には改善方法を検討した方がいいと思います。

プログラミングにおける関心の分離(責務の分離、Separation Of Concerns)を意識することで凝集度の高いプログラムを作成できると思います。

参照

www.shuwasystem.co.jp

https://www.tsuyama-ct.ac.jp/hata/experiments/4th/software/ja/chapter3/3_cohesion.html#:~:text=%E9%80%90%E6%AC%A1%E7%9A%84%E5%87%9D%E9%9B%86%E5%BA%A6%20(Sequential,%E3%81%A7%E3%81%BE%E3%81%A8%E3%81%BE%E3%81%A3%E3%81%A6%E3%81%84%E3%82%8B%E5%A0%B4%E5%90%88%E3%80%82

docs.sakai-sc.co.jp

devise_token_authで認証機能を作成する

今回はdevise_token_authを使っての認証機能について解説しようと思います。

devise_token_authとは?

トークンベースの認証を行いたい場合devise_token_authというGemを使います。

トークンベースの認証とは、ユーザー情報を確認し、代わりに一意のアクセストークンを受け取ることを可能にするプロトコルです。 トークンの存続期間中、ユーザーはトークンが発行されたWebサイトやアプリにアクセスできます。 同じトークンで保護されたWebページ/アプリ/リソースに再度アクセスするたびに、資格情報を再入力する必要はありません。
認証トークンが有効である限り、ユーザーはアクセスを保持します。ユーザーがログアウトすると認証トークンは無効になります。

Railsではdeviseをログインなどで使う場合もあると思います。 deviseはCookieベースの認証を行う場合に使用します。

github.com

devise_token_authを使えるようにする

ここでは、devise_token_authを使えるようにします。

今回はDBはあらかじめ作成している状態から進めたいと思いますのでご了承ください。

devise、devise_token_auth、rack-corsの3つを使うので、3つをGemfileに追加しましょう。 deviseを先にインストールしていないとエラーになることがあるので、先にdeviseをインストールしましょう。

gem 'rack-cors'
gem 'devise'
gem 'devise_token_auth'

bundle installするとインポートできているはずです。

ただし、devise_token_authは次のコマンドでインストールしてください。

$ rails g devise_token_auth:install User auth

devise_token_auth.rbの設定

成功すると、config/initializers/devise_token_auth.rbというファイルが作成されているはずです。

ファイルの中身を確認して次の変更を行なってください(場合に応じて)。

// コメントを外してtrueからfalseに変更する
config.change_headers_on_each_request = false 

// コメントをはずす
config.token_lifespan = 2.weeks 

// コメントを外す
config.headers_names = {:'access-token' => 'access-token',
                        :'client' => 'client',
                        :'expiry' => 'expiry',
                        :'uid' => 'uid',
                        :'token-type' => 'token-type' }

config.change_headers_on_each_requestは trueの場合、リクエストごとにtokenを新しくする必要があるという設定になります。 毎回、トークンが変更されるという動きは今回は期待していないので、falseに変更します。

token_lifespanはtokenの有効期間を定義します。今回は、2週間に設定しておきます。

headers_namesは認証用ヘッダーの名前の定義です。必要でない限り、変更しなくて良いと思います。

Userモデルの設定

devise_token_authの導入に成功していれば、db/migrate/日付_devise_token_auth_create_users.rbというマイグレーションファイルが作成されていると思います。 デフォルトである程度定義されていますが、必要に応じてカラムを追加してください。

class DeviseTokenAuthCreateUsers < ActiveRecord::Migration[7.0]
  def change
    
    create_table(:users) do |t|
      ## Required
      t.string :provider, :null => false, :default => "email"
      t.string :uid, :null => false, :default => ""

      ## Database authenticatable
      t.string :encrypted_password, :null => false, :default => "password"

      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at
      t.boolean  :allow_password_change, :default => false

      ## Rememberable
      t.datetime :remember_created_at

      ## Confirmable
      t.string   :confirmation_token
      t.datetime :confirmed_at
      t.datetime :confirmation_sent_at
      t.string   :unconfirmed_email # Only if using reconfirmable

      ## Lockable
      # t.integer  :failed_attempts, :default => 0, :null => false # Only if lock strategy is :failed_attempts
      # t.string   :unlock_token # Only if unlock strategy is :email or :both
      # t.datetime :locked_at

      ## User Info
      t.string :name
      t.string :email

      ## Tokens
      t.json :tokens

      t.timestamps
    end

    add_index :users, [:email, :encrypted_password],   unique: true
    add_index :users, [:uid, :provider],     unique: true
    add_index :users, :reset_password_token, unique: true
    add_index :users, :confirmation_token,   unique: true
    # add_index :users, :unlock_token,         unique: true
  end
end

作成したら、マイグレーションを行います。

$ rails db:migrate

さらに、ユーザーモデルを修正していきます。

models/user.rbというファイルがあるはずなので下記のように修正します。

class User < ActiveRecord::Base
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
  include DeviseTokenAuth::Concerns::User
end

deviseではモジュールの設定を定義して、使える機能を定義しています。 下記のような機能があるので、使用したいものを羅列していきます。

機能 概要
database_authenticatable パスワードを暗号化してDBに登録する機能
registerable 登録処理を通してユーザーをサインアップする機能。ユーザーに自身のアカウントを編集したり削除することを許可します。
recoverable パスワードをリセットし、通知する機能
rememberable 保存されたcookieから、ユーザーを記憶するためのトークンを生成・削除する機能
trackable サインイン回数や、サインイン時間、IPアドレスを記録します
validatable Emailやパスワードのバリデーションを提供します。独自に定義したバリデーションを追加することもできます
confirmable メールに記載されているURLをクリックして本登録を完了するという認証機能
lockable 一定回数サインインを失敗するとアカウントをロックします。ロック解除にはメールによる解除か、一定時間経つと解除するといった方法があります
timeoutable 一定時間活動していないアカウントのセッションを破棄します
omniauthable TwitterFacebookなどの認証機能

application_controller.rbの設定

app/controllers配下にapplication_controller.rbが作成されていると思うので、次のように変更してください。

class ApplicationController < ActionController::Base
        // 追加
        include DeviseTokenAuth::Concerns::SetUserByToken
end

上記を追加することでSetUserByTokenという拡張機能を使えるようになります。

DeviseTokenAuth::Concerns::SetUserByTokenは、ユーザーのトークンを使用して認証された場合に、current_useruser_signed_in?などのヘルパーメソッドを提供するためのコードが含まれたモジュールです。 このモジュールをApplicationControllerに含めることで、全てのコントローラーでこれらのヘルパーメソッドを使用できるようになります。

application_controller.rbの設定

config/application_controller.rbではexpose: ["access-token", "expiry", "token-type", "uid", "client"]を設定して、フロントエンドでもトークンの情報を確認できるようにします。

config.middleware.insert_before 0, Rack::Cors do
      allow do
        origins ENV["API_DOMAIN"]
        resource '*',
        headers: :any,
        expose: ["access-token", "expiry", "token-type", "uid", "client"],
        methods: [:get, :post, :put, :patch, :delete, :options, :head]
     end
end

ルーティングの設定

ルーティングの設定のために、config/routes.rbに変更を加えます。

Rails.application.routes.draw do
  // 追加
  mount_devise_token_auth_for 'User', at: 'auth', controllers: {
    registrations: 'signup'
  }
end

registrations: 'signup'のsignupは適時変更してください。コントローラー名になります。 他の記事では、'auth/registrations'となっていることが多いです。

会員登録とログイン

ここからは実際に会員登録やログインの処理を見ていこうと思います。

まず、ここまで実行していれば、ルーティングは行われているはずなので、確認してみましょう。

rake routesを行うと次のように表示されるはずです。(表形式にしています)

Prefix Verb URI Pattern Controller#Action
new_user_session GET /auth/sign_in(.:format) devise_token_auth/sessions#new
user_session POST /auth/sign_in(.:format) devise_token_auth/sessions#create
new_user_registration GET /auth/sign_up(.:format) signup#new
user_registration POST /auth(.:format) signup#create

このように一部抜粋なのですが、ルーティングされているので、その情報をもとにして進めます。

Controllerの修正

まずは会員登録についてです。 会員登録はURIパターンがPOST /auth(.:format)なのでSignUpControllerを作成する必要があります。

class SignupController < DeviseTokenAuth::RegistrationsController
  def sign_up_params
    params.permit(:email, :password, :password_confirmation)
  end
end

上記のようにすることで、DeviseTokenAuthのRegistrationsControllerを継承しています。 そして、DeviseTokenAuth::RegistrationsControllerで定義されているsign_up_paramsの設定を更新しています。

フロントエンド(今回はVue.js)では次のように呼び出します。

async signUp () {
            try {
                const res = await axios.post(import.meta.env.VITE_APP_API_BASE + '/auth', {
                    email: this.email,
                    password: this.password,
                    password_confirmation: this.passwordConfirmation
                })

                console.log(response.headers["access-token"])
                console.log(response.headers["client"])
                console.log(response.headers["uid"])
               
            } catch (error) {
                console.log({ error })
            }
        }

成功すれば、responseの中にトークンの情報があるはずなので以降はトークンの情報をヘッダーに付与してリクエストを行います。

ログインはURIパターンがPOST /auth/sign_in(.:format)なので、devise_token_auth/sessions#createを使用します。 ルーティングの際に、mount_devise_token_auth_forを使ってUserモデルに対してDevise Token Authをマウントしたので、devise_token_auth/sessions#createなどのデフォルトのアクションが自動的に使えます。
カスタマイズする場合には継承したクラスを作成するという流れになるのですが、IDとパスワードでログインするだけであれば特に継承する必要はないと思います。

フロントエンドでは次のように呼び出します。

 async login() {
            try {
                const response = await axios.post(import.meta.env.VITE_APP_API_BASE + '/auth/sign_in', {
                    email: this.email,
                    password: this.password,
                })
                console.log(response.headers["access-token"])
                console.log(response.headers["client"])
                console.log(response.headers["uid"])
            } catch (error) {
                console.log({ error })
            }
        }

大まかに解説しましたが、認証機能はどのアプリケーションでも使用すると思うので、ぜひ細かく調べてみてください!

参照

nomad.office-aship.info

qiita.com

qiita.com

zenn.dev

poinorou.hatenablog.com

github.com

blog.furu07yu.com

qiita.com

serip39.hatenablog.com

qiita.com

www.webcyou.com

qiita.com

https://qiita.com/kskumgk63/items/aa581b6b3f8c66fa82e2qiita.com

github.com

RailsをAPIモードで使用する

今回はRailsAPIモードにしてフロントエンドとバックエンドを分ける方法についてまとめます。

Railsの環境構築については下記記事を参照してください。

kumaskun.hatenablog.com

今回はAPIモードで環境構築された状態から進めていきたいと思います。

また、今回はフロントはVue.jsで進めます。 Vue.js以外の他のフロントエンド環境でも同じように進められると思いますので参考にしてください(Railsを変えることはないはず)

処理の大まかな流れは以下の流れです

フロントからバックエンドへ処理を依頼→バックエンドで処理を行う→結果をJson形式でフロントエンドに渡す

ここで処理を依頼したり、結果を受け取る際にaxiosというライブラリを使用します。

axiosについて

axiosとは、簡単にHTTPリクエストのやり取りができるPromiseベースのライブラリです。 非同期にHTTPメソッドを取り扱うことができます。 今回は、バックエンドとの非同期な通信に利用します。

まずはリクエストを行うフロントエンドでの処理についてです。

使い方は公式がわかりやすく記載されています。

github.com

下記のように使用します(一部分を抜粋しています)。

<script>
import axios from 'axios'

export default {
    methods: {
        async getUser() {
            try {
                const res = await axios.get(import.meta.env.VITE_APP_API_BASE + '/users')

                 console.log({ res });
            } catch (error) {
                console.log({ error })
            }
        }
    }
}
</script>

やっていることは次のような処理です。

  • import fromでaxiosモジュールをインポートする
  • 取得したデータを格納するための変数を定義する
  • axiosの処理を記述する

axiosの処理とは今回の例のようにaxios.get("URL")でAPIの処理をバックエンドに依頼することです。

今回の例ではgetでしたが、postやputなども可能です。また、パラメータを付与するなども可能です。

そして、実行結果はresで受け取れるようになっています。

さらに場合に応じてcatchでエラー処理を行なっています。

バックエンドの処理

次にバックエンドで処理を行い、フロントエンドに結果を渡すまでの流れです。

ここからはRailsで開発する際の基本的な流れと変わりはないです。

Modelの作成

まずはModelを作成します。今回はUserというModelを作成します。

rails g model User id:integer name:string

今回はUserデータをフロントエンドに渡します。 そして、マイグレートを行います。

rails db:migrate

ここでデータは入って入る状態とします。 必要に応じて仮データを入れるなどしてください。

Controllerの作成

次にコントローラーを作成します。

rails g controller Users

ルーティング

ここでルーティングを行います。

ルーティングはフロントエンドでAPIの処理を依頼したときに指定したURLと一致する必要があります。

config/routes.rbでルーティングの設定を行います。

Rails.application.routes.draw do
  resources :users
end

rake routesコマンドで確認してルーティングが設定できているか確認しましょう。

Controllerで処理を行う

最後に先ほど作成したコントローラーで処理を行います。

取得したデータはrender json: オブジェクトとすると、JSON形式のデータを返却することができます。

class UsersController < ApplicationController
      def index
        users = User.all
        render json: { status: 200, message: "API Success", data: users }
      end
end

実行すると下記のようなレスポンスになり、フロントエンドで値を受け取れると思います。(dataというレスポンス)

{
    "status":"SUCCESS",
    "message":"Get User Success!",
    "data":{"id":1,"name":"TEST"}
}

クロスオリジンの設定

Railsの処理をここまで記載しましたが、これだけではレスポンスを取得することはできません。

CORS(クロスオリジン)の設定が必要になります。

Webブウラウザから送信されてくるリクエストが正当なものかどうかをチェックするための仕組みが存在しています。同一生成元ポリシー(Same-Origin Policy)です。

同一生成元ポリシーを使うことで、異なるオリジンからのリソースへのアクセスに制限をかけることができます。 結果的に、クロスサイトリクエストフォージェリ(CSRF)などのセキュリティリスクを防ぐことができます。
CORSはこの制限を一部解除し、異なるオリジン間でリソースを共有できるようにするための仕組みです。

今回はフロントエンドとバックエンドという異なるオリジンでデータをやりとりするので、CORSの設定が必要になります。(ポート番号が異なるため同じオリジンではない扱いになる)

Rails APIは、rack-corsというGemを使用することでCORSの設定ができます。

github.com

今回は、Gemを導入した状態から進めます。

config/application.rbで以下の設定を追加します。

config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'localhost:3000'
    resource '*', 
        headers: :any, 
        methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end

originsは、許可するオリジンを指定します。
resourceは許可するリソースファイルを指定します。*とすることで、すべてのリソースファイルを許可します。
headersは許可するHTTPヘッダーのこと。anyとすることですべて許可しています。
methodsは許可するHTTPメソッドを指定します。

CORSを用いると、異なるオリジンにCRUDリクエストを送る際、そのリクエストが許可されていることをサーバに一度確認してからでないと、リクエストを送ることができなくなります。 このリクエストのメソッドにoptionsが使用されます。 CORSを有効にするとこのプリフライトリクエストのために、optionsメソッドが必要なので、methods内の:optionsを削除すると動かなくなるので削除しないようにしてください。

注意点

ここでRailsAPIモードにしてフロントエンドとバックエンドを分ける場合の注意点について少し触れておこうと思います。

APIを実行するとActionController::InvalidAuthenticityTokenCan't verify CSRF token authenticityというエラーが発生することがあります。 これはRailsCSRF対策によるものです。

CSRFとは自分がログインしている(常時ログインなどで)サービスへ、知らぬ間にリクエストを送らされる攻撃です。

通常、常時ログインなどでは、cookieにセッション情報が格納されますが、Railsではリクエストのたびにcookieを自動的に取得します。 cookieはブラウザもしくはサーバに保存されているので、別サイトからリクエストが送られたときにも正しいcookieが使用されてしまうのです。

RailsではCSRF対策を自動生成してくれています。 それがapplication_controllerが作られるときに自動で書き込まれるprotect_from_forgeryメソッドです。

RailsではViewでform_forやform_tagを使うと、自動でCSRF対策用のトークンを埋め込んでくれています。 そして、protect_from_forgeryによって、アクション実行前に正しいトークンが付随されているかをチェックしています。
しかし、今回のようにフロントエンドとのやりとりの場合には自分で対策をしなければいけません。

とりあえずエラーが出ないようにするためであれば、protect_from_forgeryをControllerに設定すれば、チェックを行わないのでエラーが発生しないようにはなります。
ただし、根本的な解決ではないです。 リクエスト時にヘッダにX-CSRF-Tokenを追加し、そこにトークンの値を埋めて送るとRailsサーバーはその値を使ってprotect_from_forgeryを実行してくれるので、この方法が望ましいです。

class UsersController < ApplicationController
      // これは根本的な解決ではない
      protect_from_forgery
      def index
        users = Post.order(created_at: :desc)
        render json: { status: 200, message: "API Success", data: users }
      end
end

参照

prograshi.com

learning-mind.hatenablog.com

qiita.com

qiita.com

web-bonsai.hatenablog.com

zenn.dev

Androidのideaフォルダについて

久しぶりの投稿になってしましました。

今回はAndroidのideaフォルダのファイルについてまとめようと思います。

コミットする際にideaフォルダ内のファイルが変更ファイルとして表示されたときにどうすればいいか迷ったことはありませんか?

まずはどんなファイルがあるのかを知るところから始めましょう!

ideaフォルダ

.ideaフォルダはandroid studioの設定について記述されたもので、ほぼ自動生成されます。

なので、基本的にはコミットする必要はなく、gitignoreに入れてしまっても大丈夫なものが多いです。

しかし、チーム開発する場合、いくつか共有するのを推奨するファイルもあるため少し触れようと思います。

misc.xml

まずは.idea/misc.xmlファイルについてです。

misc.xmlファイルはプロジェクトで使用するJavaSDKのバージョン情報を定義するファイルです。

プロジェクトを複数人で開発する場合には開発者で足並みを揃えた方がいいと思うので、git管理した方がいいと思います。

下記のようなファイルになると思います。ProjectRootManagerでJavaSDKのバージョン情報が定義されています。

 <?xml version="1.0" encoding="UTF-8"?>
 <project version="1">
 <component name="ProjectRootManager" version="1" languageLevel="JDK_11" project-jdk-name="1.8" project-jdk-type="JavaSDK">
     <output url="file://$PROJECT_DIR$/build/classes" />
   </component>
 <component name="VisualizationToolProject">
   <option name="state">
     <ProjectState>
     </ProjectState>
   </option>
 </component>
 </project>

ユーザ固有のファイルパスで生成される場合あるのですが、その場合は適宜$PROJECT_DIR$に置き換えましょう。

compiler.xml

.idea/compiler.xmlファイルはコンパイラの設定ファイルです。

コンパイル時のJavaのバージョン情報を定義するファイルなので、misc.xmlと同様にgit管理した方がいいと思います。

また、アノテーションプロセッサのON/OFFの設定もあります。

下記のようなXMLファイルです。

<?xml version="1.0" encoding="UTF-8"?>
<project version="1">
  <component name="CompilerConfiguration">
    <bytecodeTargetLevel target="1.8" />
  </component>
</project>

bytecodeTargetLevel target="1.8"の部分は⁠⁠Preferencesの Gradle⁠で設定した内容になるはずです。

gradle.xml

.idea/gradle.xmlファイルはGradle設定を定義するXMLファイルです。

Gradleのオプション設定(⁠⁠Preferencesの Gradle⁠の設定)は、主に.idea/gradle.xmlに保存されます。

下記のようにgradleJvmというオプションでGradle JDKの情報を定義しているのでgit管理した方がいいと思います。

<?xml version="1.0" encoding="UTF-8"?>
<project version="1">
  <component name="GradleMigrationSettings" migrationVersion="1" />
  <component name="GradleSettings">
    <option name="linkedExternalProjectsSettings">
      <GradleProjectSettings>
        <option name="gradleJvm" value="Embedded JDK" />
          <set>
            <option value="$PROJECT_DIR$" />
            <option value="$PROJECT_DIR$/app" />
          </set>
        </option>
      </GradleProjectSettings>
    </option>
  </component>
</project>

最後に

gitignoreはテンプレートがあり、基本的には従えばいいと思います。 ただし、最小限しか構成されていないので、プロジェクトに応じてカスタマイズするのが望ましいと感じました。

参照

gihyo.jp

stackoverflow.com

pleiades.io

gihyo.jp

CSSのdisplay要素について

今回はCSSdisplayについてまとめようと思います。

displayはCSSのプロパティの1つです。大まかには、要素(主に子要素)の表示形式を決めるものです。

displayには3つ覚えておきたい値があります。 blockinlineinline-blockです。

それぞれまとめていきます。

block

blockは要素が横までいっぱいに広がり、縦に並んでいく表示形式です。 下記のようなイメージで並びます。

要素の幅や高さを指定することができます。

また、blockの場合、text-align:centervertical-align:middleを指定しても、要素が真ん中に表示されるようにはなりません。 要素の中身のテキストを中央揃えにすることはできます。

下記のように設定しても中央揃えにはならないです。

<p class="block">要素</p>

.block {
  background: orange;
  width: 80px;
  text-align: center;
}

blockの要素を中央に配置したいときにはmargin-right:auto;margin-left:autoという指定により横に中央配置にすることができます。

.block {
  background: orange;
  width: 80px;
  text-align: center;
  margin-right:auto;
  margin-left:auto;
}

inline

inlineの要素どうしの間には改行が入らず、横に並んでいく形式です。 テキストと同じように右端までたどり着くと改行されます。

inlineは文中の一部として使われることがあります。 たとえば、下記のようにテキストの装飾やリンクなどの要素に用いられます。

<p>これは<a href="https://test.com/">aタグ</a>と<span>spanタグ</span>です。</p>

a {
  background: green;
  color:white;
}
span {
  background: orange;
}

inlineの要素は、CSSで幅と高さを指定することができません。 inlineの要素の幅と高さは要素の中身に応じて決まります。 つまり、テキストの長さや文字の大きさにより幅と高さが自動で決まります。

また、marginは左右のみ利用可能なので、上下の余白は指定できないと思っていいと思います。 下記のように要素がかぶる場合も発生します。

<p>これから書くのは<span class="example">margin20pxのspanタグ</span>の例文です。</p>
<p>これから書くのは<span class="example2">このように要素がかぶる場合もありえます</span>!</p>

.example {
  margin: 20px;
  background: skyblue;
}
.example2 {
  padding: 30px;
  background: skyblue;
}

横方向のpaddingとmarginはきちんと指定されています。 一方、上下のmarginは無効になっています。 上下のpadding指定は反映されていますが、前の行とかぶってしまいます。

inlineの要素の表示位置に関してはtext-alignやvertical-alignを指定することができます。 ただし、inlineの要素にtext-alignを指定するときには、親要素に対して指定する必要があります。

<div class="test">
  <span>inline align</span>
</div>

.test {
  /*親要素のdivに対して指定する*/
  text-align:center;
}
span {
  background: skyblue;
}

inline-block

inline-blockはinlineとblockの間を取ったようなものです。 大まかには、要素の並び方はinline的で、要素の中身はblock的な性質を持ちます。

inline-blockの並びはinlineと同じで改行が入らず、横に並んでいきます。

要素の中身はblockと同じで幅と高さ・余白も上下左右に指定できます。
また、text-alignとvertical-alignの指定もできます。

横に並べる場合にはinline-blockを使うのが細い調整ができそうです。

その他

今まで主要な値について説明してきました。 ここではCSS3から使えるようになったflexについて確認していきます。

display: flexを設定することでinline-blockなどと同じように横並び(並列)にすることができます。 ただし、flexは親要素に設定して対象の子要素の並びを設定します。

下記のように指定します。

<div id="flexbox">
  <div class="box-item">1</div>
  <div class="box-item">2</div>
  <div class="box-item">3</div>
  <div class="box-item">4</div>
  <div class="box-item">5</div>             
</div>

#flexbox{
  display:flex;
  width: 100%;
  height:auto;
  padding:10px;
}
.box-item{
  background:orange;
  text-align: center;
  padding:15px 40px;
}

子要素の中に要素がある場合(孫要素)、影響を受けないので横並びにはならず通常の配置になるので注意してください。

さらにdisplay:flexにはjustify-content: space-betweenで等間隔で配置やflex-directionで並びの向きを指定することができます。
多くの設定値があるのでここでは割愛しますが、参照に載せているサイトでは視覚的にも結果が確認できます。 また、親要素に指定するのか子要素に指定するのかがわかるようになっているので一度確認してみてください。

参照

webst8.com

developer.mozilla.org

fromkato.com

Vueのルーティングについて

今回はVue Routerについてまとめようと思います。

Vue Routerはルーティング機能を提供するライブラリです。 ルーティングはクライアントから要求されたURLに応じてコンポーネントを決定する仕組みです。

導入

まずはVue Routerを使えるようにライブラリをインポートします。

npm install vue-router@4

yarn install  vue-router@4

上記はnpmとyarnでインストールする場合です。@以下でインストールするバージョンを指定しています(今回はバージョン4)。

インストールが完了したら、正しくインストールできたかをpackage.jsonを見て確認します。

"dependencies": {
    "vue": "^3.2.47",
    "vue-router": "4"
  },

このようにvue-routerが追加されていればインストールされているはずです。

使ってみる

ここからは実際にVue Routerを使って画面遷移(のようなもの)を行います。

まずはルータの設定を行います。 どのURLに対してどのコンポーネントを紐づけるかを定義する必要があります。
そして、ルーティング設定は/src/router/index.jsで定義します。 下記はindex.jsの一例です。

import { createRouter, createWebHistory } from 'vue-router';
import Home from '/src/components/Home.vue';
import SignUp from '/src/components/SignUp.vue';

// ルーティング情報
const routes = [
  {
    path: '/', 
    name: 'Home', 
    component: Home,
  },
  {
    path: '/signUp', 
    name: 'SignUp', 
    component: SignUp,
  },
];

const router = createRouter({
  history: createWebHistory(),
  routes,
});

export default router;

ルータを利用するためにはcreateRouterメソッドでRouterオブジェクトを作成します。 createRouterメソッドではHistoryモードの情報とルーティング情報を設定しています。

Historyモードにはhistoryもしくはhashどちらかを指定します。
hashの場合にはルーティングにURL hashを使用しています。 この形式で入力されるとvue-routerは、urlの # を見つけてそれより先の文字列を元に動的にコンポーネントを出し分けます。 また、サーバへの通信が発生しない分historyモードより若干早いです。
一方、historyの場合には、URLにハッシュは含まれません。 ホスティングサービスを利用する際は、index.htmlを返すように設定しなければならないので注意が必要です。
今回はhistoryを使用するためcreateWebHistory()を設定しています。

ルート定義

Routerオブジェクトを作成する際にルーティング情報を設定します。 今回はconst routesで定義したものを設定しているため、定義している部分を説明します。

// ルーティング情報
const routes = [
  {
    path: '/', 
    name: 'Home', 
    component: Home,
  },
  {
    path: '/signUp', 
    name: 'SignUp', 
    component: SignUp,
  },
];

pathはリクエストパスの定義です。 path: '/signUp'の場合には「http://localhost:8080/signup」のようにエンドポイントの後に追加するパスを指定します。

componentは呼び出されるコンポーネントの名前です。 例えば、Homeの場合にはHome.vueというコンポーネントファイルが呼び出されます。

nameを指定することで、router-linkでリンク先を指定するときにルート名で指定することが可能になります。

ルート定義に利用するコンポーネントはインポートする必要があります。

ルータの有効化

定義されたルーターはmain.jsで利用できるように設定を行う必要があります。 Routerオブジェクトを下記のようにuseメソッドに渡すことで利用できるようになります。

import { createApp } from 'vue';
import App from './App.vue';
import router from './router';

createApp(App).use(router).mount('#app')

ここでメインコンポーネント(初期表示の画面)を修正します。

<script setup>
import { onMounted, ref } from 'vue'
</script>

<template>
  <router-view />
</template>

<style scoped>
</style>

注目する箇所は<router-view />の部分です。
ルーター経由で呼び出されたコンポーネントは<router-view />要素で確保された領域で表示されます。

画面遷移

画面遷移についても見ていきます。 画面遷移については2パターン方法があります。

①router-linkによる遷移

router-linkはVue Router利用時におけるaリンクに変わるカスタムコンポーネントです。 router-linkを用いることで実際に画面をリロードすることなく内部の処理による画面遷移を実現します。 下記のように定義します。

<router-link to="/limk">リンク</router-link>

// 名前つきルート
<router-link v-bind:to={ name: `SignUp`}>リンク</router-link>

to以降に遷移先のパスを書きます。 また、名前つきリンクはindex.jsで定義したルート情報のnameを指定することも可能です。

②pushによる遷移

ここではプログラムで遷移する方法をまとめます。 例えば、ボタンをクリックしたら画面遷移する場合などに使います。

Vueコンポーネント内部でthis.$routerによりVue Routerのインスタンスにアクセスできます。 そこからrouter.pushメソッドを呼び出して、パスを渡します。

<script setup>
import { useRouter } from 'vue-router';

const router = useRouter();

function showSignUp() {
    router.push('/signUp');
};
</script>

<template>
    <div>
        <button class="commonButton" @click="showSignUp">会員登録</button>
    </div>
</template>

<style>
.commonButton {
    background-color: #ffa500;
}
</style>

router.push('/signUp')とすることでパスを指定して、対応するコンポネントに画面を変更します。

まとめ

Vue Routerについてまとめました。 SPA(Single Page Application)がVue.jsの良さだともうので、ほぼ使うことになると思います。

一度試してみてください!!

参照

qiita.com

reffect.co.jp

codelikes.com

v3.router.vuejs.org

prograshi.com

AndroidアプリのApplicationクラスについて

今回はAndroidアプリのApplicationクラスについてまとめます。

場合によっては使う必要はないのですが、たまに必要になる場合もあるので知っておいて損はないと思います。

Applicationクラスとは?

Applicationクラスは大まかにはアプリ全体のことと捉えてください。

実際の動きとしては、Androidアプリが起動した時に最初にインスタンス化されるクラスです。

このクラスでは、アプリ全体で用いる設定をこのクラスで行います。 例えば、ローカルDBの初期化やHiltのトリガーなどです。

Applicationクラスを作成する

カスタムのApplicationクラスを作成する場合にはApplicationクラスを継承した状態で作成します。

class CuastomApplication: Application() {

    // デバイス構成が変更されたとき
    override fun onConfigurationChanged(newConfig: Configuration) {
        super.onConfigurationChanged(newConfig)
    }

    // アプリケーションの起動時、アクティビティ、サービス、またはレシーバー オブジェクト (コンテンツ プロバイダーを除く) が作成される前
    override fun onCreate() {
        super.onCreate()
    }

     // システム全体のメモリが不足しているとき
    override fun onLowMemory() {
        super.onLowMemory()
    }

    // 不要なメモリを削除するのに適した時期とOSが判断したとき
    override fun onTrimMemory(level: Int) {
        super.onTrimMemory(level)
    }
}

上記がカスタムのApplicationクラスです。Applicationクラスでoverrideできるメソッドでアプリのタイミングを検知することができます。

また、Applicationクラスは次のようにAndroidManifestでクラス名を設定する必要があります。

<application
        android:name=".CuastomApplication">
</application>

Activityの挙動を検知する

先程のカスタムクラスは正直あまり使い所がないと感じませんでしたか?

Applicationクラスを継承するだけでなく、ActivityLifecycleCallbacksを継承することでActivityでの挙動を検知することができます。

ActivityLifecycleCallbacksはActivityのライフサイクルイベントを受け取るインターフェースです。

class CuastomApplication : Application(), Application.ActivityLifecycleCallbacks {
    // Activityでsuper.onCreate()が呼ばれたとき
    override fun onActivityCreated(p0: Activity, p1: Bundle?) {
    }

    // Activityが開始されたとき
    override fun onActivityStarted(p0: Activity) {
    }

    // Activityでsuper.onResume()が呼ばれたとき
    override fun onActivityResumed(p0: Activity) {
    }

    // Activityでsuper.onPause()が呼ばれたとき
    override fun onActivityPaused(p0: Activity) {
    }

    // Activityでsuper.onStop()が呼ばれたとき
    override fun onActivityStopped(p0: Activity) {
    }

    // Activityでsuper.onSaveInstanceState()が呼ばれたとき
    override fun onActivitySaveInstanceState(p0: Activity, p1: Bundle) {
    }

    // Activityでsuper.onDestroy()が呼ばれたとき
    override fun onActivityDestroyed(p0: Activity) {
    }
}

上記がActivityLifecycleCallbacksを継承した場合のコードになります。

上記の他にもAPI29で追加された処理があるので公式ドキュメントを確認してみてください。

ただし、このままではアプリ終了時にonActivityDestroyedだけが呼ばれません。

アプリ終了を検知したい場合にApplicationクラスを使いたい人もいると思います。そこでServiceクラスを使ってアプリ終了を検知するようにします。

Serviceクラスは次のように定義します。(必要な部分のみ抜粋)

class CuastomApplication : Application(), Application.ActivityLifecycleCallbacks {

    override fun onCreate() {
        super.onCreate()

        // サービスを開始する
        startService(Intent(this, DestroyService::class.java))
    }
   
        
    // Activityでsuper.onDestroy()が呼ばれたとき
    override fun onActivityDestroyed(p0: Activity) {
    }

    // Destroy検出用のServiceクラス(onBindをimplementしないとエラーになる)
    class DestroyService: Service() {
        override fun onBind(intent: Intent?): IBinder? {
            return null
        }
    }
}

そして、AndroidManifestでサービスを追加します。

<application
    android:name=".CuastomApplication">

<service
    android:name=".CuastomApplication$DestroyService"
    android:stopWithTask="false" />

</application>

android:stopWithTask="false"を設定することで、アプリをキルしたときにServiceを止めるだけでなく、なんらかの処理を行うようにします。(今回はApplicationクラスでアプリキルを検知するためにfalseを設定します)

ここまで設定するとアプリ終了を検知できると思います。

ProcessLifeCycleOwnerについて

アプリのフォアグラウンドやバックグラウンド、アプリ終了の検知にProcessLifeCycleOwnerでいいのではないかと思った方もいると思います。

結論から言うと、使う際には注意が必要です。

以下は、ProcessLifeCycleOwnerを使ったクラスの一例です。

class CuastomApplication : Application(), LifecycleObserver {
    override fun onCreate() {
        super.onCreate()

        ProcessLifecycleOwner.get().lifecycle.addObserver(this)
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
    fun onCreateLifecycle() {
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_START)
    fun onStartLifecycle() {
        // アプリ開始時 or バックグラウンドからの復帰時
    }
}

ProcessLifeCycleOwnerでforegroundを検知したからといって必ずActivityが表示されているわけではないです。 なので、Activityがない状態でダイアログを表示させようとするとクラッシュすると言う例外的なパターンが発生します。

ActivityLifecycleCallbacksはActivityの生成を監視して、1つ目のActivityがスタートされたタイミングを検知するのでContextがないというのはないはずです。

そして、もう1点注意しないといけないのはルート以外のActivity表示されてたら検知できないです。

これらを踏まえて、個人的にはSeriviceクラスを使って検知したほうが安全だと感じています。

最後に

ここまででアプリのライフサイクルをみていきました。

Applicationクラスは便利だと思います。しかし、Contextが想定外だったり、不必要な共通処理を作ってしまったりと不具合を生み出すもとになりえるので必要最低限の使用にとどめましょう!

参照

guides.codepath.com

developer.android.com

developer.android.com

mt312.com