以下の文について考察する
Javaが実装と分けるためにインタフェースを使うと言っても、インタフェースを明示したのでは
使う側(クライアント)と使われる側(プロバイダ)が結合されてしまいます。
おかげでJava(及びその他の明示的インタフェースを採用している言語)では依存性の合うものを
入れ替えることがGoよりもずっと難しくなっているのです
まず、Javaの場合インタフェースを「明示的」に扱う必要がある。
プロバイダ側は以下のようにimplements句を使って実装するインタフェースを明示的にする。
// library側(プロバイダ側のJARにいる想定)
public interface EmailSender {
void send(String to, String body);
}
public class SmtpEmailSender implements EmailSender {
@Override
public void send(String to, String body) {
// SMTPで送る実装
}
}
クライアント側は以下のようにemailSenderに対して実装を注入する
public class NotificationService {
private final EmailSender emailSender;
public NotificationService(EmailSender emailSender) {
this.emailSender = emailSender;
}
public void notifyUser(String userEmail) {
emailSender.send(userEmail, "Hello!");
}
}
上記の例ではinterfaceの定義がプロバイダ側にあったが、もしクライアント側にある場合は、プロバイダ側では以下のようにimportする必要がある。
// smtpmailer パッケージ(プロバイダ側)
package smtpmailer;
import notification.EmailSender;
public class SmtpEmailSender implements EmailSender {
@Override
public void send(String to, String body) {
// 実装
}
}
そして、EmailSenderの実装を変更したい場合は以下のように別のEmailSenderを実装してDIコンテナやnew箇所を変更することができる
public class SesEmailSender implements EmailSender {
@Override
public void send(String to, String body) {
// SESで送る実装
}
}
一方で別のチームが作ったライブラリがあって、この実装を使いたい場合が発生したとする。
// 別ライブラリのコード(自分では変更できない)
public class ThirdPartyMailer {
public void sendMail(String address, String content) {
// なんかいい感じに送る
}
}
この場合、EmailSenderインタフェースのimplementsもしていないし、メソッド名sendMailも異なるので、使いたい場合はアダプタを書く必要がある
public class ThirdPartyMailerAdapter implements EmailSender {
private final ThirdPartyMailer mailer;
public ThirdPartyMailerAdapter(ThirdPartyMailer mailer) {
this.mailer = mailer;
}
@Override
public void send(String to, String body) {
mailer.sendMail(to, body);
}
}
そして、使う側は以下のようになる
ThirdPartyMailer thirdParty = new ThirdPartyMailer();
EmailSender sender = new ThirdPartyMailerAdapter(thirdParty);
NotificationService svc = new NotificationService(sender);
いずれにせよインタフェースを使う側と使う側の結合が強い。
一方でGoの場合は以下のようにクライアント側がインタフェースを持つ。
package notification
type EmailSender interface {
Send(to, body string) error
}
type Service struct {
sender EmailSender
}
func NewService(sender EmailSender) *Service {
return &Service{sender: sender}
}
func (s *Service) NotifyUser(email string) error {
return s.sender.Send(email, "Hello!")
}
プロバイダ側の実装に際してはこのインタフェースをimportする必要がなく、また、implements句もいらない
package smtpmailer
type Client struct {
// ...
}
func (c *Client) Send(to, body string) error {
// SMTPで送る
return nil
}
使う際には以下のようになる
package main
import (
"myapp/notification"
"myapp/smtpmailer"
"myapp/sesmailer"
)
func main() {
smtp := &smtpmailer.Client{}
svc1 := notification.NewService(smtp) // OK: smtpmailer.Client は EmailSender を満たす
...
}
先程のJavaの例のようにThirtPartyMailerを使いたい場合は以下のようになる
// 別の人が作ったライブラリ(自分で変更できない)
package thirdparty
type Mailer struct {
// ...
}
func (m *Mailer) SendMail(address, content string) error {
// なんか送る
return nil
}
ここで、もしメソッドの追加を自分でできるのであれば、以下のようにSendMailメソッドをSendメソッドでラップしておしまい
func (m *Mailer) Send(to, body string) error {
return m.SendMail(to, body)
}
もしメソッドの追加ができない場合はJavaのケースと同じくアダプタを書く必要がある。
package adapter
import (
"myapp/notification"
"thirdparty"
)
type MailerAdapter struct {
M *thirdparty.Mailer
}
func (a *MailerAdapter) Send(to, body string) error {
return a.M.SendMail(to, body)
}
// MailerAdapter は notification.EmailSender を満たす
var _ notification.EmailSender = (*MailerAdapter)(nil)
ただし、Javaの例と比較するとわかるように、Javaでは以下のコストが発生する
- プロバイダ側がそのインタフェースを明示的に
implementsする必要がある -
send()がサードパーティのsendMail()を呼び出すことを明示的に示す必要がある(名寄せコスト)
一方でGoではラップする際にインタフェースをimportする必要がない。もちろんimplementsもいらない。つまり、インタフェースについて知る必要がなく、自分の興味があるメソッドだけを切り出すことが比較的簡単に出来る、という違いがある