現状
現在のセットアップは、インターフェース分離の原則(SOLIDのI)に違反しています。
参照
ウィキペディアによると、インターフェース分離原則(ISP)は、クライアントが使用しないメソッドに依存することを強制するべきではないと述べています。インターフェイス分離の原則は、1990年代半ばにRobert Martinによって策定されました。
言い換えると、これがインターフェースの場合:
public interface IUserBackend
{
User getUser(int uid);
User createUser(int uid);
void deleteUser(int uid);
void setPassword(int uid, string password);
}
次に、このインターフェースを実装するすべてのクラスは、インターフェースのすべてのリストされたメソッドを利用する必要があります。例外なし。
一般化された方法があると想像してください:
public void HaveUserDeleted(IUserBackend backendService, User user)
{
backendService.deleteUser(user.Uid);
}
実装クラスの一部のみが実際にユーザーを削除できるように実際に作成した場合、このメソッドは時々顔を爆破します(または何もしません)。それは良い設計ではありません。
提案されたソリューション
IUserInterfaceに、アクションのビットごとのORと要求されたアクションのビットごとのORの結果である整数を返すimplementActionsメソッドがあるソリューションを見てきました。
あなたが本質的にやりたいことは:
public void HaveUserDeleted(IUserBackend backendService, User user)
{
if(backendService.canDeleteUser())
backendService.deleteUser(user.Uid);
}
特定のクラスがユーザーを削除できるかどうかを正確に判断する方法は無視しています。ブール値かビットフラグかどうかは関係ありません。それはすべて、バイナリの答えに要約されます。ユーザーを削除できますか、yesまたはnoですか?
それで問題は解決しますよね?まあ、技術的にはそうです。しかし、今、あなたはリスコフ代替原理(SOLIDのL)に違反しています。
かなり複雑なウィキペディアの説明を控えて、StackOverflowに適切な例を見つけました。「悪い」例に注意してください。
void MakeDuckSwim(IDuck duck)
{
if (duck is ElectricDuck)
((ElectricDuck)duck).TurnOn();
duck.Swim();
}
ここで似ていると思います。抽象化されたオブジェクト(IDuck
、IUserBackend
)を処理することになっているメソッドですが、クラスデザインが損なわれているため、最初に特定の実装を処理 する必要があります(ElectricDuck
、IUserBackend
ユーザーを削除できないクラスではないことを確認してください)。
これは、抽象化されたアプローチを開発する目的に反します。
注:ここの例は、あなたの場合よりも簡単に修正できます。この例では、メソッド内でElectricDuck
ターン自体をオンにするだけで十分です。両方のアヒルはまだ泳ぐことができるため、機能的な結果は同じです。Swim()
同様のことをしたいかもしれません。しないでください。ユーザーを削除するふりをすることはできませんが、実際には空のメソッド本体があります。これは技術的な観点からは機能しますが、実装クラスが何かをするように求められたときに実際に何かをするかどうかを知ることを不可能にします。これは、維持できないコードの繁殖地です。
私の提案したソリューション
しかし、実装クラスがこれらのメソッドの一部のみを処理することは可能である(そして正しい)と言いました。
例のために、これらのメソッドの可能な組み合わせごとに、それを実装するクラスがあるとしましょう。それは私たちのすべての基盤をカバーしています。
ここでの解決策は、インターフェイスを分割することです。
public interface IGetUserService
{
User getUser(int uid);
}
public interface ICreateUserService
{
User createUser(int uid);
}
public interface IDeleteUserService
{
void deleteUser(int uid);
}
public interface ISetPasswordService
{
void setPassword(int uid, string password);
}
これが私の答えの最初に来るのを見たことがあることに注意してください。インターフェイスの棲み分け原理の名は、すでにこの原則は、あなたが作るように設計されていることを明らかにインターフェースを分離十分な程度に。
これにより、インターフェイスを自由に組み合わせて使用できます。
public class UserRetrievalService
: IGetUserService, ICreateUserService
{
//getUser and createUser methods implemented here
}
public class UserDeleteService
: IDeleteUserService
{
//deleteUser method implemented here
}
public class DoesEverythingService
: IGetUserService, ICreateUserService, IDeleteUserService, ISetPasswordService
{
//All methods implemented here
}
すべてのクラスは、インターフェースの契約を破ることなく、何をしたいかを決定できます。
これは、特定のクラスがユーザーを削除できるかどうかを確認する必要がないことも意味します。IDeleteUserService
インターフェースを実装するすべてのクラスは、ユーザーを削除できます= Liskov Substitution Principleの違反なし。
public void HaveUserDeleted(IDeleteUserService backendService, User user)
{
backendService.deleteUser(user.Uid); //guaranteed to work
}
誰かが実装していないオブジェクトを渡そうとするIDeleteUserService
と、プログラムはコンパイルを拒否します。これが、型の安全性が好きな理由です。
HaveUserDeleted(new DoesEverythingService()); // No problem.
HaveUserDeleted(new UserDeleteService()); // No problem.
HaveUserDeleted(new UserRetrievalService()); // COMPILE ERROR
脚注
この例を極端に取り上げて、インターフェースを可能な限り小さなチャンクに分離しました。ただし、状況が異なる場合は、より大きなチャンクで回避できます。
たとえば、ユーザーを作成できるすべてのサービスが常にユーザーを削除できる場合(およびその逆)、これらのメソッドを単一のインターフェイスの一部として保持できます。
public interface IManageUserService
{
User createUser(int uid);
void deleteUser(int uid);
}
小さいチャンクに分割する代わりに、これを行う技術的な利点はありません。ただし、必要なボイラーめっきが少ないため、開発が少し簡単になります。
IUserBackend
はdeleteUser
メソッドを含めないでください。それはIUserDeleteBackend
(またはあなたがそれを呼びたいもの)の一部であるべきです。ユーザーを削除する必要のあるコードにはの引数がありIUserDeleteBackend
、その機能を必要としないコードはIUserBackend
実装されていないメソッドで問題を起こすことはありません。