API 디자인에서 언제 임시 다형성을 사용하거나 피할 수 있습니까?


14

Sue는 JavaScript 라이브러리를 디자인하고 있습니다 Magician.js. 린치 핀은 Rabbit전달 된 인수 에서 벗어나는 함수입니다 .

그녀는 사용자가 토끼를 String, a Number, a Function, 아마도조차 꺼내려고 할 수도 있다는 것을 알고 있습니다 HTMLElement. 이를 염두에두고 다음과 같이 API를 디자인 할 수 있습니다 .

엄격한 인터페이스

Magician.pullRabbitOutOfString = function(str) //...
Magician.pullRabbitOutOfHTMLElement = function(htmlEl) //...

위 예제의 각 함수는 함수 이름 / 매개 변수 이름에 지정된 형식의 인수를 처리하는 방법을 알고 있습니다.

또는 다음과 같이 디자인 할 수 있습니다.

"애드혹"인터페이스

Magician.pullRabbit = function(anything) //...

pullRabbitanything인수가 예상 할 수있는 다양한 예상되는 유형과 예상치 못한 유형 을 고려해야 합니다.

Magician.pullRabbit = function(anything) {
  if (anything === undefined) {
    return new Rabbit(); // out of thin air
  } else if (isString(anything)) {
    // more
  } else if (isNumber(anything)) {
    // more
  }
  // etc.
};

전자 (엄격)는 유형 검사 나 유형 변환에 대한 오버 헤드가 거의 없거나 전혀 없으므로보다 명확하고 안전하며 성능이 더 우수합니다. 그러나 후자 (임시)는 API 소비자가 전달하기에 편리한 모든 인수와 함께 "그냥 작동"한다는 점에서 외부에서 보는 것이 더 간단하다고 생각합니다.

이 질문에 대한 답변을 위해 Sue가 라이브러리의 API를 디자인 할 때 어떤 접근 방식을 취해야하는지에 대한 특정 장단점을보고 싶습니다.


답변:


7

몇 가지 장단점

다형성의 장점 :

  • 더 작은 다형성 인터페이스는 읽기 쉽습니다. 한 가지 방법 만 기억하면됩니다.
  • 그것은 언어가 사용되는 방식 인 오리 타이핑과 함께합니다.
  • 토끼를 꺼내려는 물체가 확실하다면 모호성이 없어야합니다.
  • 마술사가 토끼를 잡아 당기는 물건의 유형을 실제로 구별 해야하는 경우 객체 유형에 대한 유형 점검이 많으면 못생긴 코드를 만드는 Java와 같은 정적 언어에서도 많은 유형 검사를 수행하는 것이 좋지 않은 것으로 간주됩니다. ?

Ad-hoc의 장점 :

  • 덜 명확합니다. Cat인스턴스 에서 문자열을 가져올 수 있습니까? 그냥 작동할까요? 그렇지 않은 경우 동작은 무엇입니까? 여기에 유형을 제한하지 않으면 문서 또는 계약을 악화시킬 수있는 테스트에서 제한해야합니다.
  • 당신은 마술사 한 곳에서 토끼를 당기는 모든 취급을 가지고 있습니다 (일부는 이것을 사기라고 생각할 수도 있습니다)
  • 최신 JS 최적화 프로그램은 단형 (한 유형에서만 작동)과 다형성 함수를 구분합니다. 그들은 단형을 최적화하는 방법을 훨씬 잘 알고 있으므로 V8과 같은 엔진 에서는 pullRabbitOutOfString버전이 훨씬 빠를 것입니다. 자세한 내용은이 비디오를 참조하십시오. 편집 : 나는 perf를 직접 썼다. 실제로 이것이 항상 그런 것은 아니라는 것이 밝혀졌다 .

일부 대안 솔루션 :

제 생각에는 이런 종류의 디자인은 처음에는 'Java-Scripty'가 아닙니다. JavaScript는 C #, Java 또는 Python과 같은 언어와 다른 관용구가있는 다른 언어입니다. 이 관용구는 언어의 약하고 강한 부분을 이해하려고 노력하는 수년간의 개발자들에게서 유래 한 것입니다.

내가 생각할 수있는 두 가지 좋은 해결책이 있습니다.

  • 객체를 높이고 객체를 "풀 러블"상태로 만들고 런타임의 인터페이스를 준수한 다음 마술사가 풀 러블 객체를 작업하게합니다.
  • 전략 패턴을 사용하여 마술사에게 다양한 유형의 객체를 처리하는 방법을 동적으로 교육합니다.

해결 방법 1 : 물체 높이기

이 문제에 대한 일반적인 해결책 중 하나는 토끼를 빼낼 수있는 능력을 가지고 물체를 '높이는'것입니다.

즉, 어떤 유형의 객체를 가져 와서 모자를 당기는 기능을 추가하십시오. 다음과 같은 것 :

function makePullable(obj){
   obj.pullOfHat = function(){
       return new Rabbit(obj.toString());
   }
}

makePullable다른 객체에 대해 이러한 기능을 만들 수 있고 makePullableString, 등을 만들 수 있습니다 . 각 유형에 대한 변환을 정의하고 있습니다. 그러나 객체를 올린 후에는 일반적인 방식으로 사용할 유형이 없습니다. JavaScript의 인터페이스는 오리 타이핑에 의해 결정됩니다 . 메소드의 방법으로 가져올 있는 pullOfHat방법 이 있다면 오리 타이핑에 의해 결정됩니다 .

그러면 마술사는 할 수 있습니다 :

Magician.pullRabbit = function(pullable) {
    var rabbit = obj.pullOfHat();
    return {rabbit:rabbit,text:"Tada, I pulled a rabbit out of "+pullable};
}

일종의 mixin 패턴을 사용하여 객체를 높이는 것이 더 많은 JS 작업과 같습니다. (이것은 문자열, 숫자, null, 정의되지 않고 부울 인 언어의 값 유형에 문제가 있지만 모두 상자를 사용할 수 있습니다)

다음은 그러한 코드의 예입니다

해결 방법 2 : 전략 패턴

StackOverflow의 JS 대화방에서이 질문에 대해 이야기 할 때 친구 현상 현상전략 패턴 의 사용을 제안했습니다 .

이를 통해 런타임에 토끼를 다양한 객체에서 꺼내는 기능을 추가하고 JavaScript 코드를 만들 수 있습니다. 마술사는 다른 유형의 물체를 모자에서 꺼내는 방법을 배울 수 있으며 그 지식을 기반으로 잡아 당깁니다.

CoffeeScript에서 다음과 같이 보일 수 있습니다.

class Magician
  constructor: ()-> # A new Magician can't pull anything
     @pullFunctions = {}

  pullRabbit: (obj) -> # Pull a rabbit, handler based on type
    func = pullFunctions[obj.constructor.name]
    if func? then func(obj) else "Don't know how to pull that out of my hat!"

  learnToPull: (obj, handler) -> # Learns to pull a rabbit out of a type
    pullFunctions[obj.constructor.name] = handler

동등한 JS 코드를 볼 수 있습니다 여기 .

이렇게하면 두 세계에서 이익을 얻을 수 있습니다. 당기는 방법은 물체 또는 마술사와 밀접하게 연결되어 있지 않으므로 매우 훌륭한 해결책이라고 생각합니다.

사용법은 다음과 같습니다.

var m = new Magician();//create a new Magician
//Teach the Magician
m.learnToPull("",function(){
   return "Pulled a rabbit out of a string";
});
m.learnToPull({},function(){
   return "Pulled a rabbit out of a Object";
});

m.pullRabbit(" Str");


2
나는 :-) ... SE의 규칙에 따라, 당신은 하나에 만족해야 할 것이다, 나는 많은 것을 배웠있는 아주 철저한 대답 +10이 싶지만
마르 얀 Venema의에게

@MarjanVenema 다른 답변도 좋습니다. 꼭 읽어보십시오. 나는 당신이 이것을 즐거워서 기쁘다. 들러서 더 많은 디자인 질문을하십시오.
Benjamin Gruenbaum가

4

문제는 JavaScript에 존재하지 않는 다형성 유형을 구현하려고한다는 것입니다. JavaScript는 일부 유형의 기능을 지원하지만 거의 보편적으로 오리 유형 언어로 처리됩니다.

최상의 API를 만들려면 두 가지를 모두 구현해야합니다. 조금 더 타이핑하지만 API 사용자에게는 장기적으로 많은 작업을 저장합니다.

pullRabbit유형을 확인하고 해당 객체 유형과 관련된 적절한 함수 (예 :)를 호출하는 중재자 메소드 여야합니다 pullRabbitOutOfHtmlElement.

그렇게하면 프로토 타이핑 사용자는을 사용할 수 pullRabbit있지만 속도가 느리면 최종적으로 유형 검사를 구현하고 더 빠른 방법으로 pullRabbitOutOfHtmlElement직접 호출 할 수 있습니다.


2

이것은 JavaScript입니다. 당신이 그것을 더 잘 얻을 때 당신은 종종 이와 같은 딜레마를 부정하는 데 도움이되는 중간 도로가 있음을 알 수 있습니다. 또한 컴파일되지 않은 런타임이 없기 때문에 누군가 사용하려고 할 때 지원되지 않는 '유형'이 무언가에 걸리거나 중단되는지 여부는 중요하지 않습니다. 잘못 사용하면 고장납니다. 파산했을 때 숨겨 지거나 반쯤 작동하게한다고해서 무언가가 파손되었다는 사실은 바뀌지 않습니다.

따라서 케이크를 먹고 먹으며 올바른 이름으로 모든 올바른 장소에 올바른 세부 사항과 함께 모든 것을 실제로, 분명하게 유지하여 유형 혼란과 불필요한 파손을 피하는 법을 배우십시오.

우선, 유형을 확인하기 전에 오리를 한 줄로 모으는 습관을들이는 것이 좋습니다. 가장 간결하고 가장 효율적인 (그러나 항상 기본 생성자가 염려되는 곳은 아니지만)해야 할 일은 프로토 타입을 먼저 쳐서 지원되는 유형이 무엇인지 신경 쓰지 않아도됩니다.

String.prototype.pullRabbit = function(){
    //do something string-relevant
}

HTMLElement.prototype.pullRabbit = function(){
    //do something HTMLElement-relevant
}

Magician.pullRabbitFrom = function(someThingy){
    return someThingy.pullRabbit();
}

참고 : 모든 것이 Object에서 상속되므로 Object 에이 작업을 수행하는 것은 잘못된 형식으로 널리 간주됩니다. 나는 개인적으로 기능도 피할 것입니다. 어떤 사람들은 나쁜 정책이 아닐 수도있는 네이티브 생성자의 프로토 타입을 건 드리는 것에 대해 불안감을 느낄 수 있지만, 자신의 객체 생성자와 함께 작업 할 때 예제가 여전히 제공 될 수 있습니다.

덜 복잡한 응용 프로그램에서 다른 라이브러리의 무언가를 클로버하지 않을 가능성이있는 특정 용도의 메소드에 대해서는이 방법에 대해 걱정하지 않지만 JavaScript가 아닌 경우 기본 메소드에서 일반적으로 지나치게 일반적으로 주장하는 것을 피하는 것이 좋습니다. 오래된 브라우저에서 최신 메소드를 정규화하지 않는 한해 야합니다.

다행스럽게도 항상 타입이나 생성자 이름을 메소드에 미리 매핑 할 수 있습니다 (생성자 속성의 toString 결과에서 구문 분석해야하는 <object> .constructor.name이없는 IE <= 8에주의하십시오). 당신은 여전히 ​​생성자 이름을 검사하는 중입니다 (유형은 객체를 비교할 때 JS에서 쓸모가 없습니다). 그러나 적어도 거대한 switch 문이나 메소드의 모든 호출에서 if / else 체인보다 더 넓을 수 있습니다. 다양한 개체.

var rabbitPullMap = {
    String: ( function pullRabbitFromString(){
        //do stuff here
    } ),
    //parens so we can assign named functions if we want for helpful debug
    //yes, I've been inconsistent. It's just a nice unrelated trick
    //when you want a named inline function assignment

    HTMLElement: ( function pullRabitFromHTMLElement(){
        //do stuff here
    } )
}

Magician.pullRabbitFrom = function(someThingy){
    return rabbitPullMap[someThingy.constructor.name]();
}

또는 동일한 맵 접근 방식을 사용하여 다른 객체 유형의 'this'구성 요소에 액세스하여 상속 된 프로토 타입을 건드리지 않고 메소드 인 것처럼 사용하려는 경우 :

var rabbitPullMap = {
    String: ( function(obj){

    //yes the anon wrapping funcs would make more sense in one spot elsewhere.

        return ( function pullRabbitFromString(obj){
            var rabbitReach = this.match(/rabbit/g);
            return rabbitReach.length;
        } ).call(obj);
    } ),

    HTMLElement: ( function(obj){
        return ( function pullRabitFromHTMLElement(obj){
            return this.querySelectorAll('.rabbit').length;
        } ).call(obj);
    } )
}

Magician.pullRabbitFrom = function(someThingy){

    var
        constructorName = someThingy.constructor.name,
        rabbitCnt = rabbitPullMap[constructorName](someThingy);

    console.log(
        [
            'The magician pulls ' + rabbitCnt,
            rabbitCnt === 1 ? 'rabbit' : 'rabbits',
            'out of her ' + constructorName + '.',
            rabbitCnt === 0 ? 'Boo!' : 'Yay!'
        ].join(' ');
    );
}

모든 언어 IMO에서 좋은 일반적인 원칙은 실제로 트리거를 당기는 코드에 도달하기 전에 이와 같은 분기 세부 정보를 정렬하는 것입니다. 그렇게하면 최고의 API 레벨에 관련된 모든 플레이어를 쉽게 볼 수있을뿐만 아니라 누군가가 관심을 가질만한 세부 정보를 찾을 수있는 위치를 쉽게 정렬 할 수 있습니다.

참고 : 이것은 실제로 아무도 RL을 사용하지 않는다고 가정하기 때문에 모두 테스트되지 않았습니다. 오타 / 버그가 있다고 확신합니다.


1

이것은 (나에게) 흥미롭고 복잡한 질문에 대한 대답입니다. 나는 실제로이 질문을 좋아하기 때문에 최선을 다해 대답 할 것입니다. 자바 스크립트 프로그래밍의 표준에 대해 전혀 연구를하지 않는다면, "올바른"방법을 선전하는 사람들이있는 것처럼 많은 "올바른"방법을 찾을 수있을 것입니다.

그러나 어떤 방법이 더 나은지에 대한 의견을 찾고 있기 때문에. 아무 것도 없어요

저는 개인적으로 "애드혹"디자인 방식을 선호합니다. c ++ / C # 배경에서 나온이 스타일은 내 개발 스타일에 가깝습니다. 하나의 pullRabbit 요청을 작성하고 하나의 요청 유형이 전달 된 인수를 확인하고 무언가를 수행하도록 할 수 있습니다. 즉, 어떤 유형의 인수가 한 번에 전달되는지 걱정할 필요가 없습니다. 엄격한 접근 방식을 사용하는 경우 변수가 어떤 유형인지 확인해야하지만 대신 메소드 호출 전에 수행해야합니다. 결국 질문은 전화를 걸기 전이나 후에 유형을 확인하고 싶습니까?

이것이 도움이되기를 바랍니다.이 답변과 관련하여 더 많은 질문을 자유롭게하십시오. 내 입장을 분명히하기 위해 최선을 다할 것입니다.


0

Magician.pullRabbitOutOfInt를 작성할 때 메소드를 작성할 때 생각한 내용을 문서화합니다. 호출자는 정수를 전달하면 이것이 작동 할 것으로 예상합니다. Magician.pullRabbitOutOfAnything을 작성할 때 호출자는 생각할 내용을 모르고 코드를 파고 실험해야합니다. Int에서는 작동하지만 Long에서는 작동합니까? 플로트? 더블? 이 코드를 작성한다면 얼마나 멀리 갈 수 있습니까? 어떤 종류의 논쟁을 기꺼이 지원 하시겠습니까?

  • 줄?
  • 배열?
  • 지도?
  • 스트림?
  • 기능?
  • 데이터베이스?

모호성은 이해하는 데 시간이 걸립니다. 글쓰기가 더 빠르다고 확신조차하지 않습니다.

Magician.pullRabbit = function(anything) {
  if (anything === undefined) {
    return new Rabbit(); // out of thin air
  } else if (isString(anything)) {
    // more
  } else if (isNumber(anything)) {
    // more
  } else {
      throw new Exception("You can't pull a rabbit out of that!");
  }
  // etc.
};

대 :

Magician.pullRabbitFromAir = fromAir() {
    return new Rabbit(); // out of thin air
}
Magician.pullRabbitFromStr = fromString(str)) {
    // more
}
Magician.pullRabbitFromInt = fromInt(int)) {
    // more
};

OK, 그래서 코드에 예외를 추가하여 호출자에게 그들이 한 일을 전달할 것이라고 상상하지 않았다는 것을 알려주었습니다. 그러나 특정 메소드를 작성하는 것은 (JavaScript 로이 작업을 수행 할 수 있는지 모르겠습니다) 더 이상 코드가 아니며 호출자로 이해하기가 훨씬 쉽습니다. 이 코드의 작성자가 생각한 것에 대한 현실적인 가정을 설정하고 코드를 사용하기 쉽게 만듭니다.


그냥 자바 스크립트로 할 수 있다는 것을 알려 주면 :)
Benjamin Gruenbaum

초 명시 적 표현이 읽기 / 이해하기 쉬웠다면, 법 책은 합법적 인 사람처럼 읽을 것입니다. 또한, 거의 같은 작업을 수행하는 유형별 메서드는 일반적인 JS 개발자에게 큰 타격을줍니다. 유형이 아닌 의도의 이름입니다. 필요한 인수는 코드의 한 곳이나 문서의 한 메소드 이름에서 허용되는 인수 목록을 확인하여 명확하거나 쉽게 찾을 수 있어야합니다.
Erik Reppen
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.