2010-11-16 30 views
39

Tôi đang làm việc trên API Scala (đối với Twilio, nhân tiện), nơi hoạt động có số lượng thông số khá lớn và nhiều thông số này có giá trị mặc định hợp lý. Để giảm đánh máy và tăng khả năng sử dụng, tôi đã quyết định sử dụng các lớp chữ hoa với các đối số được đặt tên và mặc định. Ví dụ cho TwiML Thu thập động từ:Các tùy chọn và đặt tên các đối số mặc định như dầu và nước trong API Scala?

case class Gather(finishOnKey: Char = '#', 
        numDigits: Int = Integer.MAX_VALUE, // Infinite 
        callbackUrl: Option[String] = None, 
        timeout: Int = 5 
       ) extends Verb 

Tham số quan tâm ở đây là callbackUrl. Đây là tham số duy nhất là thực sự là tùy chọn theo nghĩa là nếu không có giá trị nào được cung cấp, sẽ không có giá trị nào được áp dụng (hoàn toàn hợp pháp).

Tôi đã tuyên bố nó như là một tùy chọn để thực hiện thói quen bản đồ monadic với nó ở phía bên thi hành API, nhưng điều này đặt thêm một số gánh nặng cho người sử dụng API:

Gather(numDigits = 4, callbackUrl = Some("http://xxx")) 
// Should have been 
Gather(numDigits = 4, callbackUrl = "http://xxx") 

// Without the optional url, both cases are similar 
Gather(numDigits = 4) 

As far như tôi có thể tạo ra, có hai lựa chọn (không có ý định chơi chữ) để giải quyết vấn đề này. Hoặc là làm cho khách hàng API nhập một chuyển đổi ngầm vào phạm vi:

implicit def string2Option(s: String) : Option[String] = Some(s) 

Hoặc tôi có thể redeclare lớp trường hợp với một mặc định rỗng và chuyển nó sang một tùy chọn ở phía bên thực hiện:

case class Gather(finishOnKey: Char = '#', 
        numDigits: Int = Integer.MAX_VALUE, 
        callbackUrl: String = null, 
        timeout: Int = 5 
       ) extends Verb 

Câu hỏi của tôi như sau:

  1. Còn cách nào khác để giải quyết trường hợp cụ thể của tôi không?
  2. Thông thường hơn: Đối số được đặt tên là một tính năng ngôn ngữ mới (2.8). Nó có thể bật ra rằng tùy chọn và đặt tên đối số mặc định giống như dầu và nước? :)
  3. Có thể sử dụng giá trị mặc định null là lựa chọn tốt nhất trong trường hợp này?
+0

Cảm ơn rất nhiều câu trả lời hay cho câu hỏi 1 và 3! Tôi sẽ thực hiện một số mẫu sử dụng nhiều hơn các trường hợp sử dụng API trước khi quyết định có nên sử dụng tùy chọn của Aaron hay không, với tùy chọn hoặc chỉ sử dụng một giá trị mặc định null. Tuy nhiên, quá xấu không có sự lựa chọn rõ ràng - có lẽ chúng ta cần một tính năng ngôn ngữ mới để làm cho sự lựa chọn không có trí tuệ? – DaGGeRRz

Trả lời

23

Đây là một giải pháp khác, một phần được lấy cảm hứng từ Chris' answer. Nó cũng liên quan đến một wrapper, nhưng wrapper là trong suốt, bạn chỉ cần định nghĩa nó một lần, và người sử dụng của API không cần phải nhập khẩu bất kỳ chuyển đổi:

class Opt[T] private (val option: Option[T]) 
object Opt { 
    implicit def any2opt[T](t: T): Opt[T] = new Opt(Option(t)) // NOT Some(t) 
    implicit def option2opt[T](o: Option[T]): Opt[T] = new Opt(o) 
    implicit def opt2option[T](o: Opt[T]): Option[T] = o.option 
} 

case class Gather(finishOnKey: Char = '#', 
        numDigits: Opt[Int] = None, // Infinite 
        callbackUrl: Opt[String] = None, 
        timeout: Int = 5 
       ) extends Verb 

// this works with no import 
Gather(numDigits = 4, callbackUrl = "http://xxx") 
// this works too 
Gather(numDigits = 4, callbackUrl = Some("http://xxx")) 
// you can even safely pass the return value of an unsafe Java method 
Gather(callbackUrl = maybeNullString()) 

Để giải quyết những thiết kế lớn hơn vấn đề, tôi không nghĩ rằng sự tương tác giữa các tùy chọn và các tham số mặc định được đặt tên là nhiều dầu và nước như nó có thể có vẻ ở cái nhìn đầu tiên. Có một sự phân biệt rõ ràng giữa một trường tùy chọn và một với một giá trị mặc định. Trường tùy chọn (nghĩa là một trong các loại Option[T]) có thể không bao giờ có một giá trị. Mặt khác, một trường có giá trị mặc định không yêu cầu giá trị của nó được cung cấp như một đối số cho hàm tạo. Hai khái niệm này là như vậy trực giao, và nó không có gì ngạc nhiên khi một trường có thể là tùy chọn và có một giá trị mặc định.

Điều đó nói rằng, tôi nghĩ rằng một lập luận hợp lý có thể được thực hiện để sử dụng Opt thay vì Option cho các trường như vậy, ngoài việc lưu khách hàng một số thao tác nhập. Làm như vậy làm cho API linh hoạt hơn, theo nghĩa là bạn có thể thay thế đối số T bằng đối số Opt[T] (hoặc ngược lại) mà không vi phạm người gọi của hàm tạo [1].

Để sử dụng giá trị mặc định cho một trường công khai, null, tôi nghĩ đây là một ý tưởng tồi."Bạn" có thể biết rằng bạn mong đợi một null, nhưng khách hàng truy cập vào trường này có thể không. Ngay cả khi trường là riêng tư, sử dụng một số null đang yêu cầu gặp sự cố khi con đường phát triển khác phải duy trì mã của bạn. Tất cả các đối số thông thường về các giá trị null đều được phát tại đây - Tôi không nghĩ trường hợp sử dụng này là bất kỳ ngoại lệ đặc biệt nào.

[1] Miễn là bạn xóa chuyển đổi option2opt để người gọi phải vượt qua T bất cứ khi nào yêu cầu Opt[T].

+0

Tôi sử dụng để sử dụng mặc định 'null', nhưng tôi thực sự thích giải pháp này. –

+0

Đẹp. Nó giải quyết mọi vấn đề, nhưng có nhược điểm của việc giới thiệu một kiểu mới, chưa biết (cho người dùng) trong giao diện API. – DaGGeRRz

+0

@DaGGeRRz Đúng, nhưng tôi cho rằng chi phí là không đáng kể. Đó là một loại tiện ích mà người dùng chỉ cần học một lần để được hưởng lợi từ mã nguồn của bạn, và một kiểu rất đơn giản. –

-1

Tôi cũng rất ngạc nhiên bởi điều này. Tại sao không khái quát hóa:

implicit def any2Option[T](x: T): Option[T] = Some(x) 

Bất kỳ lý do nào không thể là một phần của Predef?

+3

Có - đó là một ý tưởng khủng khiếp, như đã được chỉ ra nhiều lần. –

+1

Có nhiều lý do, ít nhất là bạn sẽ kết thúc với 'Some (null)' xả rác chương trình của bạn, và toàn bộ điểm có 'Option' là kiểu an toàn (tức là nó không thể hoán đổi với nội dung của nó) –

+0

Như tôi đã đề cập trong nhận xét ở trên, tôi muốn tránh chuyển đổi tiềm ẩn. Và tôi cũng quan tâm đến "hình ảnh rộng hơn" về thiết kế API với các đối số mặc định được đặt tên. :) – DaGGeRRz

9

Không tự động chuyển đổi bất kỳ thứ gì thành Tùy chọn. Sử dụng my answer here, tôi nghĩ bạn có thể làm điều này một cách độc đáo nhưng theo một cách an toàn.

sealed trait NumDigits { /* behaviour interface */ } 
sealed trait FallbackUrl { /* behaviour interface */ } 
case object NoNumDigits extends NumDigits { /* behaviour impl */ } 
case object NofallbackUrl extends FallbackUrl { /* behaviour impl */ } 

implicit def int2numd(i : Int) = new NumDigits { /* behaviour impl */ } 
implicit def str2fallback(s : String) = new FallbackUrl { /* behaviour impl */ } 

class Gather(finishOnKey: Char = '#', 
       numDigits: NumDigits = NoNumDigits, // Infinite 
       fallbackUrl: FallbackUrl = NoFallbackUrl, 
       timeout: Int = 5 

Sau đó, bạn có thể gọi nó như bạn muốn - rõ ràng thêm hành vi của bạn phương pháp để FallbackUrlNumDigits cho phù hợp. Các tiêu cực chính ở đây là nó là một tấn boilerplate

Gather(numDigits = 4, fallbackUrl = "http://wibble.org") 
+0

Nếu giải pháp đang nhập chuyển đổi ngầm, tôi thấy rằng điều này ít có khả năng gây rối hơn việc nhập chuỗi2tùy chọn, nhưng tôi muốn tránh hoàn toàn chuyển đổi. Về phía triển khai API, sẽ ít nỗ lực hơn để xử lý các giá trị rỗng ... – DaGGeRRz

+2

Để làm rõ nhận xét khác của tôi. Cách tiếp cận của bạn là tốt đẹp trong ý nghĩa rằng nó là loại an toàn và sẽ rất mạnh mẽ nếu tôi cần thêm hành vi trong các đối số thông qua. Nhưng tôi thì không. :) Thêm một lớp bao bọc và chuyển đổi ngầm cho tất cả các loại đối số là IMO quá clunky. Ngoài ra, nó làm cho các tài liệu API ít rõ ràng hơn nhiều. Các chữ ký sẽ chỉ ra các tham số kiểu WrapperX khi chỉ ints và strings có thể được sử dụng ... – DaGGeRRz

+1

Có - Tôi hoàn toàn đồng ý với tất cả các điểm của bạn. Cá nhân tôi sẽ đi với đề nghị ban đầu của bạn; ví dụ: mặc định là 'Một số ("url") ' –

1

Tôi có thể tranh luận ủng hộ cách tiếp cận hiện tại của bạn, Some("callbackUrl")? Đó là tất cả 6 ký tự khác để người dùng API nhập, hiển thị cho họ thông số là tùy chọn và có lẽ làm cho việc triển khai dễ dàng hơn cho bạn.

+0

Tất cả đối số là tùy chọn, nhưng thông số cụ thể này sẽ được đặt thành Không nếu không được chỉ định. :) – DaGGeRRz

+0

DaGGeRRz, tôi không hiểu tại sao lại là vấn đề. Là người dùng API, mẫu đầu tiên bạn đưa ra hoàn toàn dễ hiểu. Tôi không có vấn đề gì khi gọi nó với Some (url). Có vẻ như bạn đang cố tránh một vấn đề không phải là vấn đề. –

4

Tôi nghĩ miễn là không hỗ trợ ngôn ngữ ở Scala cho một loại trống thực sự (giải thích dưới đây) ‘loại’, sử dụng Option có lẽ là giải pháp sạch hơn về lâu dài. Có thể ngay cả đối với tất cả các thông số mặc định.

Vấn đề là, những người sử dụng API của bạn biết rằng một số đối số của bạn được mặc định cũng có thể xử lý chúng như là tùy chọn. Vì vậy, họ tuyên bố họ là

var url: Option[String] = None 

Mọi thứ đều tốt đẹp và sạch sẽ và họ chỉ có thể chờ xem họ có nhận được thông tin để điền vào Tùy chọn này không.

Khi cuối cùng gọi API của bạn bằng đối số mặc định, chúng sẽ gặp sự cố.

// Your API 
case class Gather(url: String) { def this() = { ... } ... } 

// Their code 
val gather = url match { 
    case Some(u) => Gather(u) 
    case _ => Gather() 
} 

Tôi nghĩ rằng nó sẽ được dễ dàng hơn nhiều sau đó để làm điều này

val gather = Gather(url.openOrVoid) 

nơi *openOrVoid sẽ chỉ bị gạt ra ngoài trong trường hợp None. Nhưng điều này là không thể.

Vì vậy, bạn thực sự nên suy nghĩ về việc ai sẽ sử dụng API của bạn và cách họ có khả năng sử dụng nó. Cũng có thể người dùng của bạn đã sử dụng Option để lưu trữ tất cả các biến vì chính lý do họ biết chúng là tùy chọn cuối cùng…

Thông số mặc định là tốt nhưng chúng cũng phức tạp; đặc biệt là khi đã có loại Option xung quanh. Tôi nghĩ có một số sự thật trong câu hỏi thứ hai của bạn.

+0

Điểm rất tốt, đặc biệt là về những gì có thể được giả định về cách người dùng API khai báo các đối số của họ trước khi gọi API. – DaGGeRRz

+0

Phần đối số mặc định của API? Nếu không, nó sẽ là xấu thực hành để vượt qua trong Không có khi bạn thực sự có nghĩa là "Tôi không quan tâm". Điều gì sẽ xảy ra nếu mặc định thay đổi thành 'Some (thingElse)'? –

1

Tôi nghĩ bạn nên cắn viên đạn và tiếp tục với Option. Tôi đã phải đối mặt với vấn đề này trước đây, và nó thường biến mất sau khi một số tái cấu trúc. Đôi khi nó không, và tôi sống với nó. Nhưng thực tế là thông số mặc định không phải là thông số "tùy chọn" - chỉ là thông số có giá trị mặc định.

Tôi khá có lợi cho Debilski'sanswer.

5

Cá nhân, tôi nghĩ sử dụng 'null' làm giá trị mặc định hoàn toàn OK ở đây. Sử dụng Option thay vì null là khi bạn muốn truyền đạt cho khách hàng của bạn rằng một cái gì đó có thể không được xác định. Vì vậy, một giá trị trả về có thể được khai báo Option [...], hoặc một đối số phương thức cho các phương thức trừu tượng. Điều này tiết kiệm cho khách hàng từ việc đọc tài liệu hoặc, nhiều khả năng, có được NPEs vì không nhận ra một cái gì đó có thể là null.

Trong trường hợp của bạn, bạn biết rằng có thể có một giá trị rỗng. Và nếu bạn thích các phương thức của Option chỉ cần thực hiện val optionalFallbackUrl = Option(fallbackUrl) khi bắt đầu phương thức.

Tuy nhiên, cách tiếp cận này chỉ hoạt động đối với các loại AnyRef. Nếu bạn muốn sử dụng cùng một kỹ thuật cho bất kỳ loại đối số nào (không kết quả là Integer.MAX_VALUE làm thay thế cho null), thì tôi đoán bạn nên đi với một trong các câu trả lời khác

+0

Tôi sẽ đi với giải pháp đó - bởi vì lý do duy nhất bạn đang sử dụng một 'Tùy chọn' là để có được một trạng thái mà không có giá trị. Nhưng một trạng thái như vậy đã tồn tại trong 'null', và bạn sẽ phải xử lý nó. –

+0

Vâng, tôi đang nghiêng về phía sử dụng null và bạn tranh luận tốt cho nó. Về Integer.MAX_VALUE, nó được sử dụng để chỉ ra rằng số chữ số mặc định là vô hạn. – DaGGeRRz

+1

Trên thực tế, tôi sẽ tranh luận mạnh mẽ chống lại bằng cách sử dụng một giá trị mặc định 'null' cho một trường lớp trường hợp. Vì trường này được tự động công khai, tất cả các lý do thông thường để tránh 'null' được áp dụng. Ngay cả trong định nghĩa lớp, có giá trị tiềm năng 'null' đó là dễ bị lỗi khi bạn tính đến khả năng bảo trì dài hạn. –

Các vấn đề liên quan