Trong thiết kế API, khi nào nên sử dụng / tránh đa hình ad hoc?


14

Sue đang thiết kế một thư viện JavaScript Magician.js. Linchpin của nó là một chức năng kéo Rabbitra khỏi đối số được thông qua.

Cô biết người dùng của nó có thể muốn kéo một con thỏ ra khỏi a String, a Number, a Function, thậm chí là a HTMLElement. Với ý nghĩ đó, cô ấy có thể thiết kế API của mình như vậy:

Giao diện nghiêm ngặt

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

Mỗi hàm trong ví dụ trên sẽ biết cách xử lý đối số của kiểu được chỉ định trong tên hàm / tên tham số.

Hoặc, cô ấy có thể thiết kế nó như vậy:

Giao diện "ad hoc"

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

pullRabbitsẽ phải tính đến sự đa dạng của các loại dự kiến ​​khác nhau mà anythingđối số có thể là, cũng như (tất nhiên) là một loại không mong muốn:

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.
};

Cái trước (nghiêm ngặt) có vẻ rõ ràng hơn, có thể an toàn hơn và có thể hiệu quả hơn - vì có rất ít hoặc không có chi phí nào cho việc kiểm tra loại hoặc chuyển đổi loại. Nhưng cái sau (ad hoc) cảm thấy đơn giản hơn khi nhìn từ bên ngoài, ở chỗ nó "chỉ hoạt động" với bất kỳ đối số nào mà người tiêu dùng API thấy thuận tiện để vượt qua nó.

Để trả lời cho câu hỏi này , tôi muốn thấy những ưu và nhược điểm cụ thể đối với cách tiếp cận (hoặc với một cách tiếp cận khác hoàn toàn, nếu không lý tưởng), Sue nên biết cách tiếp cận nào khi thiết kế API thư viện của mình.


2

Câu trả lời:


7

Một số ưu và nhược điểm

Ưu điểm cho đa hình:

  • Một giao diện đa hình nhỏ hơn dễ đọc hơn. Tôi chỉ phải nhớ một phương pháp.
  • Nó đi với cách ngôn ngữ được sử dụng - Gõ vịt.
  • Nếu nó rõ ràng những đối tượng tôi muốn kéo một con thỏ ra khỏi, dù sao cũng không nên có sự mơ hồ.
  • Thực hiện nhiều kiểm tra loại được coi là xấu ngay cả trong các ngôn ngữ tĩnh như Java, trong đó có nhiều kiểm tra loại cho loại đối tượng tạo mã xấu, nên pháp sư thực sự cần phân biệt giữa loại đối tượng mà anh ta kéo thỏ ra khỏi ?

Ưu điểm cho quảng cáo:

  • Nó ít rõ ràng hơn, tôi có thể kéo một chuỗi ra khỏi một Catthể hiện không? Điều đó sẽ làm việc? nếu không, hành vi là gì? Nếu tôi không giới hạn loại ở đây, tôi phải làm như vậy trong tài liệu hoặc trong các thử nghiệm có thể làm cho hợp đồng tồi tệ hơn.
  • Bạn có tất cả cách xử lý kéo một con thỏ ở một nơi, Pháp sư (một số người có thể coi đây là một kẻ lừa đảo)
  • Các trình tối ưu hóa JS hiện đại khác nhau giữa các hàm đơn hình (chỉ hoạt động trên một loại) và các hàm đa hình. Họ biết cách tối ưu hóa những cái đơn hình tốt hơn nhiều nên pullRabbitOutOfStringphiên bản có thể sẽ nhanh hơn nhiều trong các động cơ như V8. Xem video này để biết thêm thông tin. Chỉnh sửa: Tôi đã tự viết một bản hoàn hảo, hóa ra trong thực tế, điều này không phải lúc nào cũng đúng .

Một số giải pháp thay thế:

Theo tôi, kiểu thiết kế này không phải là 'Java-Scripty' để bắt đầu. JavaScript là một ngôn ngữ khác với các thành ngữ khác nhau từ các ngôn ngữ như C #, Java hoặc Python. Những thành ngữ này bắt nguồn từ nhiều năm các nhà phát triển cố gắng hiểu các phần yếu và mạnh của ngôn ngữ, điều tôi muốn làm là cố gắng gắn bó với các thành ngữ này.

Có hai giải pháp hay mà tôi có thể nghĩ ra:

  • Nâng cao các vật thể, làm cho các vật thể "có thể kéo được", làm cho chúng phù hợp với một giao diện trong thời gian chạy, sau đó có phép thuật hoạt động trên các vật thể có thể kéo được.
  • Sử dụng mô hình chiến lược, dạy cho Magician một cách linh hoạt cách xử lý các loại đối tượng khác nhau.

Giải pháp 1: Vật nâng

Một giải pháp phổ biến cho vấn đề này là 'nâng cao' các vật thể với khả năng có những con thỏ kéo ra khỏi chúng.

Đó là, có một chức năng lấy một số loại đối tượng và thêm kéo ra khỏi mũ cho nó. Cái gì đó như:

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

Tôi có thể tạo các makePullablehàm như vậy cho các đối tượng khác, tôi có thể tạo một makePullableString, v.v. Tôi đang xác định chuyển đổi trên từng loại. Tuy nhiên, sau khi tôi nâng các đối tượng của mình, tôi không có loại nào để sử dụng chúng theo cách chung chung. Một giao diện trong JavaScript được xác định bằng cách gõ vịt, nếu nó có pullOfHatphương thức tôi có thể kéo nó bằng phương thức của Magician.

Sau đó, Magician có thể làm:

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

Nâng cao các đối tượng, sử dụng một số kiểu mẫu mixin có vẻ như là việc cần làm nhiều hơn. (Lưu ý đây là vấn đề với các loại giá trị trong ngôn ngữ là chuỗi, số, null, không xác định và boolean, nhưng tất cả chúng đều có thể đóng hộp)

Dưới đây là một ví dụ về những mã như vậy có thể trông như thế nào

Giải pháp 2: Mô hình chiến lược

Khi thảo luận về câu hỏi này trong phòng trò chuyện JS trong StackOverflow, bạn tôi phenomnomnominal đã đề nghị sử dụng mẫu Chiến lược .

Điều này sẽ cho phép bạn thêm các khả năng để kéo thỏ ra khỏi các đối tượng khác nhau trong thời gian chạy và sẽ tạo mã rất JavaScript. Một pháp sư có thể học cách kéo các vật thể khác nhau ra khỏi mũ và nó kéo chúng dựa trên kiến ​​thức đó.

Đây là cách nó có thể trông trong 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

Bạn có thể xem mã JS tương đương ở đây .

Bằng cách này, bạn được hưởng lợi từ cả hai thế giới, hành động làm thế nào để kéo không được kết hợp chặt chẽ với các đối tượng hoặc Pháp sư và tôi nghĩ rằng điều này tạo ra một giải pháp rất hay.

Cách sử dụng sẽ là một cái gì đó như:

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
Tôi đã +10 câu trả lời này cho câu trả lời rất kỹ lưỡng từ đó tôi đã học được rất nhiều, nhưng, theo quy tắc SE, bạn sẽ phải giải quyết cho +1 ... :-)
Marjan Venema

@MarjanVenema Các câu trả lời khác cũng tốt, hãy nhớ đọc chúng. Tôi rất vui vì bạn thích điều này. Hãy dừng lại và đặt thêm câu hỏi thiết kế.
Benjamin Gruenbaum

4

Vấn đề là bạn đang cố gắng thực hiện một loại đa hình không tồn tại trong JavaScript - JavaScript gần như được đối xử tốt nhất như một ngôn ngữ gõ vịt, mặc dù nó hỗ trợ một số loại khoa.

Để tạo API tốt nhất, câu trả lời là bạn nên triển khai cả hai. Việc gõ nhiều hơn một chút, nhưng sẽ tiết kiệm rất nhiều công việc trong thời gian dài cho người dùng API của bạn.

pullRabbitchỉ nên là một phương thức trọng tài kiểm tra các loại và gọi hàm thích hợp được liên kết với loại đối tượng đó (ví dụ pullRabbitOutOfHtmlElement).

Bằng cách đó, trong khi người dùng tạo mẫu có thể sử dụng pullRabbit, nhưng nếu họ nhận thấy sự chậm lại, họ có thể thực hiện kiểm tra loại trên đầu của họ (có thể là cách nhanh hơn) và chỉ cần gọi pullRabbitOutOfHtmlElementtrực tiếp.


2

Đây là JavaScript. Khi bạn hiểu rõ hơn về nó, bạn sẽ thấy thường có một con đường giữa giúp loại bỏ những tình huống khó xử như thế này. Ngoài ra, thực sự không có vấn đề gì nếu một 'loại' không được hỗ trợ bị bắt bởi một thứ gì đó hoặc bị phá vỡ khi ai đó cố gắng sử dụng nó vì không có biên dịch so với thời gian chạy. Nếu bạn sử dụng sai nó sẽ phá vỡ. Cố gắng che giấu rằng nó bị vỡ hoặc làm cho nó hoạt động được một nửa khi nó bị vỡ không làm thay đổi thực tế là một cái gì đó bị hỏng.

Vì vậy, có bánh của bạn và ăn nó và học cách tránh nhầm lẫn và phá vỡ không cần thiết bằng cách giữ cho mọi thứ thực sự, thực sự rõ ràng, như được đặt tên tốt và với tất cả các chi tiết đúng ở tất cả các vị trí thích hợp.

Trước hết, tôi rất khuyến khích tập thói quen đưa vịt của bạn ra liên tiếp trước khi bạn cần kiểm tra các loại. Điều gọn gàng và hiệu quả nhất (nhưng không phải lúc nào cũng tốt nhất khi các nhà xây dựng bản địa quan tâm) điều cần làm là đánh vào các nguyên mẫu trước để phương thức của bạn thậm chí không phải quan tâm đến loại được hỗ trợ đang chơi.

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

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

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

Lưu ý: Nó được coi rộng rãi là hình thức xấu để làm điều này với Object vì mọi thứ thừa hưởng từ Object. Cá nhân tôi cũng sẽ tránh Chức năng. Một số người có thể cảm thấy lo lắng về việc chạm vào bất kỳ nguyên mẫu của nhà xây dựng bản địa nào có thể không phải là một chính sách tồi nhưng ví dụ này vẫn có thể phục vụ khi làm việc với các nhà xây dựng đối tượng của riêng bạn.

Tôi sẽ không lo lắng về cách tiếp cận này đối với một phương thức sử dụng cụ thể như vậy mà không có khả năng ghi đè thứ gì đó từ một thư viện khác trong một ứng dụng ít phức tạp hơn nhưng đó là bản năng tốt để tránh khẳng định bất kỳ điều gì nói chung quá mức đối với các phương thức gốc trong JavaScript nếu bạn không phải trừ khi bạn bình thường hóa các phương pháp mới hơn trong các trình duyệt lỗi thời.

May mắn thay, bạn luôn có thể chỉ các loại ánh xạ trước hoặc tên của hàm tạo cho các phương thức (hãy cẩn thận IE <= 8 không có <object> .constructor.name yêu cầu bạn phân tích ra khỏi kết quả toString từ thuộc tính của hàm tạo). Bạn vẫn đang kiểm tra hiệu quả tên hàm tạo (typeof là loại vô dụng trong JS khi so sánh các đối tượng) nhưng ít nhất nó đọc đẹp hơn nhiều so với một câu lệnh chuyển đổi khổng lồ hoặc chuỗi if / khác trong mỗi lệnh gọi của phương thức tới mức rộng nhiều đối tượng.

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]();
}

Hoặc sử dụng cùng một cách tiếp cận bản đồ, nếu bạn muốn truy cập vào thành phần 'này' của các loại đối tượng khác nhau để sử dụng chúng như thể chúng là các phương thức mà không chạm vào các nguyên mẫu được kế thừa của chúng:

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(' ');
    );
}

Một nguyên tắc chung tốt trong bất kỳ ngôn ngữ IMO nào, là cố gắng sắp xếp các chi tiết phân nhánh như thế này trước khi bạn nhận được mã thực sự bóp cò. Bằng cách đó, thật dễ dàng để thấy tất cả những người chơi tham gia ở cấp API hàng đầu đó để có cái nhìn tổng quan đẹp, nhưng cũng dễ dàng hơn nhiều để sắp xếp các chi tiết mà ai đó có thể quan tâm có thể được tìm thấy.

Lưu ý: đây là tất cả chưa được kiểm tra, vì tôi cho rằng không ai thực sự có sử dụng RL cho nó. Tôi chắc chắn có lỗi chính tả / lỗi.


1

Đây (với tôi) là một câu hỏi thú vị và phức tạp để trả lời. Tôi thực sự thích câu hỏi này vì vậy tôi sẽ cố hết sức để trả lời. Nếu bạn thực hiện bất kỳ nghiên cứu nào về các tiêu chuẩn cho lập trình javascript, bạn sẽ tìm thấy nhiều cách thực hiện "đúng" vì có người cho rằng cách làm "đúng" của nó.

Nhưng vì bạn đang tìm kiếm một ý kiến ​​về cách nào là tốt hơn. Ở đây không có gì.

Cá nhân tôi thích cách tiếp cận thiết kế "adhoc". Đến từ nền tảng c ++ / C #, đây là phong cách phát triển của tôi nhiều hơn. Bạn có thể tạo một yêu cầu pullRợi và yêu cầu một loại yêu cầu kiểm tra đối số được truyền vào và thực hiện một cái gì đó. Điều này có nghĩa là bạn không phải lo lắng về loại đối số nào được thông qua bất cứ lúc nào. Nếu bạn sử dụng cách tiếp cận nghiêm ngặt, bạn vẫn sẽ cần kiểm tra loại biến nào nhưng thay vào đó bạn sẽ làm điều đó trước khi bạn thực hiện cuộc gọi phương thức. Vì vậy, cuối cùng câu hỏi là, bạn có muốn kiểm tra loại trước khi bạn thực hiện cuộc gọi hoặc sau đó.

Tôi hy vọng điều này có ích, xin vui lòng đặt thêm câu hỏi liên quan đến câu trả lời này, tôi sẽ cố gắng hết sức để làm rõ vị trí của mình.


0

Khi bạn viết, Magician.pullRmusOutOfInt, nó ghi lại những gì bạn nghĩ về khi bạn viết phương thức. Người gọi sẽ mong đợi nó hoạt động nếu vượt qua bất kỳ số nguyên nào. Khi bạn viết, Magician.pullRợiOutOfAnything, người gọi không biết phải nghĩ gì và phải đi sâu vào mã của bạn và thử nghiệm. Nó có thể hoạt động cho một Int, nhưng nó sẽ hoạt động trong một thời gian dài? Một cái phao? Một đôi? Nếu bạn đang viết mã này, bạn sẽ đi bao xa? Những loại đối số mà bạn sẵn sàng hỗ trợ?

  • Dây?
  • Mảng nào?
  • Bản đồ?
  • Dòng suối?
  • Chức năng?
  • Cơ sở dữ liệu?

Sự mơ hồ cần có thời gian để hiểu. Tôi thậm chí không tin rằng viết nhanh hơn:

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.
};

Vs:

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

OK, vì vậy tôi đã thêm một ngoại lệ vào mã của bạn (mà tôi rất khuyến khích) để nói với người gọi rằng bạn không bao giờ tưởng tượng họ sẽ vượt qua bạn những gì họ đã làm. Nhưng viết các phương thức cụ thể (tôi không biết nếu JavaScript cho phép bạn làm điều này) thì không còn mã và dễ hiểu hơn nhiều khi là người gọi. Nó thiết lập các giả định thực tế về những gì tác giả của mã này nghĩ về và làm cho mã dễ sử dụng.


Chỉ cần cho bạn biết rằng JavaScript cho phép bạn làm điều đó :)
Benjamin Gruenbaum

Nếu siêu rõ ràng dễ đọc / dễ hiểu hơn, sách hướng dẫn sẽ đọc như hợp pháp. Ngoài ra, các phương thức theo kiểu mà tất cả đều thực hiện cùng một điều là một lỗi DRY lớn đối với nhà phát triển JS điển hình của bạn. Tên cho ý định, không phải cho loại. Những gì lập luận cần phải rõ ràng hoặc rất dễ tìm kiếm bằng cách kiểm tra tại một nơi trong mã hoặc trong danh sách các đối số được chấp nhận tại một tên phương thức trong tài liệu.
Erik Reppen
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.