LoginSignup
13
12

More than 5 years have passed since last update.

ライブラリのリファクタリング実践

Last updated at Posted at 2016-07-30

はじめに

自作で公開していたライブラリがあったのですが、ずいぶん前に公開したライブラリなので今回リファクタリングしました。
リファクタリングをしたポイントを記事にまとめましたので、他にも良さげな書き方等あれば気軽にコメントいただけると幸いです。

今回リファクタリングしたライブラリはUI/UX系アニメーションのライブラリです。

qiitaSample.gif

SYBlinkAnimationKit
CocoaControls

参考にしたスタイルガイドなど

適切なアクセス修飾子をつける

  • Public 外部からアクセス可能
  • Internal モジュール内(ライブラリ内)のみアクセス可能
  • Private ファイル内のみアクセス可能

ライブラリは特にアクセス修飾子を意識する必要があります。
外部からアクセスできるクラス、定数、変数、関数はpublic修飾子、ライブラリのファイル間で利用するものはinternal修飾子(自動的に付与されるので基本的にinternal修飾子はつけない)。それ以外はprivate修飾子をつけるようにしています。
通常の開発ではpublic修飾子はあまり使わず、private修飾子をつけるかつけないかになってくると思いますが、意図しない変数や関数の呼び出しはバグの温床です。
ファイル(クラス)間でのインタフェースはパッと見てわかるように、基本的にはprivate修飾子をつけ、外部からのインタフェースの役割をするもののみprivate修飾子をつけないようにするべきだと思います。

継承を許容しないClassは常にfinalをつける

Before
public class SYButton: UIButton {
...
After
public final class SYButton: UIButton {
...

継承を許容させないためにclassにつけるfinalですが、継承を許容させてる意図がわかりやすいように、基本的にclassにはfinalをつけ、継承を許容させてるclassのみfinalをつけないというスタイルにしました。
今回では継承して使用することを想定としたSYTableViewCellクラス以外はfinalをつけました。
githubのスタイルガイドでは、基本的にclassfinalをつけることを推奨しています。

参考: github/swift-style-guide

条件分岐の処理

Before
switch animationType {
case .border:
    syLayer.animationType = .border
case .borderWithShadow:
    syLayer.animationType = .borderWithShadow
case .background:
    syLayer.animationType = .background
case .ripple:
    syLayer.animationType = .ripple
case .text:
    syLayer.animationType = .text
}

After
syLayer.animationType = {
            switch animationType {
            case .border:
                return .border
            case .borderWithShadow:
                return .borderWithShadow
            case .background:
                return .background
            case .ripple:
                return .ripple
            case .text:
                return .text
            }
}()

条件分岐の処理を関数型っぽく書き直しました。条件によっていろいろな処理や挙動を行うものならbeforeのままでいいと思いますが、このように関数型っぽく書いたほうが、条件によってsyLayer.animationTypeになにかを代入するということが一目見て分かりやすくなると思います。この書き方ならsyLayer.animationType =というコピペ作業もいらないですね。優秀なエンジニアはコピペ作業をしないものです。

参考: Swift リファクタリング実践 Tips1

Extensionで実装を分割する(// MARK: - を使用する)

// MARK: - Private Methods -

private extension SYButton {

    private func setup() {
    ...

今回のライブラリでは、private methodsが多くなりがちなのでextensionprivateな関数を分割しています。またdelegateなどの処理をextensionで分割すると処理を一箇所にまとめることができるのでわかりやすいかと思います。

e.g.

// MARK: - UITableViewDataSource -

extension UIViewController: UITableViewDataSource {
...

// MARK: - UITableViewDelegate -

extension UIViewController: UITableViewDelegate {

// MARK: -

関数ジャンプを使ってる人も多いと思いますが、// MARK: -を使用することで、その場所にジャンプすることができます。実装が大きくなった時には相当便利になるので処理のまとまりごとに// MARK: -を使うようにしています。

スクリーンショット 2016-07-27 11.38.42.png

参考: エウレカ流Swift Style Guide

Swift実装のstruct initializersを使う

Before
let rect = CGRectMake(0, 0, 100, 100)
let point = CGPointMake(0, 0)
let size = CGSizeMake(100, 100)

let height = CGRectGetHeight(frame)
let width = CGRectGetWidth(frame)
After
let rect = CGRect(x: 0, y: 0, width: 100, height: 100)
let size = CGSize(width: 100, height: 100)
let point = CGPoint(x: 0, y: 0)

let height = frame.height
let width = frame.width

SwiftからはCGRectCGSizeなどの構造体が定義され、便利なメソッドがいくつか追加されました。Swift3からはCGRectMakeなどのObjective-Cの実装は使えなくなるので、SwiftからはCGRectなどのstruct initializersを使うことが推奨されています。

e.g.

let frame = CGRect(x: 0, y: 0, width: 100, height: 100)
let maxX = frame.maxX
let maxY = frame.maxY

let rect = CGRect.zero
let size = CGSize.zero
let point = CGPoint.zero

参考
- CGRectMake , CGPointMake, CGSizeMake, CGRectZero, CGPointZero is unavailable in Swift
- Swift: CGRect, CGSize & CGPoint

必要な時以外は、ClearColorは使用しない

このライブラリではアニメーションする初期の色をいくつか設定しているのですが、透明色UIColor.clearColor()を使用する場合は透過処理が入るため、描画パフォーマンスは通常の色と比較して落ちます。本当に必要な時以外はむやみにUIColor.clearColor()を使用しない方がいいと思います。

定数を定義する

Before
private var animationBorderColor = UIColor(red: 54/255, green: 215/255, blue: 183/255, alpha: 1)
private var animationDuration: CFTimeInterval = 1.5
After
struct AnimationDefaultColor {
    static let border     = UIColor(red: 54/255, green: 215/255, blue: 183/255, alpha: 1)
    static let background = UIColor(red: 248/255, green: 148/255, blue: 6/255, alpha: 1)
    static let text       = UIColor(red: 214/255, green: 69/255, blue: 65/255, alpha: 1)
    static let ripple     = UIColor(red: 171/255, green: 183/255, blue: 183/255, alpha: 1)
}


private var animationBorderColor = AnimationDefaultColor.border

//必要に応じてアクセスコントロールをprivateにする
private struct AnimationConstants {
        static let borderWidth: CGFloat = 1
        static let defaultDuration: CFTimeInterval = 1.5
        static let rippleDiameterRatio: CGFloat = 0.7
        ...
}

private var animationDuration: CFTimeInterval = AnimationConstants.defaultDuration

マジックナンバーを扱う場合は後から見て意図がわかりにくかったり、同じ値を多用してる時に変更しにくいなどのデメリットがあるかと思います。
このように構造体で定数を定義することにより、名前空間を汚染しにくくまとまった定数を構造体に収納することができます。

enumextensionを使用して初期化処理を便利にする

Before
let borderColorAnimtion = CABasicAnimation(keyPath: "borderColor")

let backgroundColorAnimtion = CABasicAnimation(keyPath: "backgroundColor")
After
extension CABasicAnimation {
    convenience init(type: AnimationKeyType) {
        self.init(keyPath: type.rawValue)
    }
}

enum AnimationType: String {
        case borderColor
        case backgroundColor
        ..


//使用時
let borderColorAnimtion = CABasicAnimation(type: .borderColor)

let backgroundColorAnimtion = CABasicAnimation(type: .backgroundColor)

CABasicAnimationkeyPathStringでアニメーションを指定して使用します。
今回はAnimationTypeというenumを定義しextensionで初期化処理を追加することによって、enumのケースでアニメーションを指定するようにしました。
AnimationTypeに定義することで今回使用しているアニメーションの種類をまとめて管理できることと、補完でアクセスできるので楽にCABasicAnimationを定義することができます。
enumで管理しているので、ケースごとによる挙動もここで管理できます。extensionのアクセスレベルはinternalなので、実際にライブラリを使用する側にも影響はありません。

protocolを使用する

ケース1 共通化

private func setTextLayer() {
        guard let font = titleLabel?.font, text = currentTitle else {
            return
        }

        var attributes = [String: AnyObject]()
        attributes[NSFontAttributeName] = font

        let size  = text.sizeWithAttributes(attributes)
        let x     = ( self.frame.width - size.width ) / 2
        let y     = ( self.frame.height - size.height ) / 2
        let frame = CGRect(origin: CGPoint(x: x, y: y), size: CGSize(width: size.width, height: size.height + layer.borderWidth))

        textLayer.font            = font
        textLayer.string          = text
        textLayer.fontSize        = font.pointSize
        textLayer.foregroundColor = textColor.CGColor
        textLayer.contentsScale   = UIScreen.mainScreen().scale
        textLayer.frame           = frame
        textLayer.alignmentMode   = kCAAlignmentCenter
    }

テキストのアニメーション処理のため、TextLayerの処理が書かれたクラスがいくつかあり、似たような処理がいくつかのクラスに書かれていました。これはDryの原則にも反していますし、どこかで共通化したいですよね。
今回はこれをprotocol実装で共通化しました。

protocol TextConvertible {
    var textLayer: CATextLayer { get set }

    func configureTextLayer(text: String?, font: UIFont?, textColor: UIColor)
}

extension TextConvertible where Self: UIView {
    func configureTextLayer(text: String?, font: UIFont?, textColor: UIColor) {
        guard let text = text, font = font else { return }

        var attributes = [String: AnyObject]()
        attributes[NSFontAttributeName] = font

        let size  = text.sizeWithAttributes(attributes)
        let x     = ( self.frame.width - size.width ) / 2
        let y     = ( self.frame.height - size.height ) / 2
        let frame = CGRect(x: x, y: y, width: size.width, height: size.height + layer.borderWidth)

        textLayer.font            = font
        textLayer.string          = text
        textLayer.fontSize        = font.pointSize
        textLayer.foregroundColor = textColor.CGColor
        textLayer.contentsScale   = UIScreen.mainScreen().scale
        textLayer.frame           = frame
        textLayer.alignmentMode   = kCAAlignmentCenter
    }
}

//使用時
class SYButton: UIButton, TextConvertible {
...

func resetTextLayer() {
configureTextLayer(currentTitle, font: titleLabel?.font, textColor: textColor)
...

protocol実装にすることで複数のクラスでこの処理を共通化することができます。

ケース2 処理の分割化

今回SYLayerというクラスに、CALayerの管理とアニメーションの処理の二つが実装されていたためクラスが肥大化していました。
原則として1つのクラスに1つの役割というものがあると思いますが、ここもどうにかしたいと思っていたのでprotocolを利用して処理の分割化を図りました。

スクリーンショット 2016-07-29 17.48.09.png

Before
class SYLayer {
...

// CALayerを管理する処理
func setLayer() {
...

func resizeLayer() {
...

// アニメーションの処理
func startAnimation() {
...

func stopAnimation() {
...
After

// CALayerを管理するクラス Animator protocolに準拠
class SYLayer: Animator {
...

func setLayer() {
...

func resizeLayer() {
...


// アニメーションを管理するprotocol
protocol Animatable {
..

extension Animatable {

func startAnimation() {
..

func stopAnimation() {
...


//使用時
class SYLabel {
..

let layer = SYLayer(self.layer)
layer.startAnimation()

SYLayerクラスのアニメーションの処理をAnimatable protocolに任せることで、処理が分割されSYLayerクラスの肥大化を防ぐことができたので、構成が理解しやすくなったと思います。

終わりに

今回はリファクタリングした大きなポイントをまとめました。
今後も随時更新していこうと思います。

13
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
12