protocolを使って冗長的な関数のオーバーロードを減らす

Monday, February 22, 2016

タイトルが少し難しそうに聞こえますが、内容は割と簡単です。
たまたま、自分用に書いているSwiftのextensionsを整理していた時に、「あ、こうした方がいいんじゃないかな」って思ったので、それを試したまとめになります。

例えば、CGPointやCGSizeといったCGGeometryのstruct群があるのですが、デフォルトのままだと四則演算が使えないです。

// 期待するのは、CGPoint(x:50, y: 100)
let p1 = CGPoint(x:10, y: 20)
let p2 = p1 * 5 // error!!

なので、以下のようにして、四則演算をそれぞれ定義していきます。(CGPointのみ掲載します)

import Foundation
import UIKit


extension CGPoint {

   init(_ x: CGFloat, _ y: CGFloat) {
       self.init(x: x, y: y)
   }

   init(_ x: Int, _ y: Int) {
       self.init(x: x, y: y)
   }

   init(_ x: Double, _ y: Double) {
       self.init(x: x, y: y)
   }

}

func + (lhs: CGPoint, rhs: CGPoint) -> CGPoint {
   return CGPoint(lhs.x + rhs.x, lhs.y + rhs.y)
}

func += (inout lhs: CGPoint, rhs: CGPoint) {
   lhs = lhs + rhs
}

func - (lhs: CGPoint, rhs: CGPoint) -> CGPoint {
   return CGPoint(lhs.x - rhs.x, lhs.y - rhs.y)
}

func -= (inout lhs: CGPoint, rhs: CGPoint) {
   lhs = lhs - rhs
}
func * (lhs: CGPoint, rhs: CGPoint) -> CGPoint {
    return CGPoint(lhs.x * rhs.x, lhs.y * rhs.y)
}

func *= (inout lhs: CGPoint, rhs: CGPoint) {
    lhs = lhs * rhs
}

func * (point: CGPoint, scalar: CGFloat) -> CGPoint {
    return CGPoint(point.x * scalar, point.y * scalar)
}

func *= (inout point: CGPoint, scalar: CGFloat) {
    point = point * scalar
}

func * (point: CGPoint, scalar: Double) -> CGPoint {
    return point * CGFloat(scalar)
}

func *= (inout point: CGPoint, scalar: Double) {
    point = point * scalar
}

func * (point: CGPoint, scalar: Int) -> CGPoint {
    return point * CGFloat(scalar)
}

func *= (inout point: CGPoint, scalar: Int) {
    point = point * scalar
}

func / (lhs: CGPoint, rhs: CGPoint) -> CGPoint {
    return CGPoint(lhs.x / rhs.x, lhs.y / rhs.y)
}

func /= (inout lhs: CGPoint, rhs: CGPoint) {
    lhs = lhs / rhs
}

func / (point: CGPoint, scalar: CGFloat) -> CGPoint {
    return CGPoint(point.x / scalar, point.y / scalar)
}

func /= (inout point: CGPoint, scalar: CGFloat) {
    point = point / scalar
}

func / (point: CGPoint, scalar: Double) -> CGPoint {
    return point / CGFloat(scalar)
}

func /= (inout point: CGPoint, scalar: Double) {
    point = point / scalar
}

func / (point: CGPoint, scalar: Int) -> CGPoint {
    return point / CGFloat(scalar)
}

func /= (inout point: CGPoint, scalar: Int) {
    point = point / scalar
}

一部に着目してみます。

func * (point: CGPoint, scalar: CGFloat) -> CGPoint {
    return CGPoint(point.x * scalar, point.y * scalar)
}

func *= (inout point: CGPoint, scalar: CGFloat) {
    point = point * scalar
}

func * (point: CGPoint, scalar: Double) -> CGPoint {
    return point * CGFloat(scalar)
}

func *= (inout point: CGPoint, scalar: Double) {
    point = point * scalar
}

func * (point: CGPoint, scalar: Int) -> CGPoint {
    return point * CGFloat(scalar)
}

func *= (inout point: CGPoint, scalar: Int) {
    point = point * scalar
}

この処理、やってる個と同じなのに、引数の型が違うだけじゃん! って気が付きましたでしょうか。
もちろんこれは関数のオーバーロードなので書き方が間違っているわけではないですが、これだと

  • Uint8にも対応したい!

といった具合に対応したい型が増えてしまうと、それだけオーバーロードする関数の数が増えて、冗長且つメンテナンスしづらいコードになっていきます。
そんなときは、 Protocol をうまく使って対処していきます。

以下のように、 CGGeometryCalculable を定義します。

protocol CGGeometryCalculable {
    var value: CGFloat { get }
}

これにより、 CGGeometryCalculable を採用したstruct,class等では、CGFloat型の値を返すvalueを実装しないといけないようになります。
これを、適用したい型に採用していきます。

protocol CGGeometryCalculable {
    var value: CGFloat { get }
}

extension Int: CGGeometryCalculable {
    var value: CGFloat {
        return CGFloat(self)
    }
}

extension Float: CGGeometryCalculable {
    var value: CGFloat {
        return CGFloat(self)
    }
}

extension Double: CGGeometryCalculable {
    var value: CGFloat {
        return CGFloat(self)
    }
}

extension CGFloat: CGGeometryCalculable {
    var value: CGFloat {
        return self
    }
}

これで、先ほど着目した関数が、

func * (point: CGPoint, scalar: CGGeometryCalculable) -> CGPoint {
    return CGPoint(point.x * scalar.value, point.y * scalar.value)
}

func *= (inout point: CGPoint, scalar: CGGeometryCalculable) {
    point = point * scalar
}

スッキリ まとめられます! 先にprotocolを定義して、あれこれ型に適応する手間はあるものの、 ロジックの複製が避けられる ので、
メンテナンスしやすくなります。
ちなみにこのような手法は、 にも見られます。
渡すURLのパラメータが、Stringでも、NSURLでも可能にするために、 URLStringConvertible というprotocolを実装しています。

public protocol URLStringConvertible {
    var URLString: String { get }
}

extension String: URLStringConvertible {
    public var URLString: String {
        return self
    }
}

extension NSURL: URLStringConvertible {
    public var URLString: String {
        return absoluteString
    }
}

extension NSURLComponents: URLStringConvertible {
    public var URLString: String {
        return URL!.URLString
    }
}

extension NSURLRequest: URLStringConvertible {
    public var URLString: String {
        return URL!.URLString
    }
}

これにより、

public func upload(method: Method, _ URLString: URLStringConvertible, headers: [String: String]? = nil, file: NSURL) -> Request

というメソッドがあったときに、

upload(method, "http://hogehoge.com", headers: nil, file:NSURL(file://hogehoge)!)
upload(method, NSURL(string: "http://hogehoge.com")!, headers: nil, file:NSURL(file://hogehoge)!)

と、StringでもNSURLでも許容されるようになります。でも、メソッドは 1つ です。
関数のオーバーロードも魅力的で使い勝手がいいですが、 これ、うまくまとめられないかな? となったら、 protocol で解決できないかな?って考えてみるのもありかもしれません。

techSwiftTips

IBMとSwiftと私

try?のうまい使い方