概念を型として抽出するリファクタリングの例

昨日行なったリファクタリングが、概念を型として抽出するリファクタリングのわかり易い例なんじゃないかと思って記録します。

whoisの結果の生のテキストをパースして、有効期限等を取得するパッケージです。

リファクタリング前後の差分

古いコード

リファクタリング対象の元コードです。 やっていることは、whoisの結果の生データが入っている lines と 値を取得する正規表現 regex を引数に取って、取得した値を返却する関数として実装していました。regex の例としては、/^Domain Name: *([^\s]+)/ のようなものが渡されます。

const searchPropertyValue = (lines: string[], key: string, regex: RegExp): string|null => {
    if (regex.multiline) {
        const matches = lines.join('\n').match(regex);
        if ( ! matches) {
            return null;
        }
        let val = matches[1];
        if (key.endsWith('Date')) {
            if (val) {
                const d = dayjs(val);
                val = d.format();
            }
        }
        return val;
    }
    const values = lines.map(line => {
        const matches = line.match(regex);
        if ( ! matches) {
            return null;
        }
        let val = matches[1];
        if (key.endsWith('Date')) {
            if (val) {
                const d = dayjs(val);
                val = d.format();
            }
        }
        return val;
    }).filter(val => val);
    if (values.length === 0) {
        return null;
    }
//     console.log(values);
    return values[0];
};

既に、key名の末尾が Date で終わっていたら、日付用の処理をする、などコンテキストを知らない人には複雑に感じられる条件分岐も存在しています。 日付の値を取得する場合に、dayjs を使ってパースしているんですが、場合によってパースの際にformatを指定する必要が出てきました。

更に条件分岐を追加したりしても対応可能だけど、そろそろごちゃごちゃしてきているなと思ってました。

概念を抽出するリファクタリング

日付の値を取得する時と文字列の値を取得する場合があって、日付の場合には dayjsを使って日付を作って形式を揃える。場合によってはformatを指定する場合がある、という状況です。

概念としては、値を見付ける処理という概念があって、その派生として文字列を見付ける処理と日付を見付ける処理がある、ということを認識できる。

なので、それらを型として表したのが以下の図です。

それをコードで表すと、以下のような形になります。

interface ValueFinder {
    regex: RegExp;
    find(lines: string[]): string|null;
}
class StringValueFinder implements ValueFinder {
    regex: RegExp;
 
    constructor(regex: RegExp) {
        this.regex = regex;
    }
    find(lines: string[]): string|null {
        if (this.regex.multiline) {
            const matches = lines.join('\n').match(this.regex);
            if ( ! matches) {
                return null;
            }
            let val = matches[1];
            return val;
        }
        const values = lines.map(line => {
            const matches = line.match(this.regex);
            if ( ! matches) {
                return null;
            }
            let val = matches[1];
            return val;
        }).filter(val => val);
        if (values.length === 0) {
            return null;
        }
        return values[0];
    }
}
class DateValueFinder implements ValueFinder {
    regex: RegExp;
    format: string;
 
    constructor(regex: RegExp, format: string) {
        this.regex = regex;
        this.format = format;
    }
    find(lines: string[]): string|null {
        const values = lines.map(line => {
            const matches = line.match(this.regex);
            if ( ! matches) {
                return null;
            }
            let val = matches[1];
            if (val) {
                const d = this.format.length > 0 ? dayjs(val, this.format) : dayjs(val);
                val = d.format();
            }
            return val;
        }).filter(val => val);
        if (values.length === 0) {
            return null;
        }
        return values[0];
    }
}
  • 値を見付ける処理という概念を型として表すことで、利用する側からは、findメソッドに、whoisの結果の生データをlinesとして渡せば、見付けた値を返してくれるだけという認識で使えるようになりました。
  • 文字列を探す場合なのか日付を探す場合なのかは、StringValueFinderクラスとDateValueFinderクラスのfindメソッドに閉じ込めることができたので分岐も不要になりました。
  • StringValueFinderクラスとDateValueFinderクラスは、それぞれの細分化された責務を負うようになったので、個々のパーツの循環的複雑度は下がりました。
  • コード行数という意味では、増えました。

概念を型として抽出するリファクタリングの例でした。

Leave a Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.