클래스는 구현하는 메소드의 서브 세트를 사용자에게 어떻게 전달해야합니까?


12

대본

웹 애플리케이션은 IUserBackend메소드로 사용자 백엔드 인터페이스 를 정의합니다.

  • getUser (uid)
  • createUser (uid)
  • deleteUser (uid)
  • setPassword (UID, 비밀번호)
  • ...

다른 사용자 백엔드 (예 : LDAP, SQL 등)는이 인터페이스를 구현하지만 모든 백엔드가 모든 것을 수행 할 수있는 것은 아닙니다. 예를 들어, 구체적인 LDAP 서버는이 웹 애플리케이션이 사용자를 삭제하도록 허용하지 않습니다. 따라서 LdapUserBackend구현 하는 클래스는 구현 IUserBackend하지 않습니다 deleteUser(uid).

구체적 클래스는 웹 애플리케이션이 백엔드 사용자와 수행 할 수있는 작업을 웹 애플리케이션과 통신해야합니다.

알려진 해결책

요청 된 작업과 함께 비트 단위 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

기타

따라서 웹 응용 프로그램은 필요한 것으로 비트 마스크를 설정 implementedActions()하고 부울로 지원하는지 여부를 응답합니다.

의견

나에게 이러한 비트 작업은 Cage의 유물처럼 보이므로 깨끗한 코드 측면에서 이해하기 쉽지는 않습니다.

질문

클래스가 구현하는 인터페이스 메소드의 서브 세트를 전달하기위한 현대 (더 나은) 패턴은 무엇입니까? 아니면 위의 "비트 연산 방법"이 여전히 모범 사례입니까?

( 중요한 경우 : PHP, OO 언어에 대한 일반적인 솔루션을 찾고 있지만 )


5
일반적인 해결책은 인터페이스를 분할하는 것입니다. 는 IUserBackend이 포함되지 않아야 deleteUser전혀 방법. 그것은 IUserDeleteBackend(또는 당신이 그것을 부르고 싶은 것의 일부)이어야합니다 . 사용자를 삭제 IUserDeleteBackend해야하는 코드 에는의 인수가 있으며 , 해당 기능이 필요하지 않은 코드는 사용 IUserBackend하며 구현되지 않은 메소드에는 아무런 문제가 없습니다.
Bakuriu

3
중요한 디자인 고려 사항은 작업의 가용성이 런타임 환경에 따라 달라지는 지 여부입니다. 그것은가 모두 삭제를 지원하지 않는 LDAP 서버? 아니면 서버 구성의 속성이며 시스템을 다시 시작하면 변경 될 수 있습니까? LDAP 커넥터가이 상황을 자동으로 감지해야하거나 다른 기능을 가진 다른 LDAP 커넥터를 연결하도록 구성을 변경해야합니까? 이러한 것들은 어떤 솔루션이 실행 가능한지에 큰 영향을 미칩니다.
Sebastian Redl

@SebastianRedl 예, 제가 고려하지 않은 것입니다. 실제로 런타임 솔루션이 필요합니다. 아주 좋은 답변을 무효화하고 싶지 않았기 때문에 런타임에 중점을 둔 새로운 질문 을 열었습니다
problemofficer

답변:


24

광범위하게 말하면 여기에서 취할 수있는 두 가지 접근 방식이 있습니다 : 다형성을 통한 테스트 및 던지기 또는 구성.

시험 & 던지기

이것이 이미 설명한 접근법입니다. 어떤 방법을 통해 클래스의 사용자에게 다른 특정 메소드의 구현 여부를 표시합니다. 이것은 단일 방법과 비트 열거 (설명한대로) 또는 일련의 supportsDelete()기타 방법을 통해 수행 할 수 있습니다 .

그런 경우 supportsDelete()반환 false, 호출 deleteUser()이 발생할 수 있습니다 것은 NotImplementedExeption슬로우, 또는 메소드는 아무것도 없습니다.

이것은 단순하기 때문에 일부 사람들에게 인기있는 솔루션입니다. 그러나 많은 사람들은 자신이 포함하여 Liskov의 대체 원칙 (SOLID의 L)을 위반하므로 좋은 해결책이 아니라고 주장합니다.

다형성을 통한 구성

여기서 접근하는 방법은 IUserBackend너무 무딘 기기를 보는 것입니다. 클래스가 항상 해당 인터페이스의 모든 메소드를 구현할 수없는 경우 인터페이스를보다 집중된 부분으로 나눕니다. 당신이있을 그래서 : IGeneralUser IDeletableUser IRenamableUser ... 즉, 모든 방법은 모든 백엔드는 갈 구현할 수 IGeneralUser있으며 일부 수행 할 수있는 작업 각각에 대해 별도의 인터페이스를 만들 수 있습니다.

그렇게하면 LdapUserBackend구현하지 않으며 IDeletableUser(C # 구문 사용)과 같은 테스트를 사용하여 테스트합니다.

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

(인스턴스가 인터페이스를 구현하는지 여부와 그 인터페이스로 캐스트하는 방법을 결정하는 PHP의 메커니즘에 대해서는 잘 모르겠지만 해당 언어에 해당하는 것이 확실합니다)

이 방법의 장점은 코드가 SOLID 원칙을 준수 할 수 있도록 다형성을 잘 사용한다는 것입니다.

단점은 너무 쉽게 다루기 힘들 수 있다는 것입니다. 예를 들어, 모든 구체적인 백엔드의 기능이 약간 다르기 때문에 수십 개의 인터페이스를 구현해야하는 경우 이는 좋은 해결책이 아닙니다. 따라서 나는이 방법이 당신에게이 방법이 실용적 일지에 대한 판단을 사용하고 가능하다면 사용하도록 조언 할 것입니다.


4
SOLID 설계시 +1 코드를 더 깨끗하게 유지하는 다른 접근 방식으로 답변을 표시하는 것이 좋습니다.
Caleb

2
PHP에서if (backend instanceof IDelatableUser) {...}
Rad80

이미 LSP 위반에 대해 언급했습니다. 동의하지만 약간 추가하고 싶었습니다. 입력 값으로 인해 작업을 수행 할 수없는 경우 Test & Throw가 유효합니다 ( 예 : Divide(float,float)메소드 에서 제수로 0을 전달) . 입력 값은 가변적이며 예외는 가능한 실행의 작은 하위 집합에 적용됩니다. 그러나 구현 유형을 기반으로 던지면 실행할 수 없다는 것이 주어진 사실입니다. 예외는 입력 의 일부만이 아니라 가능한 모든 입력 을 포함합니다. 이는 모든 층이 항상 젖어있는 세상의 모든 젖은 층에 "습식 바닥"표시를하는 것과 같습니다.
Flater

유형을 던지지 않는 원칙에는 예외가 있습니다. C #의 경우는입니다 NotImplementedException. 이 예외는 일시적 중단, 즉 아직 개발 되지 않았지만 개발 코드를 위한 입니다. 그것은 주어진 클래스가 개발이 완료된 후에도 주어진 메소드로 아무것도 하지 않을 것이라고 결정하는 것과 다릅니다 .
Flater

답변 감사합니다. 실제로 런타임 솔루션이 필요했지만 내 질문에서 강조하지 못했습니다. 답변을 확인하고 싶지 않았기 때문에 새로운 질문 을 작성하기로 결정했습니다 .
problemofficer

5

현재 상황

현재 설정이 인터페이스 분리 원리 (SOLID의 I)를 위반합니다.

참고

Wikipedia에 따르면 인터페이스 분리 원칙 (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에 implementationActions 메소드가있는 솔루션을 보았습니다. 요청 된 조치와 비트 단위 AND 조치의 비트 OR의 결과 인 정수를 리턴합니다.

본질적으로하고 싶은 것은 :

public void HaveUserDeleted(IUserBackend backendService, User user)
{
     if(backendService.canDeleteUser())
         backendService.deleteUser(user.Uid);
}

주어진 클래스가 사용자를 삭제할 수 있는지 여부를 정확하게 결정 하는 방법을 무시하고 있습니다. 부울인지 비트 플래그인지는 중요하지 않습니다. 그것은 모두 이진 답변으로 귀결됩니다 : 예, 아니오로 사용자를 삭제할 수 있습니까?

문제가 해결 될까요? 글쎄, 기술적으로는 그렇지 않습니다. 그러나 이제 Liskov 대체 원칙 (SOLID의 L)을 위반하고 있습니다.

다소 복잡한 Wikipedia 설명을 잊어 버린 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 대체 원칙 위반 없음 .

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이며, 이는 인터페이스의 전체 목적입니다.
Greg Burghardt

답변 감사합니다. 실제로 런타임 솔루션이 필요했지만 내 질문에서 강조하지 못했습니다. 답변을 확인하고 싶지 않았기 때문에 새로운 질문 을 작성하기로 결정했습니다 .
problemofficer

@problemofficer : 이러한 경우에 대한 런타임 평가가 가장 좋은 옵션은 아니지만 실제로 그렇게하는 경우가 있습니다. 이러한 경우 호출 할 수 있지만 아무 것도 수행하지 않을 수있는 메소드를 작성하십시오 (그것을 TryDeleteUser반영하기 위해 호출하십시오 ). 또는 가능한하지만 문제가있는 상황 인 경우 고의적으로 예외가 발생합니다. CanDoThing()and DoThing()메소드 접근 방식을 사용하는 것이 효과적이지만 외부 호출자가 두 개의 호출을 사용해야하고 실패한 경우 처벌을 받아야합니다. 이는 직관적이지 않고 우아하지 않습니다.
Flater

0

더 높은 수준의 유형을 사용하려면 원하는 언어로 설정된 유형을 사용할 수 있습니다. 잘만되면 그것은 교차점과 부분 집합을 결정하기위한 약간의 문법을 제공합니다.

이것은 기본적으로 Java가 EnumSet을 사용 하여 수행하는 작업입니다 (구문 설탕을 뺀 값이지만 Java입니다).


0

.NET 세계에서 사용자 정의 속성으로 메소드와 클래스를 장식 할 수 있습니다. 이는 귀하의 사건과 관련이 없을 수 있습니다.

당신이 가진 문제가 더 높은 수준의 디자인과 관련이 있다는 것은 나에게 들립니다.

이것이 사용자 편집 페이지 또는 구성 요소와 같은 UI 기능인 경우 다른 기능이 어떻게 숨겨 집니까? 이 경우 'test and throw'는 그 목적을 위해 매우 비효율적 인 접근 방식입니다. 모든 페이지를로드하기 전에 각 함수에 대한 모의 호출을 실행하여 위젯 또는 요소를 숨기거나 다르게 표시해야하는지 여부를 결정한다고 가정합니다. 또는 팝업 경고가 표시 될 때까지 사용자가 사용할 수없는 것을 발견하지 못하기 때문에 기본적으로 사용자가 수동으로 '테스트 및 던지기'를 통해 사용 가능한 것을 발견하도록하는 웹 페이지가 있습니다.

따라서 UI의 경우 선택한 구현이 관리 할 수있는 기능을 구동하는 대신 기능 관리를 수행하는 방법을 조사하고 사용 가능한 구현을 선택하는 것이 좋습니다. 기능 종속성을 작성하기위한 프레임 워크를보고 도메인 모델에서 기능을 명시 적으로 정의 할 수 있습니다. 이것은 심지어 인증과 관련이있을 수 있습니다. 기본적으로 권한 레벨을 기반으로 기능이 사용 가능한지 여부를 결정하는 것은 기능이 실제로 구현되는지 여부를 결정하는 데까지 확장 될 수 있으며, 상위 UI '기능'은 기능 세트에 대한 명시 적 맵핑을 가질 수 있습니다.

이것이 웹 API 인 경우 시간이 지남에 따라 기능이 확장됨에 따라 여러 사용자 버전의 '사용자 관리'API 또는 '사용자'REST 자원을 지원해야하므로 전체 설계 선택이 복잡해질 수 있습니다.

요약하자면, .NET 세계에서 어떤 클래스가 무엇을 구현하는지 미리 결정하는 다양한 반영 / 속성 방법을 활용할 수 있지만, 실제 문제는 해당 정보로 수행하는 것 같습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.