クラスは、実装するメソッドのサブセットをユーザーにどのように伝える必要がありますか?


12

シナリオ

WebアプリケーションはIUserBackend、メソッドを使用してユーザーバックエンドインターフェイスを定義します

  • getUser(uid)
  • createUser(uid)
  • deleteUser(uid)
  • setPassword(uid、password)
  • ...

異なるユーザーバックエンド(LDAP、SQLなど)がこのインターフェイスを実装しますが、すべてのバックエンドがすべてを実行できるわけではありません。たとえば、具体的なLDAPサーバーでは、このWebアプリケーションはユーザーを削除できません。したがって、LdapUserBackend実装IUserBackendするクラスはを実装しませんdeleteUser(uid)

具体的なクラスは、Webアプリケーションがバックエンドのユーザーに対して許可されていることをWebアプリケーションと通信する必要があります。

既知の解決策

私は、要求されたアクションとビット単位でANDされたアクションのビット単位のORの結果である整数を返すメソッドをIUserInterface持っているソリューションを見ましたimplementedActions

function implementedActions(requestedActions) {
    return (bool)(
        ACTION_GET_USER
        | ACTION_CREATE_USER
        | ACTION_DELTE_USER
        | ACTION_SET_PASSWORD
        ) & requestedActions)
}

どこ

  • ACTION_GET_USER = 1
  • ACTION_CREATE_USER = 2
  • ACTION_DELETE_USER = 4
  • ACTION_SET_PASSWORD = 8
  • .... = 16
  • .... = 32

そのため、Webアプリケーションは、必要なものでビットマスクを設定し、implementedActions()それらをサポートするかどうかをブール値で答えます。

意見

私にとってこれらのビット操作はC時代の遺物のように見えますが、きれいなコードの観点からは必ずしも簡単に理解できるとは限りません。

質問

クラスが実装するインターフェースメソッドのサブセットを通信するための、現代の(より良い?)パターンとは何ですか?または、上記の「ビット操作方法」は依然としてベストプラクティスですか?

重要な場合:PHP。ただし、OO言語の一般的なソリューションを探しています


5
一般的な解決策は、インターフェイスを分割することです。にIUserBackenddeleteUserメソッドを含めないでください。それはIUserDeleteBackend(またはあなたがそれを呼びたいもの)の一部であるべきです。ユーザーを削除する必要のあるコードにはの引数がありIUserDeleteBackend、その機能を必要としないコードはIUserBackend実装されていないメソッドで問題を起こすことはありません。
バクリウ

3
設計上の重要な考慮事項は、アクションの可用性がランタイム環境に依存するかどうかです。削除をサポートしていないのはすべて LDAPサーバーですか?または、それはサーバーの構成のプロパティであり、システムの再起動で変更される可能性がありますか?LDAPコネクターはこの状況を自動的に検出する必要がありますか、それとも機能が異なる別のLDAPコネクターをプラグインするように構成を変更する必要がありますか?これらのことは、どのソリューションが実行可能であるかに強い影響を及ぼします。
セバスチャンレッド

@SebastianRedlはい、それは私が考慮しなかったものです。実際に実行時のソリューションが必要です。私は非常に良い答えを無効にしたくなかったので、私は開いて新しい質問ランタイムに焦点を当てて
problemofficer

回答:


24

大まかに言えば、ここで取ることができる2つのアプローチがあります。テスト&スローまたは多型による合成です。

テストと投げ

これはすでに説明したアプローチです。何らかの手段を介して、特定の他のメソッドが実装されているかどうかをクラスのユーザーに示します。これは、1つのメソッドとビット単位の列挙(説明どおり)、または一連のsupportsDelete()etcメソッドを使用して実行できます。

その後、あればsupportsDelete()戻りfalse、呼び出しdeleteUser()につながる可能性があることNotImplementedExeptionがスローされる、または方法は、単に何もしていません。

これは簡単なため、一部の人に人気のあるソリューションです。しかし、私も含めて多くの人は、それがリスコフの代用原則(SOLIDのL)に違反しているため、良い解決策ではないと主張しています。

多型による合成

ここでのアプローチはIUserBackend、楽器をあまりにも鈍くすることです。クラスがそのインターフェイスのすべてのメソッドを常に実装できない場合は、インターフェイスをより焦点の合った部分に分割します。あなたが持つかもしれないので: IGeneralUser IDeletableUser IRenamableUser ... 他の言葉では、すべてのメソッドは、すべてのバックエンドはに行く実装できることをIGeneralUser、あなたは一部だけが実行できるアクションのそれぞれに別々のインターフェイスを作成します。

その方法でLdapUserBackendは、実装せずIDeletableUser、次のようなテストを使用してテストします(C#構文を使用):

if (backend is IDeletableUser deletableUser)
{
    deletableUser.deleteUser(id);
}

(インスタンスがインターフェイスを実装するかどうかを判断するためのPHPのメカニズムと、そのインターフェイスにキャストする方法はわかりませんが、その言語には同等のものがあると確信しています)

この方法の利点は、ポリモーフィズムをうまく利用して、コードがSOLID原則に準拠できるようにすることであり、私の考えではよりエレガントです。

欠点は、それがあまりにも簡単に扱いにくくなる可能性があることです。たとえば、すべての具体的なバックエンドの機能がわずかに異なるために、数十のインターフェースを実装する必要がある場合、これは良い解決策ではありません。そのため、この機会にこのアプローチがあなたにとって実用的であるかどうかを判断し、それがあればそれを使用することをお勧めします。


4
+1デザインの考慮事項。コードクリーナーを前進させ続けるさまざまなアプローチで答えを示すことは常に素晴らしいことです!
カレブ

2
PHPでは、if (backend instanceof IDelatableUser) {...}
Rad80

あなたはすでにLSPの違反に言及しています。私は同意しますが、少し追加したかったです:Test&Throwは、入力値がアクションの実行を不可能にする場合に有効です。たとえば、Divide(float,float)メソッドの除数として0を渡す。入力値は可変であり、例外は可能な実行の小さなサブセットをカバーします。しかし、実装タイプに基づいてスローする場合、実行できないことは事実です。例外は、それらのサブセットだけでなく、可能なすべての入力を対象としています。これは、すべての床が常に濡れている世界のすべての濡れた床に「濡れた床」の標識を置くようなものです。
フラット

型をスローしないという原則には、例外があります(意図しないしゃれ)。C#の場合、それはですNotImplementedException。この例外は、一時的な停止、つまり、まだ開発されていないが開発れるコードを対象としています。これは、開発が完了した後でも、特定のクラスが特定のメソッドで何実行しないことを決定的に決定することとは異なります。
フラット

答えてくれてありがとう。私は実際にランタイムソリューションが必要でしたが、私の質問でそれを強調することに失敗しました。あなたの答えを無効にしたくなかったので、新しい質問を作成することにしました。
problemofficer

5

現状

現在のセットアップは、インターフェース分離の原則(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();
}

ここで似ていると思います。抽象化されたオブジェクト(IDuckIUserBackend)を処理することになっているメソッドですが、クラスデザインが損なわれているため、最初に特定の実装を処理 する必要があります(ElectricDuckIUserBackendユーザーを削除できないクラスではないことを確認してください)。

これは、抽象化されたアプローチを開発する目的に反します。

注:ここの例は、あなたの場合よりも簡単に修正できます。この例では、メソッド内で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);
}

小さいチャンクに分割する代わりに、これを行う技術的な利点はありません。ただし、必要なボイラーめっきが少ないため、開発が少し簡単になります。


インターフェースがサポートする動作によってインターフェースを分割するための+1。これはインターフェースの全体的な目的です。
グレッグブルクハルト

答えてくれてありがとう。私は実際にランタイムソリューションが必要でしたが、私の質問でそれを強調することに失敗しました。あなたの答えを無効にしたくなかったので、新しい質問を作成することにしました。
problemofficer

@problemofficer:これらのケースのランタイム評価はめったに最良の選択肢ではありませんが、実際にそうするケースがあります。このような場合、呼び出すことはできるが、何もしない(最終的にそれTryDeleteUserを反映するために呼び出す)メソッドを作成します。または、問題が発生する可能性がある場合に、メソッドが意図的に例外をスローするようにします。使用するCanDoThing()と、DoThing()メソッドのアプローチは動作しますが、それは2つのコールを使用するように外部の発信者が必要になる(そしてそうすることができないために処罰を受ける)エレガントとしてはあまり直感的ではないです。
18年

0

より高いレベルのタイプを使用する場合は、選択した言語のセットタイプを使用できます。うまくいけば、集合の交差とサブセットの決定を行うための構文シュガーを提供します。

これは基本的に、JavaがEnumSetで行うことです(構文の砂糖を除いて、しかしJavaです)。


0

.NETの世界では、メソッドとクラスをカスタム属性で装飾できます。これはあなたのケースに関係ないかもしれません。

しかし、あなたが抱えている問題は、より高いレベルの設計に関係していると思われます。

これがユーザー編集ページやコンポーネントなどのUI機能である場合、さまざまな機能はどのようにマスクされますか?この場合、「テストとスロー」はその目的には非常に非効率的なアプローチになります。すべてのページをロードする前に、各関数の模擬呼び出しを実行して、ウィジェットまたは要素を非表示にするか、異なる方法で表示するかを決定すると想定しています。あるいは、ユーザーがポップアップ警告が表示されるまで何かが利用できないことを発見しないため、基本的にユーザーが手動の「テストとスロー」で利用可能なものを発見するように強制するWebページがあります。

そのため、UIの場合、選択した実装にどの機能を管理できるかを駆動させるのではなく、機能管理を行う方法を調べて、使用可能な実装の選択をそれに結び付けることができます。機能の依存関係を構成するためのフレームワークを見て、ドメインモデルのエンティティとして機能を明示的に定義することができます。これは認可に結び付けられることもあります。基本的に、許可レベルに基づいて機能が利用可能かどうかの判断は、機能が実際に実装されているかどうかの判断にまで拡張でき、高レベルのUI「機能」は機能セットへの明示的なマッピングを持つことができます。

これがWeb APIである場合、機能の経時的な拡張に伴い、「Manage User」APIまたは「User」RESTリソースの複数のパブリックバージョンをサポートする必要があるため、設計全体の選択が複雑になる場合があります。

要約すると、.NETの世界では、どのクラスが何を実装するかを事前に決定するさまざまなリフレクション/属性の方法を活用できますが、いずれにしても、実際の問題はその情報をどうするかということになりそうです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.