8
3

Go言語 暗黙的なinterfaceのメリット

Last updated at Posted at 2024-04-03

Go言語の何がいいかって、色々な部分で実践的な感じがする(唐突)
その代表例として今回はGo言語の暗黙的なインターフェースの何がいいかコードを例に説明したい

まず初めにGoのインターフェースの例を示します

type Writer interface {
	Write(data string) error
}

type File struct {
	Name string
}

func (f *File) Write(data string) error {
	file, _ := os.OpenFile(f.Name, os.O_WRONLY|os.O_CREATE, 0644)
	defer file.Close()
	file.WriteString(data)
	return nil
}

これでFile構造体(クラスみたいなイメージ)はWriterインターフェースを実装していることになります
JavaやTypeScriptと違って明示的にimplementsする必要はないんですね
これの何がいいんかって話です

Javaでの実装

今回はFileクラスを例に考えていきます
まず一般的な静的型付け言語の代表としてJavaから
Fileのインターフェースを定義します

interface FileInterface {
    void write(String data) throws IOException;
    String read() throws IOException;
}

このインターフェースを実装したFileクラスを作ります

class File implements FileInterface {
    private String name;

    public File(String name) {
        this.name = name;
    }

    @Override
    public void write(String data) throws IOException {
        try (FileOutputStream fos = new FileOutputStream(this.name)) {
            fos.write(data.getBytes());
        }
    }

    @Override
    public String read() throws IOException {
        try (FileInputStream fis = new FileInputStream(this.name)) {
            byte[] buf = new byte[1024];
            int n = fis.read(buf);
            return new String(buf, 0, n);
        }
    }
}

ファイルクラスはインターフェースを実装しているので、使用者側から具体的な実装を隠蔽することができます
実際に↓で使用してみます

public class Main {
    public static void main(String[] args) {
        try {
            FileInterface file = new File("test.txt");
            
            file.write("Hello World");
            String data = file.read();
            System.out.println(data);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

FileInterfaceを実装しているのでFileクラスの実装を追わなくてもfileがString型を引数とするwriteメソッドを持っていることがわかります
ここまでは普通のクラスとインターフェースの使い方だと思いますし何の問題もありません

インターフェースの分離原則

唐突ですがここでWriteメソッドだけ使いたい人が現れます

public class Main {
    public void saveTask(String task) {
        try {
            FileInterface file = new File("store.txt");
            file.write("todo: " + task);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

これも問題なく使えるはずです
しかし、要件が変わり新しいCSVFileクラスをsaveTaskで用いることになりました

class CSVFile implements FileInterface {
    private String name;

    public CSVFile(String name) {
        this.name = name;
    }

    @Override
    public void write(String data) throws IOException {
        // CSVファイルに書き込み
    }
    
    // readメソッドは使わないから実装したくない…
}

CSVFileはsaveTaskなどの書き込み専用メソッドでしか呼ばれない予定です
なので↑のようにwriteメソッドだけ実装したいのですがFileInterfaceに依存しているため、使わないのにreadメソッドも定義しなければなりません

こんなことが起きてしまうのはそもそもFileInterfaceインターフェースの分離原則(Interface Segregation Principle, ISP)に違反しているからです
インターフェースも単一責任であるべきでした

では上記を防ぐためにFileInterfaceとFileクラスを定義しなおしましょう

interface Reader {
    String read() throws IOException;
}

interface Writer {
    void write(String data) throws IOException;
}

class File implements Reader, Writer {
    private String name;

    public File(String name) {
        this.name = name;
    }

    @Override
    public void write(String data) throws IOException {
        try (FileOutputStream fos = new FileOutputStream(this.name)) {
            fos.write(data.getBytes());
        }
    }

    @Override
    public String read() throws IOException {
        try (FileInputStream fis = new FileInputStream(this.name)) {
            byte[] buf = new byte[1024];
            int n = fis.read(buf);
            return new String(buf, 0, n);
        }
    }
}

↑のようにすることで書き込み専用のメソッドがあったとしてもreadメソッドに依存しなくすることができました

実際に使用してみます

// Writerインターフェースを実装しているクラスをDI
class WriterService {
    private Writer writer;

    public WriterService(Writer writer) {
        this.writer = writer;
    }

    public void write(String data) throws IOException {
        writer.write(data);
    }
}

public class Main {
    public static void main(String[] args) {
        File file = new File("example.txt");

        WriterService writerService = new WriterService(file);
        try {
            writerService.write("Hello, World!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

これにより、新しくFileクラスを書き込み専用のCSVFileクラスに変更することになってもCSVFile クラスはWriterインターフェースを実装するだけで済みます(readメソッドを実装しなくていい)

これで一件落着に見えますが、新たな問題があります
Fileクラスが実装するインターフェースが二つに増えてしまっています…😭

今はまだ2つなのでさほど問題に思えないかもしれませんが、もし今後Closer, Seeker, Flusher など大量に出てくると管理が大変になってきます
このようにJavaやTSなどの明示的なインターフェースだとクラスもインターフェースの情報を持つ必要があります(これが悪いことと言いたいわけではないです)

Goでの実装

ではいよいよGo言語で上記を実装してみましょう
まずはインターフェースから

type Writer interface {
	Write(data string) error
}

type Reader interface {
	Read() (string, error)
}

次にFile 構造体

type File struct {
	Name string
}

func (f *File) Write(data string) error {
	file, _ := os.OpenFile(f.Name, os.O_WRONLY|os.O_CREATE, 0644)
	defer file.Close()
	file.WriteString(data)
	return nil
}

func (f *File) Read() (string, error) {
	file, _ := os.OpenFile(f.Name, os.O_RDONLY, 0644)
	defer file.Close()
	buf := make([]byte, 1024)
	n, _ := file.Read(buf)
	return string(buf[:n]), nil
}

これでFile 構造体はWriter ,Reader インターフェースを満たすようになりました
※Go言語では構造体に紐づくメソッドはfunc (h *Hoge) someMethod() any {} のように書きます

実際に使用してみます

type WriterService struct {
	Writer Writer
}

func (w *WriterService) Write(data string) error {
	return w.Writer.Write(data)
}

type ReaderService struct {
	Reader Reader
}

func (r *ReaderService) Read() (string, error) {
	return r.Reader.Read()
}

func main() {
	file := &File{Name: "test.txt"}
	writer := &WriterService{Writer: file}
	writer.Write("Hello World")

	reader := &ReaderService{Reader: file}
	data, _ := reader.Read()
	println(data)
}

先ほどのJavaで実装したときの問題が改善されました
暗黙的にインターフェースを実装しているおかげでFile 構造体は何個インターフェースを満たしていようと気にする必要はないのです

このおかげでGoでは自由に柔軟なDIがフレームワークに頼らずに実現できます

まとめ

明示的なインターフェースにも明確さがあるし、ダックタイピングにも柔軟さがあるしとどれが良いと決めるのは難しいかもしれません
けれどGoが謳う実践的な設計はこういった部分にあって、そこがとても良いと私は感じましたのでその紹介でした
そんなことねーだろという反論や意見はどしどし募集中です
おしまい

8
3
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
8
3