駆け出しエンジニアぱかぱかの成長記録

引くほど忘れっぽい新卒3年目駆け出しSEぱかぱかの備忘録です。

【Android】店登録画面の作成

こんにちは!ぱかぱかです!
今日は現在開発を進めている飲食店記録アプリ「グルメノート」の画面作成第3弾で、店登録ボタンの追加と店登録画面の作成についてまとめます。
技術的に特に新たな学びは無かったので進捗報告という感じになります。

グルメノート開発でちゃんと手を動かし始めて早3日目!
毎日ネタができて投稿を継続できております。
この調子で早く形にしたいところですね。

前回の記事

radish-se.hatenablog.com

RecyclerViewを使って行きたい店リスト・行った店リスト・お気に入り店リストを表示できるようになりました。

今回作るもの

・行きたい店・行った店を登録する画面へ遷移するボタン
・行きたい店・行った店登録画面

ボタンの追加

以下の画像のように店登録ボタンを右上に追加してみました。

ここでなんと、地味に店登録のアイコンを作るのにちょっと苦戦しました。
背景の上にアイコンと文字を並べただけなのですが…
昔デジタルイラストを描いていた時代に使っていたFireAlpacaというフリーペイントツールを久しぶりに引っ張り出してみました。

firealpaca.com

ダサい…

誰か…デザインセンスを恵んでください…
生まれつきのセンスの無さはどうしようもないので、時間ができたらデザインの勉強もしなきゃな…
他に勉強することありすぎて手が回らなそうだけど…興味はあるんですけどね…

とりあえずImageButtonのsrcにこの画像を設定して表示してますけど、selectorで押下イベントとかも今後つけたいですね。

画面遷移

ボタンを配置したActivityでボタンにsetOnClickListenerしてIntentを使って画面遷移させます。

        val registrationButton = findViewById<ImageButton>(R.id.registration_button)
        registrationButton.setOnClickListener(
            object : View.OnClickListener{
            override fun onClick(p0: View?) {
                val intent = Intent(this@MainActivity, RegistrationActivity::class.java)
                startActivity(intent)
            }
        })

お店登録画面

頑張って作ったボタンを押した後に遷移する画面がこちら。

うわ〜これまた非常にダサい画面ですね…

構成はもうお得意のTabLayoutとViewPager2で、FragmentAdapterでFragmentを切り替える仕組みです。
行きたい店登録画面と行った店登録画面をタブで切り替えられるようにしました。

前回タブを実装した時の記事

radish-se.hatenablog.com

中身はTextViewとEditTextをただ並べただけです…
ここも少しずつマシにしていきたいですね…

登録してDBに保存する処理はまた次回実装したいと思います!
短いですがここら辺で!

【Android】RecyclerViewを使ったリストの実装

こんにちは!ぱかぱかです!
今日は現在開発を進めている飲食店記録アプリ「グルメノート」の画面作成第2弾で、前回作成したViewPager2内に表示するFragmentの中身を作っていきたいと思います!

前回の記事

radish-se.hatenablog.com

前回はViewPager2を実装し、スワイプやタブバーのアイコンタップで画面が切り替わるようになりました。

作りたい画面のイメージ図

デザインが微妙な件はいつかどうにかしたいのですが、今のイメージは大体こんな感じです。
行きたい店リスト・行った店リスト・お気に入り店リストを切り替えたいと思っています。

タブバーの位置とかタブの順番とか、どうするのがいいかはのちのち試行錯誤するとします!

レイアウトの作成

作りたい画面は行きたい店リスト・行った店リスト・お気に入り店リストの3つです。
とりあえずこの記事では情報量が多めの行った店リストの部分だけ取り上げます。

RecyclerViewの大枠

まずRecyclerViewの大枠を作ります。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_height="wrap_content"
        android:layout_width="0dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

RecyclerViewのセル

次にRecyclerView1つ1つのセルのレイアウトを作ります。
ここはパーツが多くて長くなってしまうのでコードは省略します!
イメージとしては以下のような感じ。

今回はCardViewというカードっぽい角丸/影付きのビューを使ってみました。

データクラスの作成

VisitedRestaurantListItemクラスを作成しアダプターに引き渡すデータ項目をデータオブジェクトとして管理します。

data class VisitedRestaurantListItem(
    val restaurantName: String,
    val restaurantEvaluation: Evaluation,
    val restaurantPlace: String,
    val restaurantGenre: String,
    val restaurantRegistrationDate: String,
    val restaurantMemo: String,
    var restaurantPhoto: Drawable?,
    val restaurantFavorite: Boolean
)

ビューホルダーの作成

アダプターで利用するためのビューホルダー
ビューホルダーはビューを保持するためのクラスです。
findViewByIdで配下のウィジェットを毎度取得するのは無駄なので、ViewHolderで個々のウィジェットへの参照を準備しておくという考え方。
あくまでウィジェットを保持することが目的で、自身が処理を受け持つことはありません。

class VisitedRestaurantCardViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
    // ウィジェットへの参照をプロパティに格納
    val restaurantName = itemView.findViewById<TextView>(R.id.restaurant_name)
    val restaurantEvaluation = itemView.findViewById<TextView>(R.id.restaurant_evaluation)
    val restaurantPlace = itemView.findViewById<TextView>(R.id.restaurant_place)
    val restaurantGenre = itemView.findViewById<TextView>(R.id.restaurant_genre)
    val restaurantRegistrationDate = itemView.findViewById<TextView>(R.id.restaurant_registration_date)
    val restaurantMemo = itemView.findViewById<TextView>(R.id.restaurant_memo)
    val restaurantPhoto = itemView.findViewById<ImageView>(R.id.visited_restaurant_photo)
    val restaurantFavorite = itemView.findViewById<ImageView>(R.id.visited_restaurant_favorite)
}

アダプターの作成

アダプターはRecyclerViewにデータを橋渡しするためのクラスです。
RecyclerView.Adapter派生クラスを定義する際、まずは型パラメータとしてアダプターで利用するビューホルダーを割り当てておく必要があります。
コンストラクタでリスト表示に必要な情報を上で作成したデータクラスVisitedRestaurantListItem型として受け取ります。

RecyclerView.Adapterでは以下3つのメソッドを実装する必要があります。

onCreateViewHolder リスト個々の項目を生成するためのビューホルダー生成
onBindViewHolder ビューホルダーに値を割り当て、個々のリスト項目を生成
getItemCount リストの項目数を取得

お気に入り登録していないものはハートが灰色になるようにonBindViewHolderの中で設定しています。

class VisitedRestaurantListAdapter(private val data: List<VisitedRestaurantListItem>): RecyclerView.Adapter<VisitedRestaurantCardViewHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VisitedRestaurantCardViewHolder {
        return VisitedRestaurantCardViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.visited_restaurant_list_item, parent, false))
    }

    override fun getItemCount(): Int {
        return  data.size
    }

    override fun onBindViewHolder(holder: VisitedRestaurantCardViewHolder, position: Int) {
        holder.restaurantName.text = data[position].restaurantName
        holder.restaurantEvaluation.text = data[position].restaurantEvaluation.star
        holder.restaurantPlace.text = data[position].restaurantPlace
        holder.restaurantGenre.text = data[position].restaurantGenre
        holder.restaurantRegistrationDate.text = data[position].restaurantRegistrationDate
        holder.restaurantMemo.text = data[position].restaurantMemo
        holder.restaurantPhoto.setImageDrawable(data[position].restaurantPhoto)
        if (!data[position].restaurantFavorite) {
            holder.restaurantFavorite.setColorFilter(Color.rgb(125, 125, 125), PorterDuff.Mode.SRC_ATOP)
        }
    }
}

Fragment内でデータ生成

今後データベースから取得したいのですが、今はひとまずコードにベタ打ちでVisitedRestaurantListItemをたくさん作ってリストにし、Adapterに渡します。

class VisitedRestaurantFragment: Fragment()  {
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.fragment_visited_restaurant,container,false)

        val data = listOf(
            VisitedRestaurantListItem("吉野家",Evaluation.THREE_STAR,"東京","和食","2023/11/01","新メニューが美味しかった",
                ContextCompat.getDrawable(requireContext(), R.drawable.gyudon),true),
            VisitedRestaurantListItem("サイゼリヤ",Evaluation.FIVE_STAR,"名古屋","イタリアン","2023/11/01","安すぎ",ContextCompat.getDrawable(requireContext(), R.drawable.doria),true),
            VisitedRestaurantListItem("牛角",Evaluation.FOUR_STAR,"大阪","焼肉","2023/11/01","牛タンうまい",ContextCompat.getDrawable(requireContext(), R.drawable.yakiniku),false),
            VisitedRestaurantListItem("CoCo壱",Evaluation.THREE_STAR,"京都","カレー","2023/11/01","辛かった",ContextCompat.getDrawable(requireContext(), R.drawable.curry),true),
            VisitedRestaurantListItem("天下一品",Evaluation.ONE_STAR,"神戸","ラーメン","2023/11/01","味が濃い",ContextCompat.getDrawable(requireContext(), R.drawable.ramen),false),
            VisitedRestaurantListItem("コメダ珈琲",Evaluation.TWO_STAR,"広島","カフェ","2023/11/01","量多い",ContextCompat.getDrawable(requireContext(), R.drawable.komeda),false),
            VisitedRestaurantListItem("スシロー",Evaluation.THREE_STAR,"沖縄","寿司","2023/11/01","サーモン3皿食べた",ContextCompat.getDrawable(requireContext(), R.drawable.sushiro),true),
            VisitedRestaurantListItem("餃子の王将",Evaluation.FIVE_STAR,"札幌","中華","2023/11/01","餃子テイクアウトした",ContextCompat.getDrawable(requireContext(), R.drawable.gyoza),false),
            VisitedRestaurantListItem("やよい軒",Evaluation.THREE_STAR,"仙台","和食","2023/11/01","健康的",ContextCompat.getDrawable(requireContext(), R.drawable.yayoi),true),
            VisitedRestaurantListItem("マクドナルド",Evaluation.FOUR_STAR,"青森","ファストフード","2023/11/01","ポテト高くなってた",ContextCompat.getDrawable(requireContext(), R.drawable.mac),false)
        )

        val rv = view.findViewById<RecyclerView>(R.id.recycler_view)
        rv.setHasFixedSize(true)
        rv.layoutManager = LinearLayoutManager(context).apply {
            orientation = LinearLayoutManager.VERTICAL
        }
        rv.adapter = VisitedRestaurantListAdapter(data)
        return view
    }
}

完成

こんな感じに出来上がりました!

もちろんリストはスクロールできるようになっています!
行きたい店リストもお気に入り店リストも微妙にレイアウトが違うのでそれぞれ作っていますよ。
(データ書き換えるのが面倒で同じような画面に見えてしまうので写真は割愛します…)

タブバーは上と下どちらがいいんでしょうかね〜
当初は下にしていたけど、上でも悪くないような…これからまた考えるとします!
写真を見てるとお腹が空いてきますね!

次回は実際にDBからデータを取得したり登録したりというところを作っていきたいですね!

それでは今日はこの辺で!

【Android】FragmentStateAdapterを使ったViewPager2の実装

こんにちは!ぱかぱかです!
今日は現在開発を進めている飲食店記録アプリ「グルメノート」の画面作成第1弾で、FragmentStateAdapterを使ったViewPager2の実装についてまとめたいと思います!
本当は開発を始める前にクラス図を整理しておきたかったのですが、手を動かしたくなったので画面を少し作り始めてしまいました…

今回作成するもの

今回はグルメノートアプリの骨格になる、タブで画面を切り替えるレイアウトを作成していきます。
成果物は以下のような感じです。

画面キャプチャをGifに変換して貼り付ける初の試み(画質はひどい)

スワイプやタブのアイコンタップでタブを切り替えることができます。
タブ内にはフラグメントで異なるレイアウトを入れたいと思います。(今回は背景色だけ)

初めはRecyclerView.Adapterを使った実装で頑張ろうとしていたのですが、タブごとにレイアウトをマルっと変えたいならFragmentで管理した方がやりやすそう?と思ったのでFragmentStateAdapterを採用して実装していきます。
TabLayoutとViewPagerの紐付けも一緒に行うことで、タブバーの領域もカスタマイズしていきます。

レイアウトファイルの作成

今回は3つの画面(行きたい店画面・行った店画面・お気に入り店画面)をタブによって切り替えられるようにします。
以下のようなレイアウトファイルを3つ作成しました。

背景色と表示される文字列だけ、画面ごとに変えています。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#98fb98">

    <TextView
        android:text="@string/wish_restaurant"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toBottomOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

Fragmentの作成

次にレイアウトをonCreateView()に返すFragmentを作成します。
これも3つの画面についてそれぞれ作成します。

class WishRestaurantFragment: Fragment() {
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_wish_restaurant,container,false)
    }
}

Adapterの作成

AdapterはFragmentStateAdapterを使用しました。
FragmentStateAdapterではgetItemCountとcreateFragmentの実装が求められます。

createFragment Fragmentを新しいページとして供給
getItemCount アダプターが作成するページの数を返す
class TabFragmentAdapter(fragment: FragmentActivity): FragmentStateAdapter(fragment) {
    companion object {
        const val TAB_NUMBER:Int = 3
    }

    override fun getItemCount(): Int {
        return TAB_NUMBER
    }

    override fun createFragment(position: Int): Fragment {
        return when (position) {
            0 -> WishRestaurantFragment()
            1 -> VisitedRestaurantFragment()
            2 -> FavoriteRestaurantFragment()
            else -> WishRestaurantFragment()
        }
    }
}

TabLayoutとViewPager2の追加

activity_main.xmlにTabLayoutとViewPager2を追加します。
TabLayoutは上のタブ名とアイコンが表示されている部分で、ViewPager2はスライドで表示が変わる画面本体になります。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.google.android.material.tabs.TabLayout
        android:id="@+id/tabs"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:contentDescription="@string/tab_layout"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <androidx.viewpager2.widget.ViewPager2
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:id="@+id/pager"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tabs"
        app:layout_constraintBottom_toBottomOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

データクラスの作成

タブバーの表示に使うデータはクラス化してしまいます。
今回はひとまず以下を持つことにしました。
・shortTitle:タブバーに表示するタイトル
・icon:タブバーに表示するアイコン

data class TabItem(
    val shortTitle: String,
    val icon: Drawable?
)

Activityの作成

先ほどのデータクラスに表示したい文字列や画像を格納してリストに入れつつ、TabLayoutMediator#attachでTabLyoutとViewPager2を紐づけTabLayout部分を表示します。
またViewPager2にTabFragmentAdapterを設定してあげることで、positionごとに表示されるFragmentが切り替わるようになります。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // ページで利用するデータを準備
        val data = listOf(
            TabItem(getString(R.string.wish_restaurant), ContextCompat.getDrawable(this, R.drawable.tab_wish)),
            TabItem(getString(R.string.visited_restaurant), ContextCompat.getDrawable(this, R.drawable.tab_visited)),
            TabItem(getString(R.string.favorite_restaurant), ContextCompat.getDrawable(this, R.drawable.tab_favorite))
        )

        val tabs =findViewById<TabLayout>(R.id.tabs)
        val pager = findViewById<ViewPager2>(R.id.pager)

        // ViewPagerとFragmentの紐付け
        pager.adapter = TabFragmentAdapter(this)

        // TabLayout/ViewPagerの紐付け
        TabLayoutMediator(tabs, pager) {
            tab, position ->
            tab.apply {
                text = data[position].shortTitle
                contentDescription = "Page ${position + 1}"
                icon = data[position].icon
            }
        }.attach()
    }
}

感想

アプリの骨組みができて、やる気が湧いてきました…!
次回からはそれぞれのFragmentについて実装を進めていきたいと思います!
ではまた!

【AWS】Webサーバを立ち上げてみた

こんにちは!ぱかぱかです!
自主アプリ作成を進めているはずなのに、他にも勉強したいことが色々あって注意力散漫な今日この頃。
設計に時間がかかっているとなんだか技術的に前進しているのかわからなくなってしまい、何か新しいことを学びたくなってしまうのです…

というわけで先週末は、唐突にAWSでサーバの構築を行ってみました!
サーバとのやり取りもアプリに組み込もうと思っているので、その予行練習ということで!

学習教材

AWSはあいにく業務で使う機会がなかった私。
研修ぶりにAWSに触れるので初心者向けの以下Udemy教材で学習を進めました!

www.udemy.com

定価で受講すると¥11,800もするんですね…
私は今回も裏技で無料で受講させてもらいました。
裏技は以下記事で紹介しています。

radish-se.hatenablog.com

山浦清透先生にはいろんな教材でお世話になっています。
とても丁寧かつわかりやすいです。

やったこと

全部書き出すと長くなりそうなので、詳細は実際にアプリ用のサーバを立ち上げた時に記載することにします。
大まかにやったのは以下のことです。

AWSアカウント作成
・IAM:作業ユーザ作成
VPC:ネットワーク構築
・EC2:Webサーバ構築
・Route53:ドメイン登録
・RDS:DB構築

感想

AWSに触れるのは研修以来!
研修の時は何をやっているのか理解せず手順に従って進めているだけでしたが、今回は何をするために何を構築しているのか考えながら進めることができました。
実際のアプリ開発でも活かしていきたいですね!

短いですがこの辺にします!

【オブジェクト指向】SOLIDの原則について

こんにちは!ぱかぱかです!
今日は現在設計を進めている飲食店記録アプリ「グルメノート」のクラス設計にあたり、SOLIDの原則をおさらいしてみたいと思います。

SOLIDの原則とは

SOLIDの原則はソフトウェア設計をより平易かつ柔軟にして保守しやすくすることを目的としたオブジェクト指向で用いられる原則で、以下の5つの項目からなります。

S → Single Responsibility Principle: 単一責任の原則
O → Open-Closed Principle: 開放閉鎖の原則
L → Liskov Substitution Principle: リスコフの置換原則
I → Interface Segregation Principle: インタフェース分離の原則
D → Dependency Inversion Principle: 依存性逆転の原則

以前プロジェクトの勉強会で取り上げられておりメンバーで具体例など考えながら話してましたが、改めてみると忘れている…
改めてグルメノートの開発ではどう気をつけたらいいかそれぞれ例を挙げて考えてみます。

今回の記事では以下の本も参考にしました。

グルメノートの構成(仮)

まず今のところ決まっているグルメノートの構成をまとめておきます。

画面構成

行ったお店リスト
 行ったお店をリスト形式で表示。

行きたいお店リスト
 行きたいお店をリスト形式で表示。

お気に入り店リスト
 お気に入り店をリスト形式で表示。

お店詳細画面
 行ったお店の詳細を閲覧する画面。

お店登録画面
 行きたいお店/行ったお店を登録する画面。


操作フロー


Single Responsibility Principle: 単一責任の原則

関数やクラスが保持する責務は1つであるべきだという原則です。
これはSOLIDの原則の中でも一番言葉のまま意味がわかりやすい気がしています。

例えばグルメノートでお店クラスを作るとします。
ここに行きたい店登録メソッド行った店登録メソッドを作ったらどうなるでしょうか。
これでは1つのお店クラス行きたい店の登録行ったお店の登録の2つの責務を担うことになります。

登録情報として行きたい店は「店名、ジャンル、行きたい理由メモ」を持っていて、行った店は「店名、ジャンル、訪問日、評価、行った理由メモ」を持っている場合、お店クラスで持つプロパティはそれら両方を含んだものである必要が出てきます。
こういうクラスにしてしまうと、行った店にはあるが行きたい店にはないプロパティのようなものが存在してしまいます。
行きたい店に対して評価を取得するようなロジックを誤って書いてしまった場合に例外が発生してしまう、といった問題が起きかねません。

単一責任の原則に従うなら、行きたい店クラス行った店クラスに分けたほうが良いことになるでしょうか。
もっと正しくやるなら、そもそも登録を行うという責務を持った登録クラスというものを作ってさらに責務を分けてもいいかもしれませんね。


Open-Closed Principle: 開放閉鎖の原則

クラスは、拡張にはオープンで、変更にはクローズドであるべきだという原則です。
わかりやすく言うと、変更が発生した場合に既存のコードには修正を加えずに、新しくコードを追加するだけで対応できるような状態にする必要があります。

例えばグルメノートのお店クラスで店の登録をなんでも店登録メソッドで行うよう設計した場合を考えます。
しかし本当は、行きたい店の登録と行った店の登録では登録の内容が異なっています。
先に行きたい店登録用のメソッドが存在した場合、行った店の登録に対応するためには既存の店登録メソッドの内容に手を加える必要が出てきます。
ここからさらにお気に入り店登録を行おうとした場合はどうなるでしょう。
さらに店登録メソッド内で条件分岐が増えどんどんぐちゃぐちゃしてしまいます。

もしも店登録メソッドというものを使うならば、それをお店クラスで持つのではなく行きたい店クラス行った店クラスに分けた上でそれぞれ店登録メソッドをオーバーライドするようにすれば、新しい店のタイプが出てきた時も元の店登録メソッドを修正することなく新しい登録方法を実装することができます。

そもそも継承すること自体が色々な危険を孕んでいるので、周りではあまり推奨されていないのですが…
イメージとしてはこんな感じでしょうかね。

Liskov Substitution Principle: リスコフの置換原則

親クラスを継承した子クラスがあった場合、子クラスを親クラスで置換可能でなければならないという原則です。

例えばお店クラス行きたい店クラス行った店クラスお気に入り店クラスが継承している構成だったとします。
お店クラスがどのお店でも共通に使うお気に入り登録メソッドを持っていたが、お気に入り登録は行った店でしかできないという仕様になっていた場合、親クラスであるお店クラスで行きたい店クラスやお気に入り店クラスを置き換えられないため、リスコフの置換原則に則っていない状態になってしまいます。

そんなのお気に入り登録メソッドはお店クラスに定義せず行った店クラスにだけ追加すればいいだけではと言われたらそれまでなのですが…
ちょっと自分のアプリで全て例えようとするのには限界を感じてきました…トンチンカンになっているかもしれませんがご了承ください…

Interface Segregation Principle: インタフェース分離の原則

クラスは不要なインターフェースを持たないようにすべきという原則です。
インターフェース継承先で使わないメソッドがないようにインターフェースを分けることがポイントになります。
これは先ほどのリスコフの置換原則の例と関連付けられるかもしれません。
インターフェースを登録インターフェースとお気に入りインターフェースみたいな分け方にしてみたらどうでしょう。

これで仕様を知らない人が行きたい店クラスを実装する際に、行きたい店にはできないはずの「お気に入り登録」を実装してしまうという事態を避けることができます。

Dependency Inversion Principle: 依存性逆転の原則

使う側が使われる側に依存してはいけないという原則です。
登録Viewから登録メソッドを呼び出したい場合、登録の内容が行きたい店・行った店・お気に入り店で異なります。
このとき使う側である「登録View」が使われる側である「行きたい店・行った店・お気に入り店」の種類を意識して登録メソッドを使い分ける必要が出てきます。
これでは使う側が使われる側に依存している状態になっています。

これは間にファクトリークラスとインターフェースを挟むことで解決できます。
「登録View」は登録メソッドを呼び出す際にまずファクトリークラスの登録作成メソッドを呼びます。
「行きたい店」「行った店」「お気に入り店」のいずれに対する処理を行うべきかの判断はファクトリークラスの中で行います。
ファクトリークラスではその条件に応じていずれかの店クラスのインスタンスを生成します。
「登録View」では登録作成メソッドで生成されたインスタンスから何も考えずに登録メソッドを呼び出すことで、生成されたインスタンスが継承している登録メソッドが動くようになります。


まとめ

いざ書いてみると、それぞれ正しく説明できているのか自信がありません…
誤りあればご指摘いただけると幸いです。
クラス構成を考えるにも、色々気をつけることがありそうです。
これから慎重に考えていきたいと思います。
ではまた!

【セキュリティ】箱庭BadStoreでSQLインジェクションしてみる

こんにちは!ぱかぱかです!
ここ数回の記事ではAndroidアプリ開発について書いていたのですが、今日は少し趣向を変えてセキュリティのお話をしたいと思います。
というのも私、セキュリティ関係のプロジェクトに配属されている先輩から以下の本を1年以上借りっぱなしになっているのです…

セキュリティ界隈では有名で徳丸本と呼ばれているこの本。
読もう読もうと思いつつ、資格勉強やモバイル開発の勉強を優先してしまいずっと後回しにして今に至ります…
流石に返したいのですが、こんなに長いこと借りているのに全く理解できていないのは先輩にも合わせる顔がないので、ブログにまとめつつ出来る範囲で理解して記憶に残しておきたい…
応用情報でもセキュリティの分野の出題が多くとっかかりの知識はできたので、もう少し深められればと思います!
今日は基本情報の頃から登場していて耳馴染みのあるSQLインジェクションについてです。

SQLインジェクションとは

SQLインジェクションは「SQLの呼び出し方に不備がある場合に発生する脆弱性」のことです。
DBへの問い合わせを行うWebサイト上で入力内容に悪意ある操作を行うSQLを埋め込んで不正な操作を行います。

これによって以下のような様々な不正を行うことができてしまいます。

・情報漏洩
・データ改竄
・認証の回避
・プログラムの実行
・ファイルの参照・更新

なんでもありですね…
非常に影響の大きな脆弱性として位置付けられています。

実際にやってみる

ここで実際にSQLインジェクションをやってみたいと思います!
といっても実際のWebサイトに試行するのは不正アクセス禁止法違反となり、3年以下の懲役または100万円以下の罰金に処せられる可能性があるそう…やばい…!
ということで今回は、Burp Suite箱庭BadStoreという脆弱性診断環境を作って不正行為に及んでみたいと思います。

環境のセットアップは以下ブログを参照させていただきました。

hex-0xff-255.hatenablog.com

Burp Suiteとは

Burp Suiteはローカルプロキシツールの一つで、Web アプリケーションのセキュリティや侵入テストに使用されるものです。
リクエストとレスポンスを見て通信を解析したり、ボディを書き換えて擬似的に攻撃を行ったりできる模様。
これ、セキュリティに限らず通信の準正常系のテストとかにも使えるのでは…?と思ったり。便利ですね。

箱庭BadStoreとは

箱庭BadStoreは脆弱性が組み込まれたやられ用のwebアプリケーションです。
本家のBadStoreはもう現在アクセスできないため、拡張として動作するよう書き直してくれている以下GitHubからインストールしました。

github.com

立ち上げるとこんな感じの画面が出てきます!

なんか怪しげ…

それでは早速SQLインジェクションをしてみましょう…!

トライ①:パスワードを知らないけどログインしちゃう

まずはBadStoreのログイン画面を開きます。

前提として「test1@test.com」というメールアドレスのアカウントが存在することだけを知っていることとします。
(事前にRegisterからこのアドレスで登録して実施しています。)
でもパスワードはわからない…

ここでとりあえず適当なパスワードを入力してみると

パスワードが違うので怒られました。

しかしEmail Addressの欄に以下を入力すると…

test1@test.com' or 'a' = 'a

パスワードに何も入力していないのにログインできてしまいました…!

種明かし

BadStoreのコードをあさってみるとEmail Addressの欄に入力した内容は以下SQL文の「id」の部分に入ります。

SELECT * FROM userdb WHERE email='id' AND passwd='passwd'

ここに先ほどのtest1@test.com' OR 'a' = 'aが入ると

SELECT * FROM userdb WHERE email='test1@test.com' OR 'a' = 'a' AND passwd='passwd'

初めはパスワードはAND条件となっており必ず見られていましたが、OR 'a' = 'a'のOR条件が間に挟まったことで、パスワードを見ずに前方のemail='test1@test.comの条件がtrueになるものを取得することができてしまいます。

トライ②テーブルの情報を取得しちゃう

また別の不正行為に及んでみます。

エラーベースインジェクション

商品検索のフォームにSQLの構文として意味を持つ「'」を入力してみます。

すると、画面いっぱいにゴリゴリのスタックトレースが表示されてしまいました。

悪い人たちはここから「ほう、DBはSQLiteを使っているのか」「カラム名はitemnum,sdesc,ldescか」と情報を読み取ってしまいます。
これをエラーベースインジェクションといいます。

テーブル構成がわかるとさらに悪いことができてしまいます。

ユニオンインジェクション

UNION句では結合時に指定するカラム数やデータ型を一致させる必要があります。
先ほどのような方法でテーブル構成の情報を手に入れてしまえば、あれこれ試さずとも簡単にUNION句を使えてしまうのです。

商品検索のフォームに以下を入力してみます。

' UNION SELECT null,null,sql,null FROM sqlite_master --

するとテーブルのメタ情報を記録しているsqlite_masterテーブルの情報が結合して表示され、データベースにどのようなテーブルが存在するのか、そのようなカラム構成なのか詳しくわかってしまうのです…

次は商品検索のフォームに以下を入力してみます。

' UNION SELECT null,email,fullname,null FROM userdb --

すると、メールアドレスとユーザー名の情報が一覧で取得できてしまいました!
これは危ないですね…

対策

SQLインジェクションの対策方法は以下の通りです。

プレースホルダーを利用する
 ここでJava Goldの知識が生きる時…!
 以下の記事にも登場したJDBCのPreparedStatementのようなフレームワークの仕組みを使うことで想定外のSQL文組み立てを防げます。

radish-se.hatenablog.com

エスケープ処理を実施する

SQL的に意味のある’や-の文字列を無害化することで防ぐことができます。


長くなりましたが今日はここまでにします。
ありがとうございました。

【Android】飲食店記録アプリ〜データ設計〜

こんにちは!ぱかぱかです!
今日は前回の記事で要件を詰めていた飲食店を記録するアプリ「グルメノート」のデータ設計を行いたいと思います。

radish-se.hatenablog.com
radish-se.hatenablog.com

前回のまとめ

・アプリの目的、システム方式・構成、画面と機能、操作フローを決めた!
・カラーパレットを使って多少マシなデザインを作った!

ぱっと見こんな感じのアプリを作ろうとしている

データ設計とは

前回の記事でも述べた通り要件定義すらまともにやったことがない私、データ設計なんてなおさらよくわかっていません…
OSS-DB Silverを取得するなどDBについて勉強する機会はあったのですが、設計となるとまた話は別ですよね。
以下の動画と記事を見ながら、なんちゃってデータ設計を進めていきたいと思います!


www.youtube.com

giginc.co.jp

記事によるとデータベース設計における手順は以下の通り。

手順1. 要件定義を行う【概念設計】
手順2. エンティティを抽出する【概念設計】
手順3. ER図を作成する【概念設計】
手順4. ER図をRDBのテーブルに変換する【論理設計】
手順5. 正規化を行う【論理設計】
手順6. 性能要件を確認する【物理設計】
手順7. インデックスを作成する【物理設計】
手順8. データ格納領域を決める【物理設計】

今回の目標は手順3. ER図を作成する【概念設計】までとします!

手順1. 要件定義を行う【概念設計】

作成しようとしているデータベースで「どのようなデータをどう管理したいのか」を明確にすることからスタートこれにはデータベースを使用する対象の業務を詳細に分析し、必要な要件を洗い出すことが不可欠でしょう。

なるほど…というわけで、どのようなデータをどう管理したいのか考えます。
ここでまず、現状の画面構成を眺めてみます。


ふむ…まず画面から読み取れる必要そうな情報とその例を列挙してみましょう。

・店名:天下一品
・場所:東京
・ジャンル:ラーメン
・登録日:2023年10月15日
・メモ:佐藤さんおすすめ
・写真:ラーメンの画像
・評価:★★★☆☆
・訪問日:2023年10月15日
・シーン:女子会
・感想:初めてきた。めっちゃ濃い。

行った店リスト、行きたい店リスト、お気に入り店リストと3つのリストがあるけど、結局表示する情報が異なるだけで似たような情報を共有してそう。

あとは今後サーバで情報を管理することを考えると、ユーザ情報も別で持っておく必要がありそう。

手順2. エンティティを抽出する【概念設計】

エンティティといえば「ある目的のためにまとめられたデータのかたまり」で、もっと分かりやすく言えば「データベースにおけるテーブル」というイメージをもっていただけると分かりやすいでしょう。

今回の場合エンティティとしては何を定義すれば良いでしょうか…
なんのテーブルを作ればよさそうかなという観点で列挙してみます。

・飲食店
・場所
・ジャンル
・訪問記録(訪問日ごとに評価や感想を持つ)
・シーン
・ユーザ

中間テーブル
・行った店
・行きたい店
・お気に入り店

とりあえずこんなもんかな…?

手順3. ER図を作成する【概念設計】

業務に必要なエンティティを全て洗い出したら、それをもとにした「概念データモデル」を作成していきます。概念データモデルを作成する手法としてよく用いられるのが「ER図」です。

よし、とりあえずER図を作ってみましょう。
前回の画面遷移図に引き続き、今回もplantUMLを使って書いてみます。

んんん…これで合ってるのか…?
わからないけど考えずに突き進んだ前回よりはマシと信じたい…

おかしさに気づいたらまた直すとしましょう!
とりあえず今日はここまで!ではまた!