はじめに
皆さんオブジェクト指向言語と言われると何を思い浮かべますかね。私の場合はC#やJavaなんかが一番最初に出てくるのですが、実はクラス構文がないGo言語でもオブジェクト指向プログラミングができます。今回は、オブジェクト指向プログラミングをまったく知らなかった一年前の私が書いた「手続き型」の電卓コードを、オブジェクト指向プログラミング(OOP)に沿ってリファクタリングしてみました。
一年前に書いた電卓プログラム
一年前にプログラミングの学習という趣旨で手続き型の電卓コードを書いた記事を執筆しました。その記事は下のリンクから見れます。
【学習レポート】GoとPythonで簡単な計算機作ってみた
以下がそのGo言語で書いた電卓コードです。
当時のコード
package calculation
//calculation module
func Plus(a int, b int) int {
result := a + b
return result
}
func Minus(a int, b int) int {
result := a - b
return result
}
func Multiply(a int, b int) int {
result := a * b
return result
}
func Divide(a int, b int) int {
if b == 0 {
println("Zero division error")
return 0
}
result := a / b
return result
}
package main
import (
"fmt"
"test/internal/calculation"
)
var (
input int
a int
b int
result int
)
func main() {
fmt.Printf("Choose an operator [1:PLUS, 2:MINUS, 3:MULTIPLY, 4:DIVIDE]")
fmt.Scanln(&input)
fmt.Printf("Enter a first number: ")
fmt.Scanln(&a)
fmt.Printf("Enter a second number: ")
fmt.Scanln(&b)
switch input {
case 1:
result = calculation.Plus(a, b)
fmt.Printf("The result is %d", result)
case 2:
result = calculation.Minus(a, b)
fmt.Printf("The result is %d", result)
case 3:
result = calculation.Multiply(a, b)
fmt.Printf("The result is %d", result)
case 4:
result = calculation.Divide(a, b)
fmt.Printf("The result is %d", result)
default:
println("ERROR: Invalid operator")
}
}
詳細なファイル構造は記事から確認できます。
Go言語におけるOOP
通常のOOP言語(Java、C#)では基本的にクラスを作成してそこに変数を入れるフィールド、フィールドの値を初期化するコンストラクタ、そして関数を持つメソッドで構成されてると思います。Go言語は少し特殊でクラス構文がありません。しかし、クラス構文がないからといってOOPができないわけではないです。
Go言語は構造体プログラミング言語である
JavaやC#のような一般的なOOP言語の構造は
クラス
- フィールド
- フィールド
コンストラクタ
メソッド
メソッド
のようなイメージだと思います。
しかしGo言語ではこのクラス構文がありません。その代わりに構造体を使います。
構造体のイメージ
type User struct {
Name string //変数1
Age int //変数2
}
このように構造体とは、変数のかたまりです。
OOPにおいてクラスを作るということはよく新しい型を設計すると表現されることがあります。構造体でも同じことが言えて、上記のコードはName変数とAge変数のデータを持ったUser型を作ったようなものです。クラス構文でいうフィールドだけ持っている状態ですね。しかし、ここで疑問が浮かびませんか?「フィールドだけ持っているけどどうやってコンストラクタとメソッドを実装するのか?」と。
Go言語は関数に「レシーバー」を持たせることができる
上記の疑問の答えとして、Go言語の関数は構造体と紐づけさせるレシーバーをつけることができます。それを付けることにより実質的にその関数がその構造体の「メソッド」として機能します。
私が書いた手続き型の電卓コードの一部から抜粋すると、レシーバーを持たない関数は以下のような形になります。
func Plus(a int, b int) int {
result := a + b
return result
}
基本的な構造はfunc 関数名(引数) 戻り値といった書き方になりますね。
そこで、構造体と紐づけるレシーバーを加えると
//構造体
type calculator struct {
num1 float64
num2 float64
result float64
}
//メソッド部分の関数
func (cal *calculator) Add() float64 {
cal.result = cal.num1 + cal.num2
return cal.result
}
func レシーバー(変数名 型【構造体】) 関数名(引数) 戻り値といった形になります。
レシーバー(変数名 型【構造体】)をもっと嚙み砕いてみると、calが構造体のインスタンスで、*calculatorが構造体のポインタ型です。ここでポインタ型を使う理由としてはこのメソッドの中で自身の値を置き換えてるからです。もしここでポインタを使わない(cal calculator)と、メソッドが呼び出された際に勝手にインスタンスのコピーが作られ、そいつが呼び出されてメソッド内に渡されます。そのためコピーのデータを受け取ったcal.resultをいくら計算して書き換えても、構造体内のresult変数には一切反映されません。
ここでOOPに触れたことのある方なら上記の構造体+レシーバー持ち関数の形に見覚えがあるのではないでしょうか。そう、構造体が実質的なクラス名とフィールドで、レシーバー持ち関数がそれと紐づけられてるためメソッドとなります。
実際に構造体と関数を紐づけた書き方で書いたコード
これらの点を踏まえてリファクタリングしたコードが以下になります。
リファクタリングしたコード
package main
import (
"errors"
"fmt"
)
//構造体部分
//OOPでいうフィールド部分
type calculator struct {
num1 float64
num2 float64
result float64
}
type input struct {
input1 float64
input2 float64
}
//コンストラクタ
func NewCalculator(n1 float64, n2 float64) *calculator {
return &calculator{
num1: n1,
num2: n2,
}
}
//メイン関数
func main() {
var (
operator int
)
inp := input{}
fmt.Println("演算子を選択してください[1:+][2:-][3:*][4:/]")
fmt.Scanln(&operator)
if operator < 1 || operator > 4 {
fmt.Println("1〜4の数値を入力してください")
return
}
inp.userInput()
calc := NewCalculator(inp.input1, inp.input2)
switch operator {
case 1:
output := calc.Add()
fmt.Printf("計算結果: %f + %f = %f\n", inp.input1, inp.input2, output)
case 2:
output := calc.Subtract()
fmt.Printf("計算結果: %f - %f = %f\n", inp.input1, inp.input2, output)
case 3:
output := calc.Multiply()
fmt.Printf("計算結果: %f * %f = %f\n", inp.input1, inp.input2, output)
case 4:
output, err := calc.Division()
if err != nil {
fmt.Println("ERROR", err)
return
}
fmt.Printf("計算結果: %f / %f = %f\n", inp.input1, inp.input2, output)
}
}
//input構造体のメソッド
func (inp *input) userInput() {
fmt.Println("最初の数字を入力してください")
fmt.Scanln(&inp.input1)
fmt.Println("次の数字を入力してください")
fmt.Scanln(&inp.input2)
}
// calculator構造体のメソッドたち
func (cal *calculator) Add() float64 {
cal.result = cal.num1 + cal.num2
return cal.result
}
func (cal *calculator) Subtract() float64 {
cal.result = cal.num1 - cal.num2
return cal.result
}
func (cal *calculator) Multiply() float64 {
cal.result = cal.num1 * cal.num2
return cal.result
}
func (cal *calculator) Division() (float64, error) {
if cal.num2 == 0 {
return 0, errors.New("ゼロで割ることはできません")
} else {
cal.result = cal.num1 / cal.num2
return cal.result, nil
}
}
コンストラクタ
コンストラクタの代わりにファクトリ関数なるものを使いました。
Go言語にはJavaのようなnew関数はありません。そのため、構造体を初期化して返す関数つまりファクトリ関数を自作するのが一般的な慣習らしいです。
//コンストラクタの代わり
func NewCalculator(n1 float64, n2 float64) *calculator {
return &calculator{
num1: n1,
num2: n2,
}
}
関数名はNew + 構造体名にするのがGo言語のセオリーです。このNewCalculator関数に数値を渡すことで、初期値がセットされた calculator 構造体のポインタ(インスタンス)が生成されます。
データの流れ
一年前のコードではすべての処理をmain関数にべた書きしてましたが、今回はレシーバー持ちの関数を駆使してOOPの基本である役割の分離を意識して書いてみました。
- input構造体をインスタンス化して、呼び出すことにより、ユーザーが入力したデータを
input自身に保存します。 - そして保存したデータを
inp.input1、inpinput2変数としてNewCalculatorコンストラクタに渡して、calculator構造体にデータを格納します。 - calcインスタンスを作成し、
calc.Add()のようにメソッドを呼び出します。メソッド側は自身が持ってるnum1とnum2を使って計算し、結果を返します。
まとめ
今回のリファクタリングを通じて、分かったことをまとめます。
- Go言語にはクラス構文はないが、構造体と関数を紐づけることによりOOPのような振る舞いをさせることができる。
- Go言語にコンストラクタはないがファクトリ関数を使うことにより代用ができる。
一年前はOOPの概念が全然わからずにとりあえず動けばいいコードを書きましたが、今回はOOPに沿った綺麗なコードを書けたと思います。これによって可読性と拡張性が改善されました。Go言語のようなシンプルな機能を持った言語でもオブジェクト指向プログラミングができるので、皆さんもぜひ試してみてください!