最初に
Tsansaction Script撲滅には、ドメインモデル貧血症と戦う必要がある
そのためにはサービスから振る舞いを取り除きドメインはPOJOに押し込める必要がある。
Spring Framework、JPAへの適用を考慮するとこれを難しくしてるのはオブジェクト生成時、つまり、newをしたときに依存性がうまく注入できない点にある。Springは@Configurable
を使用した実現方法があるが、AOPを使用するため、Java Agentの設定が必要であったり、Lombokと相性が悪かったりなど適用を躊躇することもあると思われる。
ここではAOPに頼らず、もっと簡易に実施する方法を紹介する。
簡易と記載した意味は、今回紹介する方法はベストだとは意味していない点にある。この方法以外に良い方法があれば、ぜひ紹介していただきたい。
考え方
Domain Model Patternではオブジェクトのデータと振る舞いは同居する。オブジェクトが持つフィールドは、そのオブジェクトのデータに加えて、インフラストラクチャー・レイヤーへアクセスするリポジトリー等のフィールドも存在する。
依存性を注入する責務を持つSpring Frameworkでは、このデータではないフィールドへの実装クラスの依存性注入を行う。たとえば、Spring MVCではControllerのフィールドに@Autowired
アノテーションを記述すると、そのフィールドに依存性が注入される。
ところが、「最初に」の箇所に記載した@Configurable
を使用しない場合は、オブジェクトをnewで生成したときには依存性が注入されない。
今回のこの記事はこの生成時の依存性注入をどうコントロールするかを記載する。
実装方法
Spring FrameworkとJPAを使用した場合にインスタンスが生成されるのは以下の2つになる。
- 明示的にインスタンスをnewする場合
- JPAにより検索でオブジェクトを取得する場合
本記事はこの2つのケースをどう対処するかを記載する。
まず、オブジェクトの依存性注入をどこでもできるメソッドを準備する。
@Component
public class ApplicationContextProvider implements ApplicationContextAware {
private static ApplicationContext context;
public static ApplicationContext getApplicationContext() {
return context;
}
public static AutowireCapableBeanFactory getAutowireCapableBeanFactory() {
return context.getAutowireCapableBeanFactory();
}
public static <T> T pupulateBean(T bean) {
getAutowireCapableBeanFactory().autowireBean(bean);
return bean;
}
@Override
public void setApplicationContext(ApplicationContext ac) throws BeansException {
context = ac;
}
}
これでApplicationContextProvider.pupulateBean(bean)
で依存性注入ができていないインスタンスに依存性を注入ができる。
ただし、これをドメイン・レイヤーに記述すると、ドメイン・レイヤーがSpringに依存することになり、適切ではない。
明示的にインスタンスをnewする場合の対処
明示的にインスタンスをnewする場合と書いたが、ここで書く方法ではドメイン・レイヤーでは直接newをしない方法を紹介する。(結局newしないというところが突っ込むどころで、簡易と書いたのはこれに対する布石であった)
クラスFoo
がサービスBarService
を使用する必要がある場合の例を示す。
public class Foo {
@Autowired
BarService barService;
}
このクラスFoo
を生成するためのインターフェイスを作成する。
public interface FooFactory {
default Foo create() {
return new Foo();
}
}
このインターフェイスFooFactory
はドメイン・レイヤーに属する。そして、ここでは直接newを実施し、オブジェクトを生成している。
一方で、このFooFactoryはインターフェイスのため、実際にオブジェクトを生成する具象クラスを作成する必要がある。
public class FooFactoryImpl implements FooFactory {
@Override
public Foo create() {
return ApplicationContextProvider.pupulateBean(FooFactory.super.create());
}
}
クラスFooFactoryImpl
はインフラストラクチャー・レイヤーに属する。ここで、ApplicationContextProvider
が登場する。ApplicationContextProvider.pupulateBean
を経由し、オブジェクトに依存性注入している。
では、実際にこれを使用する例を見てみよう。
public class BazService {
@Autowired
FooFactory fooFactory;
public void doSomething() {
Foo foo = fooFactory.create();
foo.getBarService().doAnotherThing();
}
}
サービスBazService
は、ドメイン・レイヤーに属する。メソッドdoSomething
でクラスFoo
のインスタンスを生成しているが、このメソッドcreate
は具象クラスFooFactoryImpl
のものが呼び出されるため、Springの依存性注入が行われた後のインスタンスFoo
が得られている。
JPAにより検索でオブジェクトを取得する場合の対処
JPAにはイベント・リスナーという機能があり、エンティティーに対する様々なイベントに対して、コールバックを仕掛けることができる。
public class JpaEventListener {
@PostLoad
void onPostLoad(Object o) {
ApplicationContextProvider.pupulateBean(o);
}
}
あとは、このイベント・リスナーを対象クラスにアノテーションにより付与するのみである。
@EntityListeners(value = { JpaEventListener.class })
public class Foo {
@Autowired
BarService barService;
}
最終コード
最終的なコードは以下である。
ドメイン・レイヤー
@EntityListeners(value = { JpaEventListener.class })
public class Foo {
@Autowired
BarService barService;
}
public interface FooFactory {
default Foo create() {
return new Foo();
}
}
インフラストラクチャー・レイヤー
public class FooFactoryImpl implements FooFactory {
@Override
public Foo create() {
return ApplicationContextProvider.pupulateBean(FooFactory.super.create());
}
}
public class JpaEventListener {
@PostLoad
void onPostLoad(Object o) {
ApplicationContextProvider.pupulateBean(o);
}
}
@Component
public class ApplicationContextProvider implements ApplicationContextAware {
private static ApplicationContext context;
public static ApplicationContext getApplicationContext() {
return context;
}
public static AutowireCapableBeanFactory getAutowireCapableBeanFactory() {
return context.getAutowireCapableBeanFactory();
}
public static <T> T pupulateBean(T bean) {
getAutowireCapableBeanFactory().autowireBean(bean);
return bean;
}
@Override
public void setApplicationContext(ApplicationContext ac) throws BeansException {
context = ac;
}
}
まとめ
今回は実装を通し、ドメイン・モデルの実装方法を紹介した。
最近のフレームワークは、種類も豊富だが、それぞれの設定が多く、複雑化すると問題判別が難しくなるのは、皆さん経験されていることかと思う。
なるべく実現したいことに対し、簡易な方法を実施するのは良いことのひとつと思うため、今回は簡易なドメイン・モデルの実現方法を紹介した。
※このサイトの掲載内容は私個人の見解であり、必ずしも私が所属する会社、組織、団体の立場、戦略、意見を代表するものではありません。
Hirofumi Arimoto
Java, Scala, JavaScriptや、機械学習に興味あり
実際の開発での知見を可能な限り記事にしたいと思っている