1. Capability의 필요성 - Tictim/Capabilities-Within-TileEntity GitHub Wiki
먼저 근본으로 돌아가서, 왜 Capability가 필요한지에 대해서부터 되짚어 봅시다. 여러분은 블럭과 타일엔티티의 구조를 대충 이해했으나, 타일엔티티 간의 상호작용에 대해서는 생각을 해 본 적이 없습니다. 어떻게 하면 서로 다른 타입의 타일엔티티들 사이에 상호작용이 가능한 공통적인 시스템을 만들어낼 수 있을까요? 그냥 인터페이스를 갖다 박으면 되죠! 사용하기 쉽고, 명료하고, 아마 여러분들 머릿속에서 가장 먼저 떠오르는 아이디어였을 것입니다. 바로 이렇게요!
public class MyTileEntity extends TileEntity implements IMyAwesomeMagic {
// ...
@Override
public void doMyAwesomeMagic(){
// ...
}
// ...
}
그리고 사용할 때에는...
TileEntity te = world.getTileEntity(pos);
if(te instanceof IMyAwesomeMagic){
IMyAwesomeMagic myAwesomeMagic = (IMyAwesomeMagic)te;
myAwesomeMagic.doMyAwesomeMagic();
}
참 쉽죠?
하지만 이런 디자인은 얼마 못 가 여러분의 발목을 잡게 됩니다. 타일엔티티가 구현해야 되는 인터페이스의 수는 타일엔티티가 지원하고자 하는 기능의 갯수에 비례해 늘어나기 때문에, 곧 여러분은 다음과 같이 시작하는 2,000줄의 타일엔티티 코드가 눈 앞에 있는 것을 알아차리게 됩니다.
public class MyTileEntity extends TileEntity
implements IMyAwesomeMagic
IEnergyStorage,
IItemHandler,
IFluidHandler,
ISomeFeatureHandler,
ISomeAnotherFeatureHandler {
// 그리고... 끔찍한 메소드 덩어리.
타일엔티티마다 새로운 구현을 작성해야 하는 점도 문제를 야기합니다. 그야, 타일엔티티 코드의 80%를 이미 다른 곳에서 백만 번씩 쓰였던 코드로 채우게 되니까요. 이렇게 복사+붙여넣기한 코드는 내용이 완전히 같음에도 불구하고 재사용할 수 없게 됩니다. 게다가, 이 코드에 버그픽스 혹은 수정을 적용시켜야 한다면? 상상하기도 싫군요.
모든 문제를 해결시켜 주지는 않지만, 구조적인 하자를 약간이나마 줄일 수 있는 방법이 있습니다. Delegate의 사용입니다. 쉽게 말하자면, 모든 인터페이스의 메소드 콜을 기능이 이미 구현된 인스턴스에 맡기는 거죠.
public class MyTileEntity extends TileEntity implements IMyAwesomeMagic {
private final IMyAwesomeMagic myAwesomeMagicDelegate = <기능 구현>;
// ...
@Override
public void doMyAwesomeMagic(){
myAwesomeMagicDelegate.doMyAwesomeMagic(); // Delegate 인스턴스로 콜 넘기기
}
// ...
}
이렇게 되면 우리 타일엔티티가 구현하는 인터페이스와 메소드 갯수가 줄지는 않았어도, 적어도 코드의 재사용은 가능해졌습니다.
여기서 더 멀리 가 봅시다. 만약 이 Delegate 인스턴스를 그냥 넘겨주어 사용할 수 있게 만든다면? 예를 들어, 이런 식으로요.
public class MyTileEntity extends TileEntity implememts IMyAwesomeMagicProvider {
private final IMyAwesomeMagic myAwesomeMagic = <기능 구현>;
@Override
public IMyAwesomeMagic getMyAwesomeMagic() {
return myAwesomeMagic;
}
// ...
}
타일엔티티에 필요한 메소드는 단 한 개 뿐이군요! 이 코드는 다음과 같이 사용될 것입니다.
TileEntity te = world.getTileEntity(pos);
if(te instanceof IMyAwesomeMagicProvider){
IMyAwesomeMagic myAwesomeMagic = ((IMyAwesomeMagicProvider)te).getMyAwesomeMagic();
myAwesomeMagic.doMyAwesomeMagic();
}
따라서, 위의 끔찍한 메소드 덩어리는 다음과 같은 "조금 더 볼만한" 덩어리로 축약될 수 있습니다.
public class MyTileEntity extends TileEntity
implements IMyAwesomeMagicProvider
IEnergyStorageProvider,
IItemHandlerProvider,
IFluidHandlerProvider,
ISomeFeatureHandlerProvider,
ISomeAnotherFeatureHandlerProvider {
private final IMyAwesomeMagic myAwesomeMagic = /* ... */;
private final IEnergyStorage energyStorage = /* ... */;
private final IItemHandler itemHandler = /* ... */;
private final IFluidHandler fluidHandler = /* ... */;
private final ISomeFeatureHandler someFeature = /* ... */;
private final ISomeAnotherFeatureHandler someAnotherFeature = /* ... */;
@Override
public IMyAwesomeMagic getMyAwesomeMagic() {
return myAwesomeMagic;
}
@Override
public IEnergyStorage getEnergyStorage() {
return energyStorage;
}
@Override
public IItemHandler getItemHandler() {
return itemHandler;
}
@Override
public IFluidHandler getFluidHandler() {
return fluidHandler;
}
@Override
public ISomeFeatureHandler getSomeFeature() {
return someFeature;
}
@Override
public ISomeAnotherFeatureHandler getSomeAnotherFeature() {
return someAnotherFeature;
}
그런데... 과연 여기서 이 모든 Provider
들의 의미가 있을까요? 물론, Provider들이 없으면 delegate를 받아올 수 있는 통일된 방법이 사라집니다만, 이 모든 Provider들이 각각 다른 타입의 고유한 인스턴스를 반환한다는 점을 생각해보면, 모든 타입마다 Provider를 만들지는 않아도 됩니다. 대신 키 값을, 예를 들어 Class
를 받아서, 해당 값에 해당하는 인스턴스를 넘겨줄 수도 있습니다. 이런 식으로요.
public class MyTileEntity extends TileEntity implements IContentProvider {
private final IMyAwesomeMagic myAwesomeMagic = /* ... */;
private final IEnergyStorage energyStorage = /* ... */;
private final IItemHandler itemHandler = /* ... */;
private final IFluidHandler fluidHandler = /* ... */;
private final ISomeFeatureHandler someFeature = /* ... */;
private final ISomeAnotherFeatureHandler someAnotherFeature = /* ... */;
@Override
@Nullable // 이 타일엔티티에 존재하지 않는 기능의 요청에는 아무 것도 반환해서는 안 되기에, getContent 메소드는 null을 반환할 수 있어야 합니다.
public <T> T getContent(Class<T> classOfContent){
if(classOfContent==IMyAwesomeMagic.class) return (T)myAwesomeMagic;
else if(classOfContent==IEnergyStorage.class) return (T)energyStorage;
else if(classOfContent==IItemHandler.class) return (T)itemHandler;
else if(classOfContent==IFluidHandler.class) return (T)fluidHandler;
else if(classOfContent==ISomeFeatureHandler.class) return (T)someFeature;
else if(classOfContent==ISomeAnotherFeatureHandler.class) return (T)someAnotherFeature;
else return null;
}
}
이거 보세요! 우리는 인터페이스 단 한 개와 메소드 단 한 개로 모든 기능을 핸들링했으며, 코드는 가독성 있고 재사용과 유지보수에도 아무런 문제가 없습니다. 이러한 형태의 타일엔티티는 다음과 같이 사용되겠죠.
TileEntity te = world.getTileEntity(pos);
if(te instanceof IContentProvider){
IMyAwesomeMagic myAwesomeMagic = ((IContentProvider)te).getContent(IMyAwesomeMagic.class);
if(myAwesomeMagic!=null){
myAwesomeMagic.doMyAwesomeMagic();
}
}
하지만 슬프게도, 우리의 IContentProvider
디자인에는 치명적인 단점이 있습니다. IContentProvider
는 모든 모드가 사용 가능한 보편적인 라이브러리에 포함되어 있어야 하며, 그렇지 않을 경우에는 다른 모드들이 추가하는 기능은 이 구조를 사용할 수 없습니다.
흠, 만약 모든 모드들이 공유하는 라이브러리 중 IContentProvider
와 같은 기능을 제공하는 라이브러리가 존재한다면...
...잠깐, 우리 무슨 얘기 중이었죠? 아하, Capability
!