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が謳う実践的な設計はこういった部分にあって、そこがとても良いと私は感じましたのでその紹介でした
そんなことねーだろという反論や意見はどしどし募集中です
おしまい