현재 상황
현재 설정이 인터페이스 분리 원리 (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);
}
작은 덩어리로 분리하는 대신이 작업을 수행하면 기술적 인 이점이 없습니다. 보일러 도금이 덜 필요하기 때문에 개발이 약간 쉬워집니다.
IUserBackend
이 포함되지 않아야deleteUser
전혀 방법. 그것은IUserDeleteBackend
(또는 당신이 그것을 부르고 싶은 것의 일부)이어야합니다 . 사용자를 삭제IUserDeleteBackend
해야하는 코드 에는의 인수가 있으며 , 해당 기능이 필요하지 않은 코드는 사용IUserBackend
하며 구현되지 않은 메소드에는 아무런 문제가 없습니다.