「とりあえず動く」を卒業するためのアーキテクチャ入門編(2)

2.プロジェクトの変化に対応する

皆さんこんにちは。株式会社コスモルートでアンドロイドアプリの請け負い開発をしているT.Mです。

前章でKotlinの感触が少しだけ理解できたと思います。
javaで書くよりもコードが減り、バグも少なくなるのでお勧めです。
興味のある方は充実したオンラインコースやサンプルアプリの実装例がgithubに載っているので、それを元に勉強するのもおすすめします。
もしかしたら簡単なアプリを作ったことがある人もいるかもしれませんね。
単にボタンを押したら画面遷移するアプリから、電卓アプリやToDo管理アプリを作ってみるのは学習としてテッパンですね。
そういう場合は自分で要件定義をして仕様を決めているので混乱はありませんよね。


一方、実際のプロジェクトではあなたが要件定義に参加することは稀です。
中規模以上の開発(10人月~)になると、要件も複雑になり、それぞれ矛盾する仕様も出てくるでしょう。
完璧な人間は存在しないのと同様、完璧な仕様もあり得ません。
ウォーターフォール型のプロジェクトであれば、要件定義と開発期間を区切りますが、開発期間にも仕様変更って絶対ありますよね?
要するに、変更に強い開発手法を探ることが大事です。

2.1 プロジェクト構成を考えよう

改めて、変更に強いという意味を考えてみましょう。もう少し詳しく定義すると
仕様変更により生じるバグが少ないと置き換えても良いと思います。これをもう少し掘り下げると、

・変更によるコード修正の範囲が明確かつ限定的
・テストコードが書きやすい

が挙げられます。
他にも「コードの意図が伝わりやすい」、「ドキュメントが整備されている」など挙げられますが、上の2つに絞って考えてみましょう。

2.2コード修正の範囲が明確かつ限定的とは?

まず、「変更によるコード修正の範囲が明確かつ限定的」を意識したプロジェクトはどうすればよいでしょうか?
考えるヒントとして、Twitterのバスツイートだけを表示するアプリを作ることを考えてみましょう。

要件としては、簡単のため

・バスツイート=フォロアーの100倍のいいね!がついたツイート
・当日のバズツイートをリストで表示
のみ考えます。
さて、どうやって実装しましょうか?

まずはActivityに全部実装してみましょう。

```kotlin                            
                              
  class MainActivity: AppCompatActivity(){                            
                              
      //Retrofit(Httpライブラリ)                        
      val twitterService = Retrofit.Builder().baseUrl("https://twitter....").build().create(TwitterService::class)                        
                              
       override fun onCreate(state:Bundle?){                        
            super.onCreate(state)                
            setContentView(R.layout.activity_main)                
                            
            val listView = findViewById<RecyclerView>(R.id.list_view)                
            //TODO adapterの設定                
                            
            //コルーチン(メインスレッドでの処理を回避する目的)                
            GlobalScope.launch(context = Dispatchers.Main){                
                withContext(context = Dispachers.IO){            
                               
                    val info:TweetInfo = twitterService.get(LocalDate.now())        
                    val items = info.tweets.filter{ tweet->        
                        //フォロアー数×100以上の場合をフィルター    
                        tweet.likes>=info.followers.size*100    
                    }        
                    //リストを表示する        
                    listview.adapter = TweetListAdapter(items)        
                }            
            }                
        }                    
  }                            
                            
```      


サンプルコードなのである程度省略して書きました。
コードが何をしているかは理解できると思います。

しかし、急にクライアントから次の要件が加わったとしましょう。
①オフラインでも直近に取得した情報を表示したい
②データを取得している間は,ローディングを表示してほしい
③リスト上からいいね!を押せるようにしてほしい

上記の修正に対応するには、
①はデータベースを用意し、
②はactivity_main.xmlにローディングダイアログを用意、
③はいいね!を押すAPIを用意すれば良いでしょう。
しかし、確実にコードは分かりにくく、複雑になっていきます。

複雑さを回避する方法として、コードを分割すれば良いと考えるのが自然でしょう。
ではどうやって分ければよいのか、基準を設ける必要があります。
有名なMVC(モデル・ビュー・コントローラー)でも良いかもしれません。
この場合は
・Model アプリ特有のロジックのこと。
    例えば,バズツイートの取得,オフライン時にデータベースから直近のデータを取得など。
・View バズツイートのリスト表示,ローディングダイアログの表示など。

  • Controller ユーザーによる画面アクション。「いいね!」を押す動作。

でそれぞれフォルダーを分けてみると、恐らく次のような構成になると思います。

  • model/
    • TwitterRepository.kt オンライン/オフライン時を判断して、TwitterApiとTwitterDatabaseからデータを取得および、いいね!の反映。
    • TwitterApi.kt ツイートの取得と、いいね!を押して反映するAPI
    • TwitterDatabase.kt ツイートのデータベース
    • TwitterModel.kt ツイートのモデル(フォロアー数やツイート内容などを格納、バスツイートを判断)
  • view/
    • MainActivity.kt TwitterListFragmentのActivity
    • TwitterListFragment.kt リスト表示画面
    • TwitterItem.kt リストに表示される内容を格納
    • LoadingView.kt ローディング画面のカスタムView
  • controller/
    • PushLike.kt View.onClickの実装。TwitterRepositoryから、いいね!を反映する

実際にはもう少しファイルが増えると思いますが、機能ごとにうまく分けられている感じがします。
肝心のメリットについて考えてみましょう。
例えば、更に次の要件修正が入るとします。

・リツイート数も表示してほしい。

  • バスツイートの定義を、100,000ツイートで固定にしてほしい

「この要件、もっと早く知りたかったな…」って思いながらも、どのファイルを修正すれば良いか、初めてコードを見た人でも理解ができるのではないでしょうか?


1番目の修正については、TwitterItem.ktを修正、3番目はTwitterModel.ktを修正すれば良いと分かります。
これがMainActivity.ktにすべて実装してあったら、修正箇所の当たりを付けづらく、1つの修正が他の機能に影響するかも知れません。
上手くコードを分割した場合のメリットが分かりましたね。

さて、今回のタイトルが「アーキテクチャー入門」なのでMVCの概念を知らない前提で話しましたが、改めて見ると問題点もあります。
例えばcontroller部分ですが、わざわざonClickの実装だけ分けるメリットはあるでしょうか。
またデータ取得の途中でアプリを閉じた場合はどうなるでしょうか?


Android特有のライフサイクルが考慮されてない気がします。
Androidフレームワークに依存する部分とそうでない(いわばPureなkotlinで書ける)部分を分けなくてもよいでしょうか?
実際には上記に挙げた問題点は、ライブラリの発展とアーキテクチャーの洗練で補えますが、それは第3章に任せましょう。

今回は、
2 プロジェクトの変化に対応する
2.1 プロジェクト構成を考えよう
2.2 コード修正の範囲が明確かつ限定的とは?
について、説明しました。

次回は、テストコードについて考えていきます。

「とりあえず動く」を卒業するためのアーキテクチャ入門編(1)

皆さんこんにちは。株式会社コスモルートでアンドロイドアプリの請け負い開発をしているT.Mです。

僕自身コスモルートは2社目で、前職はAndroidアプリの開発や機械学習の業務を行っていました。
その後コンサルタントとして入社後、2年程上流工程を経験してます。
なので仕様変更に対する上流と下流(そもそもこの呼び方が良くない笑)のせめぎ合いや、「これ要件に書いてないですよ?」や「そもそも要件というより前提だよね?」といった喧嘩会議を両側から経験してきました。
要件定義で完璧に仕様が決まり、未来永劫変更が無い—そう断言できれば安心して開発ができます。
しかし実際のところ、プロジェクトは生き物のように変化し、仕様が固まるということは殆どありません。
選択肢は2つ。
1:「仕様が確定しないのが悪いんだ!」と悪態をつく
2:プロジェクトの変化に柔軟な開発を目指す
あなたはどちらを選びますか?

1.Android/Kotlin再入門

この章ではAndroidをJavaでしか書いたことが無い人向けのKotlinの紹介と、僕のように数年開発現場を離れていた人向けのアップデート事項をまとめました

1.1 Kotlinとは?

もともとAndroidアプリの開発にはjavaが必須でしたが、2019年でのI/OではKotlinを第一言語とし、公式サポートもこれまで以上に充実させるとしました。
言語的な特徴はいくつかありますが、中でもnullを上手く扱い、エラーを発生させにくい仕組みが受け入れられました。
まずはそれをお定まりのHelloWorldで見てみましょう。

val str:String? = null
print(str?:”Hello world”)

`?`が沢山みえますが、これがnullを管理する印です。
具体的には`String?`で定義された変数は**String型**と**null**を許容します。
これはjavaの**String**に相当します。
2段目の`str?:”Hello world”`は**str**がnullの場合、**Hello world**を出力するよという意味です。
一般的にはnullをなるべく除外したいので、?はあまり使いたくありません。
Javaから受け取った値をKotlinに渡す場合や、サーバーからのレスポンスでnullが入り込む場合などは`Type?`で変数を定義します。

変数がnullでない時のみ、関数を適用したい場合があります。
そういう場合は`let`関数がおすすめです。

入力されたパスワードが正しいか判定する場合は、

val input:String? = "password"
val isHelloWorld :Boolean? = str?.let{checkValid(it)} // let内のitはnullでない

と書けば、`if(input!=null) … else …` のような条件式は不要で、簡潔に記述できます。

1.2 変数の定義方法

変数の定義の仕方について触れておきます。
javaでは変数は2種類で、代入可能なもの(可変変数)と不可能なものです。
kotlinも同じく可変変数と不変変数があり、それぞれ以下の様に定義します。

val str = “Hello, world” //不変
str = “Hello, kotlin” // 代入不可能なのでコンパイルエラー
var str2 = str
str2 = “Hello, kotlin” //エラーにならない

kotlinに限った話ではないですが、なるべく不変変数を使うようにしましょう。
たとえばユーザーの生年月日を定義する場合、`var birthday:LocalDate`とはしないはずです。
見た目は若くできても実年齢は変わらないので、不変であるべきです!

一方で、例えば「パスワードを変更する」や、「パスワードを再登録する」などのユーザーアクションが考えられるので、パスワードは可変です。

var password:String

ですね。

同様にニックネームや好きな食べ物などの情報は可変、生まれ故郷や性別は(特別な事情がない限り)不変として良いでしょう。

これで変数の定義方法については終わります。

ただし`var password`には少々問題があります。
このままだと、`0000`や`cosmoroot`のような安易なパスワードが入力される恐れがあります。
そこで、登場するのがjavaでお馴染みのsetter/getterです。

1.3 変数のカプセル化(setter/getter)

例えば開発要件として、**パスワードは4文字以上8文字以下とする**と書いてある場合はどうしますか?
更に**パスワードは暗号化してサーバーに送る**という要件も追加されました。
そこで役立つのがsetter/getterというカプセル化の概念です。
実際にkotlinのコードを見てみます。

var password:String? = null
set(newPass:String?){
// 4~8字以内の場合は
val withIn:Boolean? = newPass?.let{IntRange(4,8).contains(it)}
//newPassを代入
if(withIn) field = newPass
}
get = hashed(field) //Hash化関数
password = “password”
print(password) // Hash化されたパスワードが出力される
password = “tooLongPassword”
print(password) //条件に満たさない場合は、nullが出力される

コード内コメントにある通り、**4文字以上8文字以下**の条件を満たさない場合は代入できませんし(setterによる入力の制限)、出力はHash化されます(getterによる出力の制限)。
ただし、まだ`null`が残っていますね。
改善の余地がありますが、次のクラス定義にまわします。

1.4 クラス定義

上で見てきたユーザー情報はクラスにすることができます。

例として

– ユーザーID
– パスワード

をもつ`User`クラスを定義しましょう。
kotlinでは

//Userクラス定義 uidは不変
class User(val uid:String,var password:String)
val user:User = User(“cosmoroot”,”password”)
print(“$user.id”) // cosmoroot と出力

と書けます。
javaと雰囲気が似てますが、クラス変数`uid`および`password`をコンストラクタで定義できるのが便利ですね。
更にpasswordは勝手に変更されては困るので、改良します。

class User(val uid:String,_password:String){
var password:String = _password
set(newPass:String){
val withIn:Boolean = IntRange(4,8).contains(newPasss)
if(withIn) field = newPass
}
get = hashed(field) //Hash化関数
}
val user = User(“cosmoroot”,”password”)
user.password = “tooLongPassword” //無効なパスワードを代入しても
print(user.password) //passwordと出力

クラス化によってpasswordはUser生成時に指定すればよいので、`null`は削除できました。
ただし、Userクラスにしたので、パスワードはsetter/getterで定義するよりも、**changePassword**関数を定義した方が分かりやすいですね。

/***
@param _passowrd:パスワード
private変数もコンストラクタで定義できる。クラス外からは参照されない。
*/
class User(val uid:String,private var _password:String){
fun changePassword(newPassword:String):Boolean{
return if(IntRange(4,8).contains(newPasss)){
_password = newPassword
true
}else false
}
}
val user = User(“cosmoroot”,”passoword”)
user.changePassword(“tooLongPassword”) // return false

とてもシンプルになりました!
また`changePassword`メソッドを定義したおかげで分かりやすくなりました。

 

まだまだ続けたいところですが、長くなってきたので、今回はこのあたりで終わりにします。

最近はKotlin関係のオンラインのコースもかなり充実していますし、サンプルアプリの実装例がgithubに載っているので、それを元に勉強するのもおすすめですよ。

次回は「プロジェクトの変化に対応する」を予定しています。
お楽しみに。