オブジェクト指向プログラミングにおけるインターフェースの紹介
オブジェクト指向プログラミング(略してOOP)におけるインターフェースは、正しく適用されれば非常に便利なものです。インターフェイスは、契約(インターフェイス)に基づいた交換可能なコンポーネントを可能にし、コードにインターフェイスを満たすよう強制します。例えば、あるプロジェクトがHTTPクライアントを使用していて、ある日突然別のクライアントに切り替えた場合、メソッドの名前が変わったり、パラメータが以前のコードと違ったり、レスポンスまで違ったりするため、コードは新しいクライアントに適応する必要がある。しかしインターフェイスの場合、開発者は新しいコードを既存のコードにどのように結合させるかだけを気にすればいい。インターフェイスには定義されたパラメーターと戻り値を持つメソッドが含まれているので、インターフェイスが実装されると、コードはそれに従う必要がある。
Goと独自のオブジェクト指向プログラミング方法
この重要な質問から始めましょう: Goはオブジェクト指向プログラミング言語なのでしょうか?そうですが、実は違います。
Goはオブジェクト指向プログラミングの特徴をいくつか持っていますが、C++やJavaと比べると、伝統的なオブジェクト指向プログラミング言語ではありません。Goにはクラスがありませんが、代わりにC言語から取り入れた構造体があり、そこからオブジェクトを作成したり、レシーバー関数と呼ばれるオブジェクト指向プログラミングのクラスメソッドのような働きをする関数を割り当てることができます。インヘリエンスも存在しませんが、代わりに構造体を別の構造体の中で宣言し、その内容を継承することができます。カプセル化はパッケージ・レベルで行われ、大文字で書かれたプロパティはパブリック・プロパティとみなされ、小文字で書かれたプロパティはプライベート・プロパティとみなされ、パッケージ内部でのみアクセス可能となります。そして最後に、アブストレーションという概念はGoには存在しない。
では、Goは伝統的なオブジェクト指向プログラミング言語ではなく、クラスも存在しません。Goのインターフェイスは、整数、文字列、浮動小数点数、ブーリアン、構造体......といったデータの型にすぎず、他のオブジェクト指向プログラミング言語と同じように、Goのインターフェイスには宣言のリストがあり、インターフェイスを実装するものなら何でもそれを持つことを期待します。
type MyInterface interface {
foo(num int) nil
bar(text string) Error
}
しかし、変数、パラメータ、戻り値には空のインターフェイスを使うことができます。
func PrintValue(value interface{}) string {
return fmt.Sprintf("Type: %T, Value: %v", value, value)
}
// Type: string, Value: hello world
fmt.Println(PrintValue("hello world"))
// Type: int, Value: 42
fmt.Println(PrintValue(42))
Goは厳密な型付け言語なので、空のインターフェイスはどんなパラメーターでも渡すことができ、どんなデータ型でも返すことができるので、コードに柔軟性を与えることができます。例えば、fmt.Println()関数を例にとると、パラメータ 「any 」は空のインターフェースのエイリアスなので、従うべき定義はありません。
func Println(a ...any) (n int, err error)
一方、何らかの定義があるインターフェイスを使えば、データはそれに従うことが期待される。JavaやC++、PHPのように明示的に実装される言語に比べて、Goのインターフェイスの実装が暗黙的なのはそのためだ。
たとえば、PHPでは次のようにインターフェイスを実装します。
interface Figure {
public function calculateArea();
}
class Rectangle implements Figure {
private $width, $height;
public function __construct($with, $height) {
$this->with = $with;
$this->height = $height;
}
}
$rectangle = new Rectangle(5, 3);
echo $rectangle->calculateArea();
そして、Goではこのように実装されている。
type Figure interface {
CalculateArea() int
}
type Rectangle struct {
width, height int
}
func (r Rectangle) CalculateArea() int {
return r.width * r.height
}
func printArea(f Figure) {
fmt.Println("The area is:",f.CalculateArea())
}
func main() {
rect := Rectangle{width: 5, height: 3}
printArea(rect)
}
難しいが、ポリモーフィズムの概念はまだ適用されるので、他の言語でのインターフェイスとは何も変わらない。
囲碁インターフェースの例 図の値を取得する
例題で実践してみよう: インターフェイスに準拠した構造体の面積と周囲長を計算する。
まず、これらのメソッドを持つインターフェース 「FigureInterface 」を作成する: CalculareArea()とCalculatePerimeter()
type FigureInterface interface {
CalculatePerimeter() float64
CalculateArea() float64
}
次に、面積と周囲長を計算するためのプロパティを持つ 「Rectangle 」と 「Circle 」構造体を作成する。
type Rectangle struct {
width, height float64
}
type Circle struct {
radius float64
}
どちらの構造体に対しても、インターフェイスの名前通りに呼び出される面積と周囲長を計算するレシーバー関数を作成する。
func (r Rectangle) CalculatePerimeter() float64 {
return (r.width * 2) + (r.height * 2)
}
func (r Rectangle) CalculateArea() float64 {
return r.width * r.height
}
func (c Circle) CalculatePerimeter() float64 {
return (2 * 3.14) * c.radius
}
func (c Circle) CalculateArea() float64 {
return (c.radius * c.radius) * 3.14
}
次に、オブジェクトの面積と周囲長を表示するPrint()関数を作成します。パラメータを見てみよう: これはインターフェイスであり、定義があるので、引数として渡すオブジェクトがそれに準拠していることが期待される。fmt.Println()を使って、引数として渡されたオブジェクトからCalculareArea()とCalcularePerimeter()の値を表示します
func Print(f FigureInterface) {
fmt.Printf("The perimeter is: %.2f \n", f.CalculatePerimeter())
fmt.Printf("The area is: %.2f \n", f.CalculateArea())
}
最後に、main()関数で構造体からオブジェクトを作成し、それをPrint()関数で使用する。
func main() {
rect := Rectangle{4.0, 5.5}
Print(rect)
cicle := Circle{12.5}
Print(cicle)
}
次のように表示される。
The perimeter is: 19.00
The area is: 22.00
The perimeter is: 78.50
The area is: 490.62
どちらのオブジェクトもインターフェイスに準拠したメソッドを持っているので、実行に問題はない。今回は、底辺と高さの2つの属性しかないので、面積だけを計算するために三角形を作ろう。
type Triangle struct {
base, height float64
}
func (t Triangle) CalculateArea() float64 {
return (t.base * t.height) / 2
}
オブジェクトを作成し、それを Print() に渡します。
func main() {
rect := Rectangle{4.0, 5.5}
Print(rect)
cicle := Circle{12.5}
Print(cicle)
triangle := Triangle{7, 12}
Print(triangle)
}
次のように表示される。
cannot use triangle (variable of type Triangle) as FigureInterface value in argument to Print: Triangle does not implement FigureInterface (missing method CalculatePerimeter)
「Triangle」 には CalculareArea() しかないが、CalculatePerimeter() のレシーバー関数がない。まあ、とにかく関数を作って0を返すことで実装してみましょう。最善の解決策ではありませんが、この例の手ぶれには十分でしょう。
func (t Triangle) CalculatePerimeter() float64 {
return 0
}
これでコードはコンパイルされる
The perimeter is: 19.00
The area is: 22.00
The perimeter is: 78.50
The area is: 490.62
The perimeter is: 0.00
The area is: 42.00
値型としてのインターフェイス
前にも言ったように、Goのインターフェイスは整数、浮動小数点数、文字列、ブール値などと同じように値の一種です。どのような場面でインターフェイスを値として使うのが適切なのでしょうか?例えば、複数のデータ値を含む設定ファイルを解析する必要がある場合:
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
)
type Config struct {
Settings map[string]interface{} `json:"settings"`
}
func loadConfig(filePath string) (Config, error) {
var config Config
data, err := ioutil.ReadFile(filePath)
if err != nil {
return config, err
}
err = json.Unmarshal(data, &config)
return config, err
}
func getConfigValue(config Config, key string) interface{} {
return config.Settings[key]
}
func main() {
config, err := loadConfig("config.json")
if err != nil {
log.Fatalf("Error loading config: %v", err)
}
dbHost := getConfigValue(config, "db_host")
dbPort := getConfigValue(config, "db_port")
debugMode := getConfigValue(config, "debug_mode")
fmt.Printf("Database Host: %v\n", dbHost)
fmt.Printf("Database Port: %v\n", dbPort)
fmt.Printf("Debug Mode: %v\n", debugMode)
}
「Config」 構造体には、文字列キーとinterface{}値のマップであるSettingsプロパティがあります。これは、「Settings」キーを読み込むgetConfigValue()関数にも当てはまり、値は何でも良いので、interface{}型として返す必要があります。
最後に
Goは、小さく、読みやすく、パフォーマンスの高いコードを作ることを目的とするなら、優れたプログラミング言語だが、他の言語と同様、長所もあれば欠点もある。インターフェイスだけでなく、エラー処理、ポインタ、配列、スライス、マップの違いなど、一見難しそうに見えるかもしれないが、完璧なものはない。Go、Elixir、Rustのような言語の人気と需要が高まっている業界では、この素晴らしい言語を時間をかけて学ぶ価値がある。