블로그에서 방문자 패턴에 대한 참조가 계속 표시되지만 인정해야합니다. 패턴에 대한 wikipedia 기사를 읽고 그 역학을 이해하지만 그것을 사용할 때 여전히 혼란 스럽습니다.
최근에 정말 사람으로 가지고 데코레이터 패턴을 그리고 지금에 대한 용도를보고있다 절대적으로 모든 곳에서 정말뿐만 아니라 직관적이 보이는 편리한 패턴을 이해할 수 있도록하고 싶습니다.
블로그에서 방문자 패턴에 대한 참조가 계속 표시되지만 인정해야합니다. 패턴에 대한 wikipedia 기사를 읽고 그 역학을 이해하지만 그것을 사용할 때 여전히 혼란 스럽습니다.
최근에 정말 사람으로 가지고 데코레이터 패턴을 그리고 지금에 대한 용도를보고있다 절대적으로 모든 곳에서 정말뿐만 아니라 직관적이 보이는 편리한 패턴을 이해할 수 있도록하고 싶습니다.
답변:
방문자 패턴에 익숙하지 않습니다. 내가 제대로했는지 보자. 동물의 계층 구조가 있다고 가정하십시오.
class Animal { };
class Dog: public Animal { };
class Cat: public Animal { };
인터페이스가 잘 구성된 복잡한 계층 구조를 제공하십시오.
이제 계층 구조에 새로운 작업을 추가하려고합니다. 즉, 각 동물이 소리를 내기를 원합니다. 계층 구조가이 간단한 한, 당신은 직선 다형성으로 그것을 할 수 있습니다 :
class Animal
{ public: virtual void makeSound() = 0; };
class Dog : public Animal
{ public: void makeSound(); };
void Dog::makeSound()
{ std::cout << "woof!\n"; }
class Cat : public Animal
{ public: void makeSound(); };
void Cat::makeSound()
{ std::cout << "meow!\n"; }
그러나 이런 방식으로 작업을 추가 할 때마다 계층 구조의 모든 단일 클래스에 대한 인터페이스를 수정해야합니다. 이제 원래 인터페이스에 만족하고 가능한 최소한의 수정을 원한다고 가정하십시오.
방문자 패턴을 사용하면 각각의 새 작업을 적절한 클래스로 이동할 수 있으며 계층의 인터페이스를 한 번만 확장해야합니다. 해보자 먼저 계층 구조의 모든 클래스에 대한 메소드가있는 추상 작업 ( GoF 의 "방문자"클래스 )을 정의합니다 .
class Operation
{
public:
virtual void hereIsADog(Dog *d) = 0;
virtual void hereIsACat(Cat *c) = 0;
};
그런 다음 새 작업을 수락하기 위해 계층을 수정합니다.
class Animal
{ public: virtual void letsDo(Operation *v) = 0; };
class Dog : public Animal
{ public: void letsDo(Operation *v); };
void Dog::letsDo(Operation *v)
{ v->hereIsADog(this); }
class Cat : public Animal
{ public: void letsDo(Operation *v); };
void Cat::letsDo(Operation *v)
{ v->hereIsACat(this); }
마지막으로 Cat이나 Dog를 수정하지 않고 실제 작업을 구현합니다 .
class Sound : public Operation
{
public:
void hereIsADog(Dog *d);
void hereIsACat(Cat *c);
};
void Sound::hereIsADog(Dog *d)
{ std::cout << "woof!\n"; }
void Sound::hereIsACat(Cat *c)
{ std::cout << "meow!\n"; }
이제 더 이상 계층 구조를 수정하지 않고 작업을 추가 할 수 있습니다. 작동 방식은 다음과 같습니다.
int main()
{
Cat c;
Sound theSound;
c.letsDo(&theSound);
}
letsDo(Operation *v)
포인터가 필요합니다.
당신의 혼란의 이유는 아마도 방문객이 치명적인 잘못된 이름이기 때문일 것입니다. 많은 (유명한 1 !) 프로그래머들이이 문제를 우연히 발견했습니다. 실제로는 기본적으로 지원하지 않는 언어로 이중 디스패치 를 구현 합니다 (대부분은 지원하지 않음).
1) 내가 가장 좋아하는 예는 Scott Eyers입니다.“Effective C ++”의 저명한 저자는 이것을 그의 가장 중요한 C ++ 아하 라고 부릅니다 ! 지금까지의 순간 .
switch
: switch
(클라이언트 측 (코드 중복)에서 의사 결정 하드 코드와 정적 유형 검사를 제공하지 않습니다 사례의 완전성 및 명확성 확인). 방문자 패턴은 유형 검사기로 확인되며 일반적으로 클라이언트 코드가 더 단순 해집니다.
virtual
현대 프로그래밍 언어에서 기능이 매우 유용한 이유 입니다. 확장 가능한 프로그램의 기본 구성 요소입니다. 내 의견으로는 c 언어 (선택한 언어에 따라 스위치 또는 패턴 일치 등)는 다음과 같습니다. 확장 할 필요가없는 코드에서 훨씬 더 깔끔하며, 9 번과 같은 복잡한 소프트웨어에서이 스타일을 보게되어 기뻤습니다. 확장 성을 제공하려는 언어는 재귀 적 단일 디스패치보다 더 나은 디스패치 패턴을 수용해야합니다. 방문객).
여기의 모든 사람은 정확하지만 "언제"를 해결하지 못한다고 생각합니다. 먼저, 디자인 패턴에서 :
방문자는 조작하는 요소의 클래스를 변경하지 않고 새 조작을 정의 할 수 있습니다.
이제 간단한 클래스 계층 구조를 생각해 봅시다. 클래스 1, 2, 3 및 4와 메소드 A, B, C 및 D가 있습니다. 스프레드 시트에서와 같이 배치하십시오. 클래스는 선이고 메소드는 열입니다.
이제 Object Oriented 디자인은 새로운 메소드보다 새로운 클래스를 확장 할 가능성이 높으므로 더 많은 라인을 추가하는 것이 더 쉽다고 가정합니다. 새 클래스를 추가하고 해당 클래스의 다른 것을 지정하고 나머지는 상속합니다.
그러나 때로는 클래스가 비교적 정적이지만 열을 추가하여 더 많은 메소드를 자주 추가해야합니다. OO 디자인의 표준 방법은 모든 클래스에 이러한 메서드를 추가하는 것이므로 비용이 많이들 수 있습니다. 방문자 패턴은 이것을 쉽게 만듭니다.
그건 그렇고, 이것은 스칼라의 패턴 일치가 해결하려는 문제입니다.
방문자 디자인 패턴은 디렉토리 트리, XML 구조 또는 문서 개요와 같은 "순환"구조 정말 잘 작동합니다.
방문자 오브젝트는 재귀 구조의 각 노드 (각 디렉토리, 각 XML 태그)를 방문합니다. 방문자 개체는 구조를 반복하지 않습니다. 대신 방문자 메소드가 구조의 각 노드에 적용됩니다.
일반적인 재귀 노드 구조는 다음과 같습니다. 디렉토리 또는 XML 태그 일 수 있습니다. [자바 사람이라면 자녀 목록을 작성하고 유지하기위한 많은 추가 방법을 상상해보십시오.]
class TreeNode( object ):
def __init__( self, name, *children ):
self.name= name
self.children= children
def visit( self, someVisitor ):
someVisitor.arrivedAt( self )
someVisitor.down()
for c in self.children:
c.visit( someVisitor )
someVisitor.up()
이 visit
메소드는 방문자 오브젝트를 구조의 각 노드에 적용합니다. 이 경우 하향식 방문자입니다. visit
상향식 또는 다른 순서를 수행 하도록 메소드 구조를 변경할 수 있습니다 .
방문자를위한 수퍼 클래스입니다. 이 visit
방법으로 사용됩니다 . 구조의 각 노드에 "도착"합니다. 때문에 visit
방법을 호출 up
하고 down
, 방문자는 깊이 추적 할 수 있습니다.
class Visitor( object ):
def __init__( self ):
self.depth= 0
def down( self ):
self.depth += 1
def up( self ):
self.depth -= 1
def arrivedAt( self, aTreeNode ):
print self.depth, aTreeNode.name
서브 클래스는 각 레벨에서 노드 수를 계산하고 노드 목록을 누적하여 멋진 경로 계층 섹션 번호를 생성 할 수 있습니다.
여기 응용 프로그램이 있습니다. 트리 구조를 만듭니다 someTree
. Visitor
,을 만듭니다 dumpNodes
.
그런 다음를 dumpNodes
트리에 적용합니다 . dumpNode
객체는 트리의 각 노드를 "방문"것입니다.
someTree= TreeNode( "Top", TreeNode("c1"), TreeNode("c2"), TreeNode("c3") )
dumpNodes= Visitor()
someTree.visit( dumpNodes )
TreeNode visit
알고리즘은 모든 TreeNode가 방문자의 arrivedAt
메소드에 대한 인수로 사용되도록합니다 .
방문자 패턴은 클라이언트가 특정 클래스 계층의 모든 클래스에 추가 메소드를 추가 할 수있게하는 방법입니다.
상당히 안정적인 클래스 계층 구조가 있지만 해당 계층 구조로 수행해야 할 사항에 대한 요구 사항이 변경 될 때 유용합니다.
전형적인 예는 컴파일러 등입니다. AST (Abstract Syntax Tree)는 프로그래밍 언어의 구조를 정확하게 정의 할 수 있지만 AST에서 수행하려는 작업은 프로젝트가 진행됨에 따라 변경됩니다 (코드 생성기, 프리티 프린터, 디버거, 복잡도 메트릭 분석).
방문자 패턴이 없으면 개발자가 새 기능을 추가 할 때마다 기본 클래스의 모든 기능에 해당 메소드를 추가해야합니다. 이것은 기본 클래스가 별도의 라이브러리에 나타나거나 별도의 팀에서 생성 할 때 특히 어렵습니다.
(방문자 패턴이 데이터 조작을 데이터에서 멀리 이동시키기 때문에 방문자 패턴이 우수 OO 사례와 충돌한다고 주장하는 것을 들었습니다. 방문자 패턴은 정상적인 OO 사례가 실패하는 상황에서 정확하게 유용합니다.)
방문자 패턴을 사용하는 데는 세 가지 이유가 있습니다.
데이터 구조가 변경 될 때 약간 다른 코드의 확산을 줄입니다.
계산을 구현하는 코드를 변경하지 않고 여러 데이터 구조에 동일한 계산을 적용합니다.
레거시 코드를 변경하지 않고 레거시 라이브러리에 정보를 추가하십시오.
내가 쓴 기사를 살펴보십시오 .
Konrad Rudolph가 이미 지적했듯이 이중 파견이 필요한 경우에 적합합니다.
다음은 이중 파견이 필요한 상황과 방문자가 어떻게 도움을 주는지를 보여주는 예입니다.
예 :
iPhone, Android, Windows Mobile의 세 가지 유형의 모바일 장치가 있다고 가정 해 보겠습니다.
이 세 장치에는 모두 Bluetooth 라디오가 설치되어 있습니다.
파란 이빨 라디오가 두 개의 개별 OEM (Intel & Broadcom)에서 나올 수 있다고 가정합니다.
예제를 논의와 관련시키기 위해 Intel 라디오에 의해 노출되는 API가 Broadcom 라디오에 의해 노출 된 API와 다르다고 가정합니다.
이것이 나의 수업 모습입니다 –
이제 모바일 장치에서 Bluetooth 켜기 작업을 소개하겠습니다.
함수 서명은 다음과 같아야합니다.
void SwitchOnBlueTooth(IMobileDevice mobileDevice, IBlueToothRadio blueToothRadio)
따라서 올바른 장치 유형에 따라 및 올바른 유형의 Bluetooth 라디오에 따라 적절한 단계 또는 알고리즘 을 호출 하여 켤 수 있습니다 .
원칙적으로 3 x 2 행렬이되어 관련 객체의 올바른 유형에 따라 올바른 작업을 벡터화하려고합니다.
두 인수의 유형에 따라 다형성 동작.
이제이 패턴에 방문자 패턴을 적용 할 수 있습니다. Wikipedia 페이지에서 영감을 얻습니다. “실제로 방문자는 클래스 자체를 수정하지 않고도 클래스 패밀리에 새로운 가상 기능을 추가 할 수 있습니다. 대신, 가상 함수의 모든 적절한 전문화를 구현하는 방문자 클래스를 작성합니다. 방문자는 인스턴스 참조를 입력으로 가져와 이중 디스패치를 통해 목표를 구현합니다.”
3x2 매트릭스로 인해 이중 디스패치가 필요합니다.
다음 링크에서 더 쉽다는 것을 알았습니다.
에서
http://www.remondo.net/visitor-pattern-example-csharp/ 나는 예를 발견 쇼 방문자 패턴의 장점은 무엇 쇼 있다는 모의 예. 여기에 다른 컨테이너 클래스가 있습니다 Pill
.
namespace DesignPatterns
{
public class BlisterPack
{
// Pairs so x2
public int TabletPairs { get; set; }
}
public class Bottle
{
// Unsigned
public uint Items { get; set; }
}
public class Jar
{
// Signed
public int Pieces { get; set; }
}
}
위에서 볼 수 있듯이, BilsterPack
Pills '쌍이 포함되어 있으므로 쌍 수에 2를 곱해야 Bottle
합니다.unit
다른 데이터 타입과 필요가 캐스팅 될 것입니다.
따라서 주요 방법에서는 다음 코드를 사용하여 알약 수를 계산할 수 있습니다.
foreach (var item in packageList)
{
if (item.GetType() == typeof (BlisterPack))
{
pillCount += ((BlisterPack) item).TabletPairs * 2;
}
else if (item.GetType() == typeof (Bottle))
{
pillCount += (int) ((Bottle) item).Items;
}
else if (item.GetType() == typeof (Jar))
{
pillCount += ((Jar) item).Pieces;
}
}
위의 코드가 위배됩니다 Single Responsibility Principle
됩니다. 즉, 새로운 유형의 컨테이너를 추가하는 경우 기본 메소드 코드를 변경해야합니다. 또한 스위치를 더 길게 만드는 것은 나쁜 습관입니다.
따라서 다음 코드를 도입하여
public class PillCountVisitor : IVisitor
{
public int Count { get; private set; }
#region IVisitor Members
public void Visit(BlisterPack blisterPack)
{
Count += blisterPack.TabletPairs * 2;
}
public void Visit(Bottle bottle)
{
Count += (int)bottle.Items;
}
public void Visit(Jar jar)
{
Count += jar.Pieces;
}
#endregion
}
의 수를 세는 책임을 Pill
호출 된 클래스 로 옮겼습니다 PillCountVisitor
(그리고 우리는 switch case 문을 제거했습니다). 즉, 새로운 유형의 환약 용기를 추가해야 할 때마다 PillCountVisitor
등급 만 변경해야합니다 . 또한 IVisitor
다른 시나리오에서 사용하기위한 인터페이스가 일반적입니다.
알약 컨테이너 클래스에 Accept 메소드를 추가하여 :
public class BlisterPack : IAcceptor
{
public int TabletPairs { get; set; }
#region IAcceptor Members
public void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
#endregion
}
우리는 방문자가 알약 컨테이너 클래스를 방문하도록 허용합니다.
결국 우리는 다음 코드를 사용하여 알약 수를 계산합니다.
var visitor = new PillCountVisitor();
foreach (IAcceptor item in packageList)
{
item.Accept(visitor);
}
즉, 모든 알약 용기는 PillCountVisitor
방문자가 알약 개수를 볼 수 있도록합니다 . 그는 약을 세는 법을 알고 있습니다.
상기 visitor.Count
알약의 값을가집니다.
http://butunclebob.com/ArticleS.UncleBob.IuseVisitor 에는 단일 책임 원칙을 따르기 위해 다형성 (답변)을 사용할 수없는 실제 시나리오가 있습니다 . 실제로 :
public class HourlyEmployee extends Employee {
public String reportQtdHoursAndPay() {
//generate the line for this hourly employee
}
}
이 reportQtdHoursAndPay
방법은보고 및 표현을위한 것이며 단일 책임 원칙을 위반합니다. 따라서 방문자 패턴을 사용하여 문제점을 극복하는 것이 좋습니다.
이중 디스패치는 이 패턴을 사용하는 이유 중 하나 일뿐 입니다 .
그러나 단일 디스패치 패러다임을 사용하는 언어로 두 번 이상의 디스패치를 구현하는 유일한 방법입니다.
패턴을 사용해야하는 이유는 다음과 같습니다.
1) 모델이 자주 변경되지 않기 때문에 매번 모델을 변경하지 않고 새로운 작업을 정의하려고합니다 .
2) 우리는 여러 응용 프로그램에서 재사용 가능한 모델 을 원 하거나 클라이언트 클래스가 자신의 클래스로 동작을 정의 할 수 있는 확장 가능한 모델 을 원하기 때문에 모델과 동작을 결합하지 않습니다 .
3) 우리는 모델의 구체적인 유형에 의존하는 공통 연산을 가지고 있지만 , 각 서브 클래스에서 로직을 구현하고 싶지 않기 때문에 여러 클래스에서 여러 장소에서 공통 로직을 분해 할 수 있습니다 .
4) 우리는 도메인 모델 설계를 사용하고 있으며 동일한 계층 구조의 모델 클래스는 다른 곳에 모을 수있는 너무 많은 고유 한 것들을 수행합니다 .
5) 우리는 이중 파견이 필요합니다 .
인터페이스 유형으로 선언 된 변수가 있으며 런타임 유형에 따라 변수를 처리 할 수 있기를 원합니다. 물론 물론 if (myObj instanceof Foo) {}
어떤 트릭 도 사용하지 않습니다.
예를 들어 특정 처리를 적용하기 위해 구체적인 유형의 인터페이스를 매개 변수로 선언하는 메서드에 이러한 변수를 전달하는 것이 좋습니다. 런타임에 선택한 호출은 수신자의 런타임 유형에만 의존하기 때문에 언어가 단일 디스패치에 의존하는 경우 이러한 방법을 즉시 사용할 수 없습니다.
Java에서 호출 할 메소드 (서명)는 컴파일시 선택되며 런타임 유형이 아니라 선언 된 매개 변수 유형에 따라 다릅니다.
방문자를 사용하는 마지막 이유는 방문자를 구현할 때 (물론 다중 디스패치를 지원하지 않는 언어의 경우) 반드시 이중 디스패치 구현을 도입해야하기 때문에 결과입니다.
각 방문자에게 방문자를 적용하기위한 요소 순회 (반복)가 패턴을 사용하는 이유는 아닙니다.
모델과 처리를 분할하기 때문에 패턴을 사용합니다.
또한 패턴을 사용하면 반복자 기능을 활용할 수 있습니다.
이 기능은 매우 강력하며 일반적인 방법과 마찬가지로 특정 방법을 사용하여 공통 유형에서 반복을 뛰어 넘습니다 accept()
.
특별한 사용 사례입니다. 그래서 한쪽에 넣겠습니다.
자바 예제
플레이어가 조각 이동을 요청할 때 처리를 정의하려는 체스 예제로 패턴의 부가 가치를 설명하겠습니다.
방문자 패턴을 사용하지 않으면 조각 서브 클래스에서 직접 조각 이동 동작을 정의 할 수 있습니다.
예를 들어 다음 Piece
과 같은 인터페이스를 가질 수 있습니다 .
public interface Piece{
boolean checkMoveValidity(Coordinates coord);
void performMove(Coordinates coord);
Piece computeIfKingCheck();
}
각 Piece 서브 클래스는 다음과 같이 구현합니다.
public class Pawn implements Piece{
@Override
public boolean checkMoveValidity(Coordinates coord) {
...
}
@Override
public void performMove(Coordinates coord) {
...
}
@Override
public Piece computeIfKingCheck() {
...
}
}
그리고 모든 Piece 서브 클래스에 대해 같은 것입니다.
이 디자인을 보여주는 다이어그램 클래스는 다음과 같습니다.
이 접근법은 세 가지 중요한 단점을 제시합니다.
- 같은 행동 performMove()
또는 computeIfKingCheck()
매우 아마 일반적인 논리를 사용합니다.
콘크리트는 어떤 예를 들어 Piece
, performMove()
마지막으로 잠재적으로 특정 위치에 현재의 조각을 설정합니다 상대의 조각을합니다.
관련 행동을 모으는 대신 여러 클래스로 나누면 어떤 방식 으로든 단일 책임 패턴이 무효화됩니다. 유지 관리가 더 어려워집니다.
– 서브 클래스가 보거나 변경할 수 checkMoveValidity()
없는 처리 Piece
.
사람이나 컴퓨터의 행동을 넘어서는 것은 점검입니다. 이 확인은 요청 된 조각 이동이 유효한지 확인하기 위해 플레이어가 요청한 각 작업에서 수행됩니다.
따라서 우리는 Piece
인터페이스 에서 그것을 제공하고 싶지 않습니다 .
– 봇 개발자에게 어려운 체스 게임에서 일반적으로 응용 프로그램은 표준 API ( Piece
인터페이스, 서브 클래스, 보드, 공통 동작 등)를 제공하고 개발자가 봇 전략을 강화할 수 있습니다.
이를 위해서는 Piece
구현시 데이터와 동작이 밀접하게 결합되지 않은 모델을 제안해야합니다 .
방문자 패턴을 사용해 봅시다!
우리는 두 종류의 구조를 가지고 있습니다 :
– 방문을 수락하는 모델 클래스 (조각)
– 방문한 방문자 (이동 작업)
다음은 패턴을 보여주는 클래스 다이어그램입니다.
상단에는 방문자가 있고 하단에는 모델 클래스가 있습니다.
PieceMovingVisitor
인터페이스 는 다음과 같습니다 (각 종류에 대해 지정된 동작 Piece
).
public interface PieceMovingVisitor {
void visitPawn(Pawn pawn);
void visitKing(King king);
void visitQueen(Queen queen);
void visitKnight(Knight knight);
void visitRook(Rook rook);
void visitBishop(Bishop bishop);
}
조각은 이제 정의됩니다 :
public interface Piece {
void accept(PieceMovingVisitor pieceVisitor);
Coordinates getCoordinates();
void setCoordinates(Coordinates coordinates);
}
주요 방법은 다음과 같습니다.
void accept(PieceMovingVisitor pieceVisitor);
첫 번째 디스패치를 제공합니다 : Piece
수신자에 기반한 호출 .
컴파일시, 메소드는 accept()
Piece 인터페이스 의 메소드에 바인드 되고 런타임시 바운딩 된 메소드가 런타임 Piece
클래스 에서 호출됩니다 .
그리고 accept()
두 번째 디스패치를 수행하는 것은 메소드 구현입니다.
실제로 객체가 Piece
방문하려는 각 하위 클래스는 인수 자체로 전달 PieceMovingVisitor
하여 PieceMovingVisitor.visit()
메서드를 호출합니다 .
이런 식으로, 컴파일러는 컴파일 시간과 함께 선언 된 매개 변수의 유형 인 구체적 유형으로 바인드됩니다.
두 번째 파견이 있습니다.
다음은이 Bishop
를 설명하는 서브 클래스입니다.
public class Bishop implements Piece {
private Coordinates coord;
public Bishop(Coordinates coord) {
super(coord);
}
@Override
public void accept(PieceMovingVisitor pieceVisitor) {
pieceVisitor.visitBishop(this);
}
@Override
public Coordinates getCoordinates() {
return coordinates;
}
@Override
public void setCoordinates(Coordinates coordinates) {
this.coordinates = coordinates;
}
}
그리고 여기에 사용 예가 있습니다.
// 1. Player requests a move for a specific piece
Piece piece = selectPiece();
Coordinates coord = selectCoordinates();
// 2. We check with MoveCheckingVisitor that the request is valid
final MoveCheckingVisitor moveCheckingVisitor = new MoveCheckingVisitor(coord);
piece.accept(moveCheckingVisitor);
// 3. If the move is valid, MovePerformingVisitor performs the move
if (moveCheckingVisitor.isValid()) {
piece.accept(new MovePerformingVisitor(coord));
}
방문자 단점
방문자 패턴은 매우 강력한 패턴이지만 사용하기 전에 고려해야 할 몇 가지 중요한 제한 사항이 있습니다.
1) 캡슐화를 줄이거 나 끊을 위험
일부 종류의 작업에서 방문자 패턴은 도메인 개체의 캡슐화를 줄이거 나 끊을 수 있습니다.
예를 들어, MovePerformingVisitor
클래스가 실제 조각의 좌표를 설정해야하므로 Piece
인터페이스는 다음과 같은 방법을 제공해야합니다.
void setCoordinates(Coordinates coordinates);
Piece
좌표 변경 의 책임 은 이제 Piece
서브 클래스 이외의 다른 클래스에 공개됩니다 . 서브 클래스
에서 방문자가 수행 한 처리를 이동 Piece
하는 것도 옵션이 아닙니다. 방문자 구현을 수락
하면 실제로 다른 문제가 발생합니다 Piece.accept()
. 방문자가 수행하는 작업을 알지 못하므로 조각 상태를 변경하는지 여부와 방법을 모릅니다.
방문자를 식별하는 방법 Piece.accept()
은 방문자 구현 에 따라 사후 처리를 수행하는 것입니다. 그것은 방문자 구현 및 조각 서브 클래스 사이에 아마 같은 트릭을 사용하는 것이 필요 게다가 높은 커플 링 만드는 것처럼 그것은 매우 좋은 생각 일 것 getClass()
, instanceof
또는 방문자 구현을 식별 어떤 마커를.
2) 모델 변경 요구 사항
Decorator
예를 들어 다른 행동 디자인 패턴과 달리 방문자 패턴은 방해가됩니다.
우리는 accept()
방문을 받아들이 는 방법을 제공하기 위해 초기 수신자 클래스를 수정해야합니다 .
우리를 위해 어떤 문제를 가지고 있지 않은 Piece
이들로 그 서브 클래스는 우리의 클래스 .
기본 제공 또는 타사 클래스에서는 상황이 그리 쉽지 않습니다. 메소드
를 추가하려면 랩핑하거나 상속 (가능한 경우)해야합니다 accept()
.
3) 간접 지정
이 패턴은 다중 간접 지정을 만듭니다.
이중 디스패치는 단일 호출 대신 두 개의 호출을 의미합니다.
call the visited (piece) -> that calls the visitor (pieceMovingVisitor)
방문자가 방문한 객체 상태를 변경함에 따라 추가 간접 지정을 가질 수 있습니다.
사이클처럼 보일 수 있습니다.
call the visited (piece) -> that calls the visitor (pieceMovingVisitor) -> that calls the visited (piece)
Cay Horstmann은 자신의 OO 디자인 및 패턴 북에서 방문자 를 적용 할 수있는 훌륭한 예를 가지고 있습니다 . 그는 문제를 요약합니다.
복합 객체는 종종 개별 요소로 구성된 복잡한 구조를 가지고 있습니다. 일부 요소에는 하위 요소가 다시있을 수 있습니다. ... 요소에 대한 작업은 하위 요소를 방문하고 해당 요소에 작업을 적용하고 결과를 결합합니다. ... 그러나 그러한 디자인에 새로운 작업을 추가하는 것은 쉽지 않습니다.
쉽지 않은 이유는 구조 클래스 자체 내에 작업이 추가 되었기 때문입니다. 예를 들어, 파일 시스템이 있다고 가정하십시오.
다음은이 구조로 구현하려는 일부 작업 (기능)입니다.
FileSystem의 각 클래스에 함수를 추가하여 오퍼레이션을 구현할 수 있습니다 (과거 사람들은 과거에이를 수행하는 방법이 매우 분명했기 때문에이를 수행했습니다). 문제는 새로운 기능 (위의 "etc."행)을 추가 할 때마다 구조 클래스에 더 많은 메소드를 추가해야한다는 것입니다. 어느 시점에서 소프트웨어에 몇 가지 작업을 추가 한 후에는 해당 클래스의 메소드가 클래스의 기능적 응집력 측면에서 더 이상 의미가 없습니다. 예를 들어, FileNode
메소드가있는calculateFileColorForFunctionABC()
파일 시스템에서 최신 시각화 기능을 구현하기 이 있습니다.
방문자 패턴 (많은 디자인 패턴과 같은)은 모든 곳에서 많은 변경을 요구하지 않고 우수한 디자인 원칙 (높은 응집력, 낮은 커플 링)을 존중하지 않고 코드를 변경할 수있는 더 좋은 방법이 있다는 것을 알고있는 개발자 의 고통과 고통 에서 비롯 되었습니다. ). 당신이 그 고통을 느낄 때까지 많은 패턴의 유용성을 이해하기 어렵다는 것이 나의 의견입니다. 고통을 설명하는 것 (우리가 추가하는 "등"기능으로 위에서 시도한 것처럼)은 설명에서 공간을 차지하고주의를 산만하게합니다. 이러한 이유로 패턴을 이해하기가 어렵습니다.
방문자는 데이터 구조 (예 :)의 기능을 FileSystemNodes
데이터 구조 자체에서 분리 할 수 있습니다. 이 패턴을 사용하면 설계가 응집력을 존중할 수 있습니다. 데이터 구조 클래스가 더 단순하고 (메소드 수가 적음) 기능도 Visitor
구현 으로 캡슐화됩니다 . 이는 이중 디스 패칭 (패턴의 복잡한 부분)을 통해 수행됩니다 accept()
. 구조 클래스의 visitX()
메소드 및 방문자 (기능) 클래스의 메소드 사용 :
이 구조를 사용하면 구조 클래스를 변경하지 않고 콘크리트 방문자로서 구조에서 작동하는 새로운 기능을 추가 할 수 있습니다.
예를 들어, PrintNameVisitor
디렉토리 목록 기능 PrintSizeVisitor
을 구현하는 a 와 크기가있는 버전을 구현하는 a 입니다. 언젠가 XML로 데이터를 생성하는 'ExportXMLVisitor'또는 JSON 등에서 데이터를 생성하는 다른 방문자가 있다고 상상할 수 있습니다. DOT와 같은 그래픽 언어를 사용하여 디렉토리 트리를 표시하여 방문자를 시각화 할 수도 있습니다. 다른 프로그램과 함께.
마지막으로 더블 디스패치 기능이있는 방문자의 복잡성은 이해, 코딩 및 디버깅이 더 어렵다는 것을 의미합니다. 요컨대, 그것은 괴짜 요소가 높으며 다시 KISS 원칙을 따릅니다. 연구원들이 수행 한 설문 조사에서 방문자는 논란의 여지가있는 패턴 인 것으로 나타났습니다 (유용성에 대한 합의는 없었습니다). 일부 실험에서는 코드를 유지 관리하기가 쉽지 않은 것으로 나타났습니다.
제 생각에 새로운 작업을 추가하는 작업량 Visitor Pattern
은 각 요소 구조를 사용 하거나 직접 수정 하여 거의 동일 합니다. 또한 새 요소 클래스를 추가하려는 경우 Cow
Operation 인터페이스가 영향을 받고 기존의 모든 요소 클래스로 전파되므로 모든 요소 클래스를 다시 컴파일해야합니다. 그래서 요점이 뭐야?
rootElement.visit (node) -> node.collapse()
. 방문자와 함께 각 노드는 모든 하위 노드에 대한 그래프 순회를 구현하므로 완료됩니다.
levelsRemaining
매개 변수로 카운터에 쉽게 전달하십시오 . 다음 단계의 어린이에게 전화하기 전에 그것을 줄이십시오. 손님의 내부 if(levelsRemaining == 0) return
.
방문자 패턴에 대한 빠른 설명. 수정이 필요한 클래스는 모두 'accept'메소드를 구현해야합니다. 클라이언트는이 accept 메소드를 호출하여 해당 클래스 계열에 대해 새로운 조치를 수행하여 기능을 확장합니다. 고객은이 하나의 accept 메소드를 사용하여 각 특정 조치마다 다른 방문자 클래스를 전달하여 광범위한 새 조치를 수행 할 수 있습니다. 방문자 클래스에는 패밀리 내의 모든 클래스에 대해 동일한 특정 조치를 수행하는 방법을 정의하는 여러 대체 된 방문 방법이 포함됩니다. 이러한 방문 메소드는 작업 할 인스턴스를 전달받습니다.
사용을 고려할 때
나는 아저씨 밥 기사를 만나고 의견을 읽을 때 까지이 패턴을 이해하지 못했습니다 . 다음 코드를 고려하십시오.
public class Employee
{
}
public class SalariedEmployee : Employee
{
}
public class HourlyEmployee : Employee
{
}
public class QtdHoursAndPayReport
{
public void PrintReport()
{
var employees = new List<Employee>
{
new SalariedEmployee(),
new HourlyEmployee()
};
foreach (Employee e in employees)
{
if (e is HourlyEmployee he)
PrintReportLine(he);
if (e is SalariedEmployee se)
PrintReportLine(se);
}
}
public void PrintReportLine(HourlyEmployee he)
{
System.Diagnostics.Debug.WriteLine("hours");
}
public void PrintReportLine(SalariedEmployee se)
{
System.Diagnostics.Debug.WriteLine("fix");
}
}
class Program
{
static void Main(string[] args)
{
new QtdHoursAndPayReport().PrintReport();
}
}
단일 책임을 확인하기 때문에보기에 좋지만 공개 / 종료 원칙을 위반 합니다. 새로운 직원 유형을 가질 때마다 유형 확인 기능이있는 경우 추가해야합니다. 그리고 당신이하지 않으면 컴파일 타임에 그것을 알 수 없습니다.
방문자 패턴을 사용하면 개방 / 폐쇄 원칙을 위반하지 않고 단일 책임을 위반하지 않기 때문에 코드를 더 깨끗하게 만들 수 있습니다. 방문을 잊어 버린 경우 컴파일되지 않습니다.
public abstract class Employee
{
public abstract void Accept(EmployeeVisitor v);
}
public class SalariedEmployee : Employee
{
public override void Accept(EmployeeVisitor v)
{
v.Visit(this);
}
}
public class HourlyEmployee:Employee
{
public override void Accept(EmployeeVisitor v)
{
v.Visit(this);
}
}
public interface EmployeeVisitor
{
void Visit(HourlyEmployee he);
void Visit(SalariedEmployee se);
}
public class QtdHoursAndPayReport : EmployeeVisitor
{
public void Visit(HourlyEmployee he)
{
System.Diagnostics.Debug.WriteLine("hourly");
// generate the line of the report.
}
public void Visit(SalariedEmployee se)
{
System.Diagnostics.Debug.WriteLine("fix");
} // do nothing
public void PrintReport()
{
var employees = new List<Employee>
{
new SalariedEmployee(),
new HourlyEmployee()
};
QtdHoursAndPayReport v = new QtdHoursAndPayReport();
foreach (var emp in employees)
{
emp.Accept(v);
}
}
}
class Program
{
public static void Main(string[] args)
{
new QtdHoursAndPayReport().PrintReport();
}
}
}
마술은 v.Visit(this)
동일하게 보이지만 방문자의 다른 과부하를 호출하기 때문에 실제로 다르다는 것입니다.
@Federico A. Ramponi의 탁월한 답변을 바탕으로합니다.
이 계층 구조가 있다고 상상해보십시오.
public interface IAnimal
{
void DoSound();
}
public class Dog : IAnimal
{
public void DoSound()
{
Console.WriteLine("Woof");
}
}
public class Cat : IAnimal
{
public void DoSound(IOperation o)
{
Console.WriteLine("Meaw");
}
}
여기에 "Walk"메소드를 추가해야하는 경우 어떻게됩니까? 그것은 전체 디자인에 고통을 줄 것입니다.
동시에 "Walk"방법을 추가하면 새로운 질문이 생깁니다. "식사"또는 "수면"은 어떻습니까? 추가하려는 모든 새로운 작업이나 작업에 대해 Animal 계층 구조에 새로운 방법을 추가해야합니까? 그것은 추악하고 가장 중요하며, Animal 인터페이스를 닫을 수 없습니다. 따라서 방문자 패턴을 사용하면 계층을 수정하지 않고도 계층에 새로운 메소드를 추가 할 수 있습니다!
따라서이 C # 예제를 확인하고 실행하십시오.
using System;
using System.Collections.Generic;
namespace VisitorPattern
{
class Program
{
static void Main(string[] args)
{
var animals = new List<IAnimal>
{
new Cat(), new Cat(), new Dog(), new Cat(),
new Dog(), new Dog(), new Cat(), new Dog()
};
foreach (var animal in animals)
{
animal.DoOperation(new Walk());
animal.DoOperation(new Sound());
}
Console.ReadLine();
}
}
public interface IOperation
{
void PerformOperation(Dog dog);
void PerformOperation(Cat cat);
}
public class Walk : IOperation
{
public void PerformOperation(Dog dog)
{
Console.WriteLine("Dog walking");
}
public void PerformOperation(Cat cat)
{
Console.WriteLine("Cat Walking");
}
}
public class Sound : IOperation
{
public void PerformOperation(Dog dog)
{
Console.WriteLine("Woof");
}
public void PerformOperation(Cat cat)
{
Console.WriteLine("Meaw");
}
}
public interface IAnimal
{
void DoOperation(IOperation o);
}
public class Dog : IAnimal
{
public void DoOperation(IOperation o)
{
o.PerformOperation(this);
}
}
public class Cat : IAnimal
{
public void DoOperation(IOperation o)
{
o.PerformOperation(this);
}
}
}
Dog
뿐만 아니라 같은 Cat
. 기본 클래스에서 만들었으므로 상속되거나 적합한 예를 선택할 수 있습니다.
방문자는 클래스 자체를 수정하지 않고도 클래스 제품군에 새로운 가상 기능을 추가 할 수 있습니다. 대신 가상 함수의 모든 적절한 전문화를 구현하는 방문자 클래스를 작성합니다.
방문자 구조 :
다음과 같은 경우 방문자 패턴을 사용하십시오.
방문자 패턴은 Object의 기존 코드를 변경하지 않고 새로운 작업을 추가 할 수있는 유연성을 제공 하지만 이 유연성에는 단점이 있습니다.
새로운 Visitable 객체가 추가 된 경우 Visitor & ConcreteVisitor 클래스의 코드를 변경해야합니다 . 이 문제를 해결하는 해결 방법이 있습니다. 리플렉션 사용. 성능에 영향을줍니다.
코드 스 니펫 :
import java.util.HashMap;
interface Visitable{
void accept(Visitor visitor);
}
interface Visitor{
void logGameStatistics(Chess chess);
void logGameStatistics(Checkers checkers);
void logGameStatistics(Ludo ludo);
}
class GameVisitor implements Visitor{
public void logGameStatistics(Chess chess){
System.out.println("Logging Chess statistics: Game Completion duration, number of moves etc..");
}
public void logGameStatistics(Checkers checkers){
System.out.println("Logging Checkers statistics: Game Completion duration, remaining coins of loser");
}
public void logGameStatistics(Ludo ludo){
System.out.println("Logging Ludo statistics: Game Completion duration, remaining coins of loser");
}
}
abstract class Game{
// Add game related attributes and methods here
public Game(){
}
public void getNextMove(){};
public void makeNextMove(){}
public abstract String getName();
}
class Chess extends Game implements Visitable{
public String getName(){
return Chess.class.getName();
}
public void accept(Visitor visitor){
visitor.logGameStatistics(this);
}
}
class Checkers extends Game implements Visitable{
public String getName(){
return Checkers.class.getName();
}
public void accept(Visitor visitor){
visitor.logGameStatistics(this);
}
}
class Ludo extends Game implements Visitable{
public String getName(){
return Ludo.class.getName();
}
public void accept(Visitor visitor){
visitor.logGameStatistics(this);
}
}
public class VisitorPattern{
public static void main(String args[]){
Visitor visitor = new GameVisitor();
Visitable games[] = { new Chess(),new Checkers(), new Ludo()};
for (Visitable v : games){
v.accept(visitor);
}
}
}
설명:
Visitable
( Element
)는 인터페이스이며이 인터페이스 메소드는 클래스 세트에 추가해야합니다.Visitor
인터페이스는 Visitable
요소에 대한 조작을 수행하는 메소드를 포함합니다 .GameVisitor
Visitor
인터페이스 ( ConcreteVisitor
) 를 구현하는 클래스 입니다.Visitable
요소 Visitor
는 관련 Visitor
인터페이스 메소드를 승인 하고 호출합니다 .Game
로 Element
및 콘크리트 게임을 좋아하는 Chess,Checkers and Ludo
로 ConcreteElements
.위의 예에서 Chess, Checkers and Ludo
세 가지 다른 게임 (및 Visitable
클래스)이 있습니다. 어느 날, 각 게임의 통계를 기록하는 시나리오에 직면했습니다. 따라서 통계 기능을 구현하기 위해 개별 클래스를 수정하지 않고도 클래스의 책임을 중앙 집중식으로 관리 할 수 있으므로 GameVisitor
각 게임의 구조를 수정하지 않고도 트릭을 수행 할 수 있습니다.
산출:
Logging Chess statistics: Game Completion duration, number of moves etc..
Logging Checkers statistics: Game Completion duration, remaining coins of loser
Logging Ludo statistics: Game Completion duration, remaining coins of loser
인용하다
소스 메이킹 기사
상세 사항은
패턴을 사용하면 동일한 클래스의 다른 객체의 동작에 영향을주지 않고 정적 또는 동적으로 개별 객체에 동작을 추가 할 수 있습니다
관련 게시물:
나는 http://python-3-patterns-idioms-test.readthedocs.io/en/latest/Visitor.html 의 설명과 예제를 정말로 좋아합니다 .
기본 클래스 계층 구조가 고정되어 있다고 가정합니다. 다른 공급 업체에서 제공 한 것일 수 있으며 해당 계층을 변경할 수 없습니다. 그러나 의도는 해당 계층 구조에 새로운 다형성 메소드를 추가하려는 것이므로 일반적으로 기본 클래스 인터페이스에 무언가를 추가해야합니다. 따라서 딜레마는 기본 클래스에 메소드를 추가해야하지만 기본 클래스를 건드릴 수 없다는 것입니다. 이 문제를 어떻게 해결합니까?
이런 종류의 문제를 해결하는 디자인 패턴을 "방문자"(디자인 패턴 책의 마지막 패턴)라고하며 마지막 섹션에 표시된 이중 디스패치 체계를 기반으로합니다.
방문자 패턴을 사용하면 방문자 유형의 별도 클래스 계층을 작성하여 기본 유형에서 수행되는 조작을 가상화하여 기본 유형의 인터페이스를 확장 할 수 있습니다. 기본 유형의 객체는 단순히 방문자를 "수락"한 다음 방문자의 동적 바인딩 멤버 함수를 호출합니다.
방법과시기를 이해했지만 그 이유를 이해하지 못했습니다. C ++과 같은 언어의 배경 지식을 가진 사람에게 도움 이되는 경우이 내용을 주의 깊게 읽으십시오 .
게으른 경우, 우리는 방문자 패턴을 사용합니다. "가상 함수는 C ++로 동적으로 전달되지만 함수 오버로딩은 정적으로 수행됩니다 . "
또는 실제로 ApolloSpacecraft 객체에 바인딩 된 SpaceShip 참조를 전달할 때 CollideWith (ApolloSpacecraft &)가 호출되도록 다른 방법을 사용하십시오.
class SpaceShip {};
class ApolloSpacecraft : public SpaceShip {};
class ExplodingAsteroid : public Asteroid {
public:
virtual void CollideWith(SpaceShip&) {
cout << "ExplodingAsteroid hit a SpaceShip" << endl;
}
virtual void CollideWith(ApolloSpacecraft&) {
cout << "ExplodingAsteroid hit an ApolloSpacecraft" << endl;
}
}
@Federico A. Ramponi 에 대한 멋진 설명에 감사드립니다 . 나는 이것을 Java 버전으로 만들었습니다 . 도움이 되길 바랍니다.
또한 @Konrad Rudolph가 지적했듯이 실제로 는 런타임 방법을 결정하기 위해 두 개의 구체적인 인스턴스를 함께 사용 하는 이중 디스패치 입니다.
따라서 실제로 조작 인터페이스가 올바르게 정의 되어 있는 한 조작 실행기에 대한 공통 인터페이스 를 작성할 필요가 없습니다 .
import static java.lang.System.out;
public class Visitor_2 {
public static void main(String...args) {
Hearen hearen = new Hearen();
FoodImpl food = new FoodImpl();
hearen.showTheHobby(food);
Katherine katherine = new Katherine();
katherine.presentHobby(food);
}
}
interface Hobby {
void insert(Hearen hearen);
void embed(Katherine katherine);
}
class Hearen {
String name = "Hearen";
void showTheHobby(Hobby hobby) {
hobby.insert(this);
}
}
class Katherine {
String name = "Katherine";
void presentHobby(Hobby hobby) {
hobby.embed(this);
}
}
class FoodImpl implements Hobby {
public void insert(Hearen hearen) {
out.println(hearen.name + " start to eat bread");
}
public void embed(Katherine katherine) {
out.println(katherine.name + " start to eat mango");
}
}
예상대로, 공통 인터페이스는 실제로이 패턴에서 필수적인 부분은 아니지만 더 명확 해집니다.
import static java.lang.System.out;
public class Visitor_2 {
public static void main(String...args) {
Hearen hearen = new Hearen();
FoodImpl food = new FoodImpl();
hearen.showHobby(food);
Katherine katherine = new Katherine();
katherine.showHobby(food);
}
}
interface Hobby {
void insert(Hearen hearen);
void insert(Katherine katherine);
}
abstract class Person {
String name;
protected Person(String n) {
this.name = n;
}
abstract void showHobby(Hobby hobby);
}
class Hearen extends Person {
public Hearen() {
super("Hearen");
}
@Override
void showHobby(Hobby hobby) {
hobby.insert(this);
}
}
class Katherine extends Person {
public Katherine() {
super("Katherine");
}
@Override
void showHobby(Hobby hobby) {
hobby.insert(this);
}
}
class FoodImpl implements Hobby {
public void insert(Hearen hearen) {
out.println(hearen.name + " start to eat bread");
}
public void insert(Katherine katherine) {
out.println(katherine.name + " start to eat mango");
}
}
당신의 질문은 알아야 할 때입니다.
먼저 방문자 패턴으로 코드를 작성하지 않습니다. 나는 표준 코드를 작성하고 필요가 발생할 때까지 기다렸다가 리팩터링합니다. 한 번에 하나씩 여러 결제 시스템을 설치했다고 가정 해 보겠습니다. 체크 아웃시 많은 if 조건 (또는 instanceOf)을 가질 수 있습니다. 예를 들면 다음과 같습니다.
//psuedo code
if(payPal)
do paypal checkout
if(stripe)
do strip stuff checkout
if(payoneer)
do payoneer checkout
이제 10 가지 지불 방법이 있다고 상상해보십시오. 따라서 방문자가 발생하는 일종의 패턴을 볼 때 방문자가 모든 것을 분리하고 나면 다음과 같은 것을 호출하게됩니다.
new PaymentCheckoutVistor(paymentType).visit()
유스 케이스를 보여주는 im의 예제 수에서 구현하는 방법을 볼 수 있습니다.