👏

脱プログラマー初心者

2024/09/20に公開

概要

このページは筆者が業務や勉強している際にプログラムの書き方で学んだことを書いているサイトです。現在サーバーサイドKotlinを使用して業務を行っているので、Kotlinを使用してプログラムを書いていこうと思います。
※サンプルの型定義は適当に記載しています

ページの構成としては、基本的な内容から、条件分岐、配列、関数、クラス、その他といった順番で記載していっています。

参考資料

大切な基本

1.わかりやすい名前を使う


// 一文字の変数名
val a = 1

// 省略した変数名
val qty = 1

// 意味のある単語を使用した変数名
val quantity = 1

上記の例はわかりやすいですが、変数名やクラス名など見ただけで何かわかるような名前をつけないと次の日の自分に怒られてしまいます。

2.空白行を使用する


// 空白行がはっきりしないコード
val price = quqntity * unitPrice
if(price < 3000) price += 500
price = price * taxRate()


// 空白行があり見通しがあるコード
val price = quqntity * unitPrice

if(price < 3000) price += 500

price = price * taxRate()

空白行を使用することで何を行っているのか見やすくなったと思います。

3.目的毎に変数を用意する


// 変数を使い回している
val price = quqntity * unitPrice

if(price < 3000) price += 500

price = price * taxRate()

// 目的毎に変数を作成している
val basePrice = quqntity * unitPrice

val shippingCost = if(price < 3000) 500 else 0

price = (basePrice + shippingCost) * taxRate()

目的毎に変数を使用することで、データを追いやすくなりプログラムも柔軟に変更することができると思います。※if文の箇所はKotlin独特の書き方なので、各言語に合った書き方をしてください


// 色々なロジックが散らばっている
val basePrice = quqntity * unitPrice

val shippingCost = if(price < 3000) 500 else 0

price = (basePrice + shippingCost) * taxRate()

// ロジックは関数として独立させる
val basePrice = quqntity * unitPrice

val shippingCost = shippingCost(basePrice)

price = (basePrice + shippingCost) * taxRate()

fun shippingCost(basePrice : int){
    return if(basePrice < 3000) 500 else 0
}

if文の条件分岐の箇所を関数として独立させました。
これにより、今後if文の条件を変更する際に変更しやすくなり保守性が上がります!

条件分岐

プログラムを作成する上で、if・switch文を使用した条件分岐をよく使用すると思いますが
このif文が厄介でプログラムを複雑にする原因の一つとなってしまう可能性があります。
ここではif・switch文とのより良い付き合い方について記載していこうと思います。

1.if文のロジックをメソッドに独立させる


class Test(val name:String){

    /**
     * if文の中にロジックが記載されている
     * ロジックの追加や変更があった際に変更が大変になる
     */
    fun test(){
        if(name.count() > 10){
            println("文字数が大きい")
        }
    }

    /**
     * if文のロジックがメソッドとして独立するため
     * 追加や変更がしやすくなる
     * また可視性もよくなる
     */
    fun test2(){
        if (checkNameCount()){
            println("文字数が大きい")
        }
    }
    
    fun checkNameCount(): Boolean {
        return name.count() > 10
    }
}

if文のロジックは基本的に分けた方がいいと思います。
上記にも記載している通り、保守性が上がります。
※業務では本当にさまざまなロジックが大量に必要になる..

2.elseがなくなると条件分岐が単純になる


class Test(val name: String) {

    fun checkNameCount10(): Boolean {
        return name.count() > 10
    }

    fun checkNameCount20(): Boolean {
        return name.count() > 10
    }

    /**
     * elseを使用することで可視性が低くなってしまっている
     */
    fun test(): Int {
        var result:Int
        if (checkNameCount10()) {
            result = 10
        } else if (checkNameCount20()) {
            result = 20
        } else {
            result = 0
        }
        return result
    }

    /**
     * elseの使用をやめて、不要な変数をなくすことで
     * 可視性が上がっている
     */
    fun test2():Int {
        if (checkNameCount20()) return 20
        if (checkNameCount10()) return 10
        return 0
    }
}

3.複数の条件分岐を統合


fun example1(): Int {
    val userA = User()

    if (userA.admin) return 10
    if (userA.age > 20) return 10
    if (userA.isTellPhone) return 10
    return 0
}

fun example2():Int {
    val userA = User()
    if(method1(userA))return 10
    return 0
}

// 複数あった条件をorで繋げています
fun method1(userA: User): Boolean{
    return userA.admin || userA.age > 20 || userA.isTellPhone
}

複数の条件があるのにも関わらず返却する値が全て同じ場合は、条件を統合した方がいいと思います。
ただし複数の判定を単一の判定と考えるのであれば実施しない方がいいです。

4.ストラテジーパターン


data class Yen(val money:Int){
    fun add(yen:Yen): Yen {
        return Yen(money + yen.money)
    }
}

class AdultFee{
    fun fee1(): Yen {
        return Yen(100)
    }

    fun label1():String{
        return "大人"
    }
}

class ChildFee{
    fun fee2(): Yen {
        return Yen(50)
    }

    fun label2():String{
        return "子供"
    }
}

class SeniorFee{
    fun fee3(): Yen {
        return Yen(80)
    }

    fun label3():String{
        return "シニア"
    }
}

/**
 * 関数を実行するにあたり
 * どのクラスの関数を実行するかロジックが発生してしまっている
 */
class Charge{
    fun <T>yen(fee : T){
        if(fee is AdultFee){
            fee.fee1()
        }
        if(fee is ChildFee){
            fee.fee2()
        }
        if(fee is SeniorFee){
            fee.fee3()
        }
    }
}

interface Fee{
    fun yen():Yen
    fun label():String
}

class AdultFee1: Fee{
    override fun yen(): Yen {
        return Yen(100)
    }

    override fun label():String{
        return "大人"
    }
}

class ChildFee1 : Fee{
    override fun yen(): Yen {
        return Yen(50)
    }

    override fun label():String{
        return "子供"
    }
}

class SeniorFee1: Fee{
    override fun yen(): Yen {
        return Yen(80)
    }

    override fun label():String{
        return "シニア"
    }
}

/**
 * interfaceを使用することで
 * どのクラスの関数を実行するか、呼び出し側で考慮する必要がなくなる
 */
class Charge1(private val fee : Fee){
    fun yen(): Yen {
     return fee.yen()
    }
}

メソッドを実行する際に条件分岐を使用する際は、上記の用にinterfaceを使用することで呼び出し側は何も意識することなくメソッドを実行することができます。
このようにメソッドの実行に観点を置く設計をストラテジーパターンと言います。
実際の業務にもすぐに使えそうな設計パターンですね!
ストラテジーパターンと似たデザインパターンにstateパターンがあります。
このパターンは、ロジックではなく状態に観点をおいて設計するパターンです。
個人的な考えになりますが、両方とも同じような実装になるかと思います。ですので、観点は異なりますが、実装が同じと認識していいのではないかと思います。

/**
 * 上記を応用すれば、反復処理でも各クラスのことを意識する必要もなくなる
 * コードの可視性と保守性が上がる
 */

class Reservation(val fee:List<Fee>){

    fun feeTotal(): Yen {
        var total = Yen(0)
        fee.forEach{ i ->
            total = total.add(i.yen())
        }
        return total
    }
}

/**
 * さらにあらかじめMapなどに区分されるキー名と
 * インスタンスを格納しておけば、クラスを生成する段階でも
 * ifをなくすことができる。
 * 言語によっては列挙型を使用することでさらにわかりやすく簡単に
 * プログラムが書けるようになるため、調べてみてほしい
 */

class FeeFactory {
    private val types:Map<String, Fee> = mapOf(
        "adult" to AdultFee(),
        "child" to ChildFee(),
        "senior" to SeniorFee(),
    )
    
    fun feeByName(name:String): Fee? {
        return types.get(name)
    }

}

5.ヌルオブジェクトをあえて作成する

上記のようにMapやswitch等を使用する際に、どのパターンにも該当しないケースがあるかもしれません。そういった際に呼び出し側でNull判定を行うのではなく呼ばれる側で、あえてNullオブジェクトを作成することで、呼び出し側でNullを意識する必要がなくなります( ・∇・)
下記ではMapを例にしていますが、スイッチやメソッドのレスポンスなど応用はたくさん聞くと思います。(Nullを意識しなくても良くなるのは個人的にとてもいいと思う)


val types:Map<String, String> = mapOf(
    "adult" to "test1",
    "child" to "test2",
    "senior" to "test3",
)

fun main(){
    example2()
}

// 呼び出し側で、Null対策用の条件分岐が存在する
fun example1(){
    val name = "adult"
    val value = types.get(name)
    if(value != null) println("value: $value")
    println("nullでした")
}

// 呼び出し側で、Null対策用の条件分岐が存在しないため、コードがスッキリと見える
fun example2(){
    val name = "adult1"
    val value = getMapValue(name)
    println(value)
}

// 呼び出される側ではNullの時の対策が必要
fun getMapValue(name:String): String {
    if(types.containsKey(name)) return types.get(name)!!
    return "Not Value"
}

6.enumとMapを使用した状態遷移の応用


enum class State {
    REVIEW,
    APPROVED,
    INPROGRESS,
    COMPLETED,
    RETURNED,
    SUSPENDED,
}


/**
 * 実際の業務では状態の遷移があると思います。
 * その際に条件分岐のif文を使用するかもしれませんが
 * enumとMapを使用することで複雑な条件分岐を作成しなくても
 * プログラムを書くことができます。
 */
class StateTransitions {

    private val allowed: Map<State, Set<State>> = mapOf(
        State.REVIEW to setOf(State.COMPLETED, State.RETURNED),
        State.RETURNED to setOf(State.INPROGRESS, State.COMPLETED),
        State.APPROVED to setOf(State.INPROGRESS, State.COMPLETED),
        State.INPROGRESS to setOf(State.SUSPENDED, State.COMPLETED),
        State.SUSPENDED to setOf(State.INPROGRESS, State.COMPLETED)
    )

    fun canTransit(from: State, to: State): Boolean {
        val allowedStates = allowed.get(from)
        return allowedStates?.contains(to) ?: false
    }
}

ここでは承認関係の状態遷移を例にしましたが、他にもステータス遷移の応用ができると思います。
これで複雑なif文を書くことがなくなります!

7.条件を部品化してしまおう!(ポリシーパターン)

ここでは条件を部品化するポリシーパターンを記載していきます。
どういうことかは例を見てもらえたら伝わると思いますが、実務では特定な条件を全てパスした場合に実行する処理などがあると思いますが、条件があまりに多いい場合がよくあると思います。
ポリシーパターンは条件を判定する側が、判定したい条件をセットするだけで他には何も意識せずに使用することができるパターンとなっています。


/* 条件の部品の元を作成 */
interface Rule {
    fun validation(history: Any): Boolean
}

/* 条件の部品を作成 */
class AdminRequest : Rule {
    override fun validation(history: Any): Boolean {
        TODO("Not yet implemented")
    }
}

/* 条件の部品を作成 */
class GeneralRequest : Rule {
    override fun validation(history: Any): Boolean {
        TODO("Not yet implemented")
    }
}

/* 条件の部品を集めてまとめて実行するクラスを作成 */
class RequestPolicy {
    private val rules = mutableSetOf<Rule>()

    fun add(rule: Rule) {
        rules.add(rule)
    }

    /* 判定を行いますが、ここでは例外にany型を使用しています */
    fun complyWithAll(history: Any): Boolean {
        for (each in rules) {
            if (!each.validation(history)) return false;
        }
        return true;
    }
}

/* 初期化処理の際に必要な条件パーツを追加する */
class Request {
    val policy = RequestPolicy()

    init {
        policy.add(AdminRequest())
        policy.add(GeneralRequest())
    }

    /* これを呼び出してルール判定を行う */
    fun complyWithAll(history: Any): Boolean {
        return policy.complyWithAll(history);
    }
}

配列、コレクション

プログラムを作成する上で、配列もよく使う場面があると思います。
この配列も扱い方を気をつけないと思わぬバグの原因となってしまいます。

1.自前でコレクション処理を実装してしまう

各プログラミング言語には、コレクション処理をスムーズにする機能があります。
自作で作成してしまうと車輪の再発明となってしまうので、積極的に利用するようにしましょう。

/* 冗長なコードになってしまっている */
fun example1(): Boolean {
    val lists = mutableListOf("test1", "test2", "test3")
    var isTest2 = false
    for (list in lists) {
        if (list.equals("test2")) {
            isTest2 = true
            break
        }
    }
    return isTest2
}

// わずか2行に収まってしまいました!
fun example2(): Boolean {
    val lists = mutableListOf("test1", "test2", "test3")
    return lists.contains("test2")
}

2.配列内の条件分岐

配列内に存在する条件分岐も上記に記載している、条件分岐の箇所と同様な対応をする必要がある。
特に特色すべきは早期リターンを行う最右にcontinuebreakを使用することで、配列内の操作を次の要素に移動したり、配列内の処理を終わらせることができます。
※パフォーマンスを考えるとコレクション操作よりも、素直にfor文を使用した方がいいかもしれません


fun example1() {
    val persons = mutableListOf(
        Person(20, "tokyo"),
        Person(25, "tibia"),
        Person(30, "katakana")
    )
    for (person in persons) {
        if (person.age < 20) continue
        if (person.address == "tibia" || person.age < 30) {
            person.winners = true
        }
    }
}

/* mapを使用してみたパターン */
fun example2() {
    val persons = mutableListOf(
        Person(20, "tokyo"),
        Person(25, "tibia"),
        Person(30, "katakana")
    )
    val newPerosns = persons.map {
        if (it.address == "tibia" && it.age < 30) {
            it.copy(winners = true)
        } else {
            it
        }
    }
}

関数

1.基本

関数はクラス内や関数そのもので使用されていますが、副作用とゆう言葉があります。
この副作用とは、引数を受け取り、戻り値を返す以外に外部の状態を変更してしまうことを言います。
例を挙げるとインスタンス変数の変更、グローバル変数の変更、引数の変更、ファイルの読み書きなどのI/O操作などが挙げられます。
副作用のある関数は、影響範囲を把握することが難しく保守しにくくなってしまいます。
これを防ぐには、関数が次のことを満たすことを前提に設計することが大切です。

  • 状態を引数で受け取る
  • 状態を変更しない
  • 値は関数の戻り値として返す

引数で状態を受け取り、状態を変更せずに、値を返すだけの関数が理想の関数と言われています。
ここではAttackPowerクラスを例に不変な関数を作成することの例を記載します。


class AttackPower(value:Int){
    val min = 0
    val value = value

    init {
        require(value < min) { throw Error("不正な値がリクエストされました")}
    }

    // 攻撃力を強化する
    fun reinForce(increment:AttackPower):AttackPower{
        return AttackPower(value + increment.value)
    }

    // 攻撃力をゼロにする
    fun disable(): AttackPower {
        return AttackPower(min)
    }
}

上記のAttackerPowerクラスは関数でインスタンス変数を変更する際は、新規でインスタンスを作成する事で、インスタンスを使い回す事なく思わぬ副作用が出ないようにしています。

2.結果を返すために引数を使わない

出力引数と言われる引数に結果を返すためのものを設定すると思わぬ副作用を発生させてしまう可能性があります。


class ActorManager {

    // インスタンス変数が存在する

    /**
     * 位置を変更する関だが、引数のlocationに値を設定しまっている
     * これでは別の場所で似たようなコードが発生する可能性があり、保守性が低い
     */
    fun shift(location: Location, shiftX: Int, shiftY: Int) {
        location.x += shiftX
        location.y += shiftY
    }

}

/**
 * データとデータを操作するロジックを同じにすることで
 * 副作用なく関数を実行することができる
 */
class Location(x: Int, y: Int) {
    val x: Int = x

    val y: Int = y

    fun shift(shiftX: Int, shiftY: Int): Location {
        val nextX = shiftX + x
        val nexty = shiftX + y
        return Location(nextX, nexty)
    }
}

3.多すぎる引数

ここでは例を記載しませんが、関数に引数が多すぎる場合、その関数内で行いたいことが多くある場合が多いいです。その場合、関数を分解したりして引数の数を減らすべきです。
主な方法としては、意味のある単位毎にクラス化することを目指すといいと思います。また、引数の数の目安としては3つ以上ある場合は検討した方がいいと思います。

4.関数の抽出

関数の抽出とはある関数内で行なっている処理をまとめて関数に抽出することを指します。
具体的には関数内が6行以上あり、行なっている処理に名前をつけたい場合などに使用できると思います。これを行うことで、一つの関数がとてもコンパクトになり、保守性や可読性を上げることができます。上記に記載した注意事項を意識しながら例を記載します。


/* 処理が6行以上存在する */
fun example1(samples: Any) {
    var totalValue = 0

    println("**************")
    println("****Hello*****")
    println("**************")

    for (sample in samples) {
        totalValue = sample.value
    }

    println("name: ${samples.name}")
    println("totalValue: ${totalValue}")
}

/* 関数の抽出を行うことでスッキリ! */
fun example2(samples: Any) {
    printBanner()
    val totalValue = calculateTotalValue(samples)
    printDetails(samples, totalValue)
}

fun printBanner() {
    println("**************")
    println("****Hello*****")
    println("**************")
}

fun calculateTotalValue(samples: Any): Int {
    var totalValue = 0
    for (sample in samples) {
        totalValue = sample.value
    }
    return totalValue
}

fun printDetails(samples: Any, totalValue: Int) {
    println("name: ${samples.name}")
    println("totalValue: ${totalValue}")
}

クラス設計

1.基本

クラスを作成する際は、クラス単体で動作することができるように設計することが大切です。
プロパティしかない、メソッドしかないクラスでは複数の箇所にロジックが重複してしまう可能性があり、可読性、保守性が低いコードになってしまう可能性があります。
極端な例を言えば、メソッドには引数を使用せずにプロパティのみ使用してクラス単体で動作するようにクラス設計することが望ましいと思います。

// よくない例

// プロパティしかないデータクラス
class Person(name:String , age:Int){
    val name:String

    val age:Int
}

// メソッドしかないクラス
class PersonMethodOnly{
    fun changeName(){
        // 何かした処理を実行
    }
}

// 良い例
// プロパティとメソッドが両方存在する
class Person(name: String, age: Int) {
    val name: String = name

    val age: Int = age

    fun changeName(){

    }
}

2.狭い関心事に特化したクラスを作成する

クラスを作成する際は狭い関心事に特化したクラスを作成した方が保守性が上がります。
もし作成したクラスが大きい場合は、関心事毎に分類しクラスに分けることでコードがスッキリします。


class ShippingCost(val basePrice : int){

    private val minimumForFree  = 3000
    private val cost = 500

    fun amout(){
        return if(basePrice < minimumForFree) cost else 0
    }
}

3.保守性が高いクラス

ここではMoneyクラスを保守性を高くするために少しづつ変更していく例を順番に記載していきます。


// クラスとインスタンス変数を容易しただけ
class Money1(
    val amount: Int,
    val currency: Currency
) {
}

/**
 * インスタンス変数に不正な値が入り混まないように修正
 * このようにインスタンス変数に不正な値が入り込まないようにすることをガード節と言います
 * ※書き方はKotlin独特の書き方になっています
 *
 */

class Money2(
    val amount: Int,
    val currency: Currency
) {
    init {
        require(amount >= 0) { throw Error("amountに不正な値が入りました")}
        requireNotNull(currency){ throw Error("currencyがnullとなっています")}
    }
}

/**
 * ロジックをデータ保持側に寄せる
 * addメソッドを持つことによりインスタンス変数をクラス内で操作できるようになりました。
 * さらにインスタンス変数の値を変える際には新規Money3クラスを作成することで
 * インスタンス変数を不変のものにすることができました。
 *
 * この時にメソッドの引数やローカル変数を不変にすることがさらに保守性が増します
 */
class Money3(
    val amount: Int,
    val currency: Currency
) {
    init {
        require(amount >= 0) { throw Error("amountに不正な値が入りました")}
        requireNotNull(currency){ throw Error("currencyがnullとなっています")}
    }

    fun add(other:Int): Money3 {
        val newAmount = amount + other
        return Money3(newAmount,currency)
    }
}

/**
 * さらに上記クラスのaddメソッドの引数をInt型ではなくMoenyクラスにすることで
 * メソッドに不正な値が入らなくなり、さらに保守性が上がります
 * ※currencyが同一かどうかも一緒に確認しています。
 */

class Money4(
    val amount: Int,
    val currency: Currency
) {
    init {
        require(amount >= 0) { throw Error("amountに不正な値が入りました")}
        requireNotNull(currency){ throw Error("currencyがnullとなっています")}
    }

    fun add(other:Money3): Money3 {
        if(checkNotEqualsCurrency(other)) throw Error("通貨の種類が異なります")
        val newAmount = amount + other.amount
        return Money3(newAmount,currency)
    }

    fun checkNotEqualsCurrency(other:Money3): Boolean {
        return other.currency == currency
    }
}

上記のように修正していくことで、より良いクラスを設計することができます。
設計パターンには色々な種類があり、一部例を記載します。

  • 完全コンストラクタ
    • 不正な状態から防護する
  • 値オブジェクト
    • 特定の値に関するロジックを集める
  • ストラテジ
    • 条件分岐を削減し、ロジックを単純にする
  • ポリシー
    • 条件分岐を単純化したり、カスタマイズできるようにする
  • ファーストクラスコレクション
    • コレクションや配列に関するロジックを集める
  • スプラウトクラス
    • 既存のロジックを変更せずに安全に新機能を追加する

4.基本テータ型の落とし穴


// 一見問題なさそうに見えますが、basePriceはint型でマイナスの数字も入ってしまうこともある
class ShippingCost(val basePrice : int){

    private val minimumForFree = 3000
    private val cost = 500

    fun amout() {
        return if(basePrice < minimumForFree) cost else 0
    }
}

// basePriceの値に不正な値が入らないようにバリデーション処理の実装
class ShippingCost(val basePrice : int){

    private val basePriceMIN = 1
    private val basePriceMAX = 100000000000

    private val minimumForFree = 3000
    private val cost = 500

    init {
        if(basePrice < basePriceMIN || basePrice > basePriceMAX){
            throw Error("不正な値です")
        }
    }

    fun amout() {
        return if(basePrice < minimumForFree) cost else 0
    }
}

クラスの値をバリデーションする事で絶対に入らない不正な値をチェックすることができ、クラスの堅牢性が高くなります。
さらにこれは文字列や電話番号やさまざまな内容にも応用できると思いますので、業務ロジックやルールをよく理解しながら不正な値を入らないようにするように気をつけるといいです。

5.値を使うための専用クラスを作成する(値オブジェクトの作成)


// basePriceはint型にしていますが、そもそもbasePriceをクラス化してもいいかも..?
class ShippingCost(val basePrice : int){

    private val basePriceMIN = 1
    private val basePriceMAX = 100000000000

    private val minimumForFree = 3000
    private val cost = 500

    init {
        if(basePrice < basePriceMIN || basePrice > basePriceMAX){
            throw Error("不正な値です")
        }
    }

    fun amout() {
        return if(basePrice < minimumForFree) cost else 0
    }
}

/**
 * basePriceを別のクラスにする事でbasePriceの値チェックを別のクラスで行うことができ、関心毎の分離を行うことができました。
 * またbasePriceに何か変更が入った際に保守しやすくもなります
 */
class ShippingCost(val basePrice : BasePrice){

    private val minimumForFree = 3000
    private val cost = 500
    

    fun amout():Int {
        return if(basePrice.price < minimumForFree) cost else 0
    }
}

class BasePrice(val price: Int){

    private val basePriceMIN = 1
    private val basePriceMAX = 1000000000

    init {
        if(price < basePriceMIN || price > basePriceMAX){
            throw Error("不正な値です")
        }
    }
}

インスタンス変数だけでなくメソッドの引数にも応用することができ、予期せずメソッドが使われる心配を減らすことができる

値オブジェクトは不変にする


/**
 * 上記で作成したBasePriceクラスで値を書き換えたい時にsetterを使用してしまうと..
 * クラス生成時のバリデーションが行えなくなり予期せぬ副作用を及ぼす可能性がある
 */

fun main1(){
    val basePrice = BasePrice(100)
    basePrice.price = 300
}

/**
 * 値は異なる場合は別のクラスを新規で作成する
 * この事で予期せぬ副作用を防ぐことができる
 */

fun main2(){
    val basePrice1 = BasePrice(100)
    val basePrice2 = BasePrice(300)
}

setterは便利で使いやすいですが、副作用を発生させてしまう原因にもなってしまいます。
そのため、基本的にはsetterを使用しない方がいいと思います。

6.クラス内での配列の扱い方~ファーストクラスコレクション

ファーストクラスコレクションとは、配列・コレクションの低凝集(データとロジックがバラバラの状態)を防ぐことを目的として作成される設計パターンです。データとロジックを一つのクラスにすることで、保守しやすいコードを作成することができます。


 * Listの操作をクラスの中で行うことでコードを保守しやすくなります。
 * 保守しやすくするために、クラスの中で配列は一つだけにする
/* 要素を追加・編集する際は必ず新しいクラスを返却している */
/* データとロジックが同じクラスにあることで、他の箇所で似たような処理が書かれることがなくなる */
class Example3(private val persons: List<Person>){

    fun add(person: Person): Example3 {
        val mutableList = persons.toMutableList()
        mutableList.add(person)
        val newList = mutableList.toList()
        return Example3(newList)
    }
    
    fun edit():Example3{
        val newList = persons.map {
            if (it.address == "tibia" && it.age < 30) {
                it.copy(winners = true)
            } else {
                it
            }
        }
        return Example3(newList)
    }
}

クラス内の配列やコレクションのgetter


/**
 * 先程のクラスでListのgetterを行おうとしますが
 * この場合Listの参照先を渡してしまう可能性があるため
 * 思わぬ副作用が発生する可能性があります
 */

data class Customer(
    val name: String,
    val email: String
)


class Customers(val customer: List<Customer>) {

    fun getCustomer(): List<Customer> {
        return customer
    }
}

/**
 *  配列やListを渡す方法として以下の三種類があります。
 *  ・操作ロジックをListクラスに移動する
 *  ・操作結果も同じ型のクラスとして返却する
 *  ・不変にして外部に渡す
 */

class Customers2(val customers: List<Customer>) {

    fun add(customer: Customer): Customers2 {
        // 既存の顧客リストを可変リストにコピーする
        val newCustomers: MutableList<Customer> = customers.toMutableList()
        // 新しい顧客をリストに追加する
        newCustomers.add(customer)
        // 新しいリストを持つCustomers2のインスタンスを返す
        return Customers2(newCustomers)
    }
    
    // どうしても参照を渡す際は必ず不変の状態で渡す
    fun getCustomers(): List<Customer> {
        return customers
    }
}

配列やコレクションを参照する場合は、目的を把握してクラス内のロジックを含めないか検討すること。
そうすることで思わぬ副作用を防ぐことができます。

staticメソッドやUtliクラス

基本

staticクラスやUtliクラスは基本的に作成すべきではありません。
理由は似たようなロジックが複数箇所に実装されてしまう可能性があるので、可視性が低くなり、どこに何が書いてあるのか把握することが難しくなってしまうためです。
ですが、以下のような機能を横断して使用するものに関しては作成してもいいと思います。

  • ログ出力
  • エラー検出
  • デバッグ
  • 例外処理
  • キャッシュ
  • 同期処理
  • 分散処理

上記処理は個別のクラス毎に作成していては、逆にコードの可視性を低くしてしまいますし、共通処理にしておいた方が何かと都合がいいと思います。

ここから先は筆者が気ままに更新していきます٩( ᐛ )و

Discussion