Có một mẫu để xử lý các tham số chức năng xung đột?


38

Chúng tôi có chức năng API chia tổng số tiền thành số tiền hàng tháng dựa trên ngày bắt đầu và ngày kết thúc cụ thể.

// JavaScript

function convertToMonths(timePeriod) {
  // ... returns the given time period converted to months
}

function getPaymentBreakdown(total, startDate, endDate) {
  const numMonths = convertToMonths(endDate - startDate);

  return {
    numMonths,
    monthlyPayment: total / numMonths,
  };
}

Gần đây, một người tiêu dùng cho API này muốn chỉ định phạm vi ngày theo các cách khác: 1) bằng cách cung cấp số tháng thay vì ngày kết thúc hoặc 2) bằng cách cung cấp khoản thanh toán hàng tháng và tính ngày kết thúc. Để đáp ứng điều này, nhóm API đã thay đổi chức năng như sau:

// JavaScript

function addMonths(date, numMonths) {
  // ... returns a new date numMonths after date
}

function getPaymentBreakdown(
  total,
  startDate,
  endDate /* optional */,
  numMonths /* optional */,
  monthlyPayment /* optional */,
) {
  let innerNumMonths;

  if (monthlyPayment) {
    innerNumMonths = total / monthlyPayment;
  } else if (numMonths) {
    innerNumMonths = numMonths;
  } else {
    innerNumMonths = convertToMonths(endDate - startDate);
  }

  return {
    numMonths: innerNumMonths,
    monthlyPayment: total / innerNumMonths,
    endDate: addMonths(startDate, innerNumMonths),
  };
}

Tôi cảm thấy sự thay đổi này làm phức tạp API. Bây giờ người gọi cần phải lo lắng về các chẩn đoán ẩn với thực hiện của chức năng trong việc xác định các thông số chụp ưu tiên trong được sử dụng để tính toán phạm vi ngày (tức là bởi thứ tự ưu tiên monthlyPayment, numMonths, endDate). Nếu một người gọi không chú ý đến chữ ký hàm, họ có thể gửi nhiều tham số tùy chọn và bị nhầm lẫn về lý do tại sao endDatebị bỏ qua. Chúng tôi chỉ định hành vi này trong tài liệu chức năng.

Ngoài ra, tôi cảm thấy nó thiết lập một tiền lệ xấu và thêm trách nhiệm cho API mà nó không nên quan tâm (ví dụ như vi phạm SRP). Giả sử người tiêu dùng bổ sung muốn chức năng hỗ trợ nhiều trường hợp sử dụng hơn, chẳng hạn như tính toán totaltừ numMonthsmonthlyPaymenttham số. Chức năng này sẽ ngày càng phức tạp hơn theo thời gian.

Sở thích của tôi là giữ chức năng như cũ và thay vào đó yêu cầu người gọi endDatetự tính toán . Tuy nhiên, tôi có thể sai và tự hỏi liệu những thay đổi họ thực hiện có phải là một cách chấp nhận được để thiết kế một hàm API hay không.

Ngoài ra, có một mô hình chung để xử lý các tình huống như thế này? Chúng tôi có thể cung cấp thêm các hàm bậc cao hơn trong API bao bọc hàm ban đầu, nhưng điều này làm mờ API. Có lẽ chúng ta có thể thêm một tham số cờ bổ sung chỉ định cách tiếp cận nào được sử dụng bên trong hàm.


79
"Gần đây, một người tiêu dùng cho API này muốn [cung cấp] số tháng thay vì ngày kết thúc" - Đây là một yêu cầu phù phiếm. Họ có thể chuyển đổi số tháng thành ngày kết thúc thích hợp trong một hoặc hai dòng mã ở cuối.
Graham

12
trông giống như mô hình chống đối số Flag và tôi cũng khuyên bạn nên chia thành nhiều chức năng
njzk2

2
Là một lưu ý phụ, có các hàm có thể chấp nhận cùng loại và số lượng tham số và tạo ra các kết quả rất khác nhau dựa trên những điều đó - xem Date- bạn có thể cung cấp một chuỗi và nó có thể được phân tích cú pháp để xác định ngày. Tuy nhiên, cách xử lý oh này cũng có thể rất khó và có thể tạo ra kết quả không đáng tin cậy. Xem Datelại. Không phải là không thể làm đúng - Khoảnh khắc xử lý nó tốt hơn nhưng rất khó sử dụng.
VLAZ

Trên một tiếp tuyến nhỏ, bạn có thể muốn nghĩ về cách xử lý trường hợp monthlyPaymentđược đưa ra nhưng totalkhông phải là bội số nguyên của nó. Và cũng là cách xử lý các lỗi làm tròn dấu phẩy động có thể xảy ra nếu các giá trị không được đảm bảo là số nguyên (ví dụ: thử với total = 0.3monthlyPayment = 0.1).
Ilmari Karonen

@Graham Tôi đã không phản ứng với điều đó ... Tôi đã phản ứng với câu nói tiếp theo "Để đáp lại điều này, nhóm API đã thay đổi chức năng ..." - cuộn lên vào vị trí của thai nhi và bắt đầu rung chuyển - Không thành vấn đề dòng đó hoặc hai mã đi, một lệnh gọi API mới với định dạng khác nhau hoặc được thực hiện ở đầu người gọi. Chỉ cần không thay đổi một cuộc gọi API hoạt động như thế này!
Baldrickk

Câu trả lời:


99

Nhìn thấy việc thực hiện, nó cho tôi thấy những gì bạn thực sự yêu cầu ở đây là 3 chức năng khác nhau thay vì một:

Bản gốc:

function getPaymentBreakdown(total, startDate, endDate) 

Người cung cấp số tháng thay vì ngày kết thúc:

function getPaymentBreakdownByNoOfMonths(total, startDate, noOfMonths) 

và người cung cấp khoản thanh toán hàng tháng và tính ngày kết thúc:

function getPaymentBreakdownByMonthlyPayment(total, startDate, monthlyPayment) 

Bây giờ, không còn tham số tùy chọn nào nữa, và rõ ràng chức năng này được gọi là như thế nào và cho mục đích gì. Như đã đề cập trong các ý kiến, trong một ngôn ngữ được gõ chính xác, người ta cũng có thể sử dụng chức năng quá tải chức năng, phân biệt 3 chức năng khác nhau không nhất thiết phải bằng tên của họ, nhưng bằng chữ ký của họ, trong trường hợp điều này không làm xáo trộn mục đích của họ.

Lưu ý các hàm khác nhau không có nghĩa là bạn phải sao chép bất kỳ logic nào - bên trong, nếu các hàm này chia sẻ một thuật toán chung, thì nó sẽ được cấu trúc lại thành hàm "riêng tư".

Có một mô hình chung để xử lý các tình huống như thế này không

Tôi không nghĩ có một "mẫu" (theo nghĩa của các mẫu thiết kế GoF) mô tả thiết kế API tốt. Sử dụng tên tự mô tả, các hàm có ít tham số hơn, các hàm có tham số trực giao (= độc lập), chỉ là các nguyên tắc cơ bản để tạo mã có thể đọc, duy trì và có thể phát triển. Không phải mọi ý tưởng tốt trong lập trình đều nhất thiết phải là "mẫu thiết kế".


24
Trên thực tế, việc triển khai "chung" của mã có thể chỉ đơn giản là getPaymentBreakdown(hoặc thực sự là bất kỳ một trong số 3) và hai hàm còn lại chỉ chuyển đổi các đối số và gọi đó. Tại sao thêm một chức năng riêng tư là một bản sao hoàn hảo của một trong 3 điều này?
Giacomo Alzetta

@GiacomoAlzetta: điều đó là có thể. Nhưng tôi khá chắc chắn việc thực hiện sẽ trở nên đơn giản bằng cách cung cấp một chức năng thông thường mà chỉ chứa các "trở lại" một phần của Ops chức năng, và để cho 3 chức năng công cộng gọi hàm này với các thông số innerNumMonths, totalstartDate. Tại sao giữ một hàm quá phức tạp với 5 tham số, trong đó 3 gần như là tùy chọn (ngoại trừ một tham số phải được đặt), khi đó hàm 3 tham số cũng sẽ thực hiện công việc?
Doc Brown

3
Tôi không có ý nói "giữ chức năng 5 đối số". Tôi chỉ nói rằng khi bạn có một số logic thông thường thì logic này không cần phải riêng tư . Trong trường hợp này, cả 3 hàm có thể được cấu trúc lại để đơn giản chuyển các tham số thành ngày bắt đầu, vì vậy bạn có thể sử dụng getPaymentBreakdown(total, startDate, endDate)hàm công khai như là triển khai chung, công cụ khác sẽ chỉ đơn giản là tính tổng ngày / ngày bắt đầu / ngày kết thúc phù hợp và gọi nó.
Giacomo Alzetta

@GiacomoAlzetta: ok, là một sự hiểu lầm, tôi nghĩ bạn đang nói về việc thực hiện thứ hai getPaymentBreakdowntrong câu hỏi.
Doc Brown

Tôi sẽ đi xa hơn khi thêm một phiên bản mới của phương thức ban đầu được gọi rõ ràng là 'getPaymentBreakdownByStartAndEnd' và không dùng phương thức ban đầu, nếu bạn muốn cung cấp tất cả các phương thức này.
Erik

20

Ngoài ra, tôi cảm thấy nó thiết lập một tiền lệ xấu và thêm trách nhiệm cho API mà nó không nên quan tâm (ví dụ như vi phạm SRP). Giả sử người tiêu dùng bổ sung muốn chức năng hỗ trợ nhiều trường hợp sử dụng hơn, chẳng hạn như tính toán totaltừ numMonthsmonthlyPaymenttham số. Chức năng này sẽ ngày càng phức tạp hơn theo thời gian.

Bạn chính xác.

Sở thích của tôi là giữ chức năng như cũ và thay vào đó yêu cầu người gọi tự tính toán endDate. Tuy nhiên, tôi có thể sai và tự hỏi liệu những thay đổi họ thực hiện có phải là một cách chấp nhận được để thiết kế một hàm API hay không.

Điều này cũng không lý tưởng, bởi vì mã người gọi sẽ bị ô nhiễm với tấm nồi hơi không liên quan.

Ngoài ra, có một mô hình chung để xử lý các tình huống như thế này?

Giới thiệu một loại mới, như DateInterval. Thêm bất cứ điều gì các nhà xây dựng có ý nghĩa (ngày bắt đầu + ngày kết thúc, ngày bắt đầu + tháng tháng, bất cứ điều gì.). Chấp nhận đây là loại tiền tệ chung để thể hiện khoảng thời gian ngày / lần trong toàn hệ thống của bạn.


3
@DocBrown Yep. Trong các trường hợp như vậy (Ruby, Python, JS), thông thường chỉ sử dụng các phương thức tĩnh / lớp. Nhưng đó là một chi tiết triển khai, mà tôi không nghĩ là đặc biệt phù hợp với quan điểm của câu trả lời của tôi ("sử dụng một loại").
Alexander

2
Và ý tưởng này thật đáng buồn đạt đến giới hạn của nó với yêu cầu thứ ba: Ngày bắt đầu, Tổng thanh toán và Thanh toán hàng tháng - và chức năng sẽ tính DateInterval từ các tham số tiền - và bạn không nên đặt số tiền vào phạm vi ngày của mình ...
Falco

3
@DocBrown "chỉ chuyển vấn đề từ hàm hiện có sang hàm tạo của loại" Có, đó là đặt mã thời gian nơi mã thời gian nên đi, để mã tiền có thể là nơi mã tiền nên đi. Đó là SRP đơn giản, vì vậy tôi không chắc bạn đang nhận được gì khi bạn nói "chỉ" làm thay đổi vấn đề. Đó là những gì tất cả các chức năng làm. Họ không làm cho mã đi, họ di chuyển nó đến những nơi thích hợp hơn. Vấn đề của bạn với điều đó là gì? "nhưng xin chúc mừng, ít nhất 5 người chơi đã lấy mồi" Điều này nghe có vẻ ngu ngốc hơn tôi nghĩ (hy vọng) bạn dự định.
Alexander

@Falco Nghe có vẻ như là một phương thức mới đối với tôi (trên lớp máy tính thanh toán này, không phải DateInterval):calculatePayPeriod(startData, totalPayment, monthlyPayment)
Alexander

7

Đôi khi những biểu hiện trôi chảy giúp ích cho việc này:

let payment1 = forTotalAmount(1234)
                  .breakIntoPayments()
                  .byPeriod(months(2));

let payment2 = forTotalAmount(1234)
                  .breakIntoPayments()
                  .byDateRange(saleStart, saleEnd);

let monthsDue = forTotalAmount(1234)
                  .calculatePeriod()
                  .withPaymentsOf(12.34)
                  .monthly();

Dành đủ thời gian để thiết kế, bạn có thể đưa ra một API vững chắc hoạt động tương tự như ngôn ngữ dành riêng cho tên miền.

Ưu điểm lớn khác là các IDE có tính năng tự động hoàn thành hầu như không phù hợp để đọc tài liệu API, vì nó trực quan do khả năng tự khám phá của nó.

Có các tài nguyên ngoài đó như https://nikas.praninskas.com/javascript/2015/04/26/fluent-javascript/ hoặc https://github.com/nikaspran/fluent.js về chủ đề này.

Ví dụ (lấy từ liên kết tài nguyên đầu tiên):

let insert = (value) => ({into: (array) => ({after: (afterValue) => {
  array.splice(array.indexOf(afterValue) + 1, 0, value);
  return array;
}})});

insert(2).into([1, 3]).after(1); //[1, 2, 3]

8
Giao diện trôi chảy tự nó không làm cho bất kỳ nhiệm vụ cụ thể dễ dàng hoặc khó khăn hơn. Điều này có vẻ giống như mô hình Builder.
VLAZ

8
Việc triển khai sẽ khá phức tạp mặc dù nếu bạn cần ngăn các cuộc gọi nhầm nhưforTotalAmount(1234).breakIntoPayments().byPeriod(2).monthly().withPaymentsOf(12.34).byDateRange(saleStart, saleEnd);
Bergi

4
Nếu các nhà phát triển thực sự muốn bắn vào chân họ, có những cách dễ dàng hơn @Bergi. Tuy nhiên, ví dụ bạn đưa ra dễ đọc hơn nhiềuforTotalAmountAndBreakIntoPaymentsByPeriodThenMonthlyWithPaymentsOfButByDateRange(1234, 2, 12.34, saleStart, saleEnd);
DanielCuadra

5
@DanielCuadra Điểm tôi đang cố gắng đưa ra là câu trả lời của bạn không thực sự giải quyết được vấn đề OP có 3 tham số loại trừ lẫn nhau. Sử dụng mẫu trình xây dựng có thể làm cho cuộc gọi dễ đọc hơn (và tăng xác suất người dùng nhận thấy rằng nó không có ý nghĩa), nhưng chỉ sử dụng mẫu trình tạo sẽ không ngăn họ chuyển 3 giá trị cùng một lúc.
Bergi

2
@Falco Sẽ không? Vâng, điều đó là có thể, nhưng phức tạp hơn, và câu trả lời không đề cập đến điều này. Các trình xây dựng phổ biến hơn mà tôi thấy chỉ bao gồm một lớp duy nhất. Nếu câu trả lời được chỉnh sửa để bao gồm mã của (các) nhà xây dựng, tôi sẽ vui vẻ xác nhận nó và xóa downvote của tôi.
Bergi

2

Vâng, trong các ngôn ngữ khác, bạn sẽ sử dụng các tham số được đặt tên . Điều này có thể được mô phỏng trong Javscript:

function getPaymentBreakdown(total, startDate, durationSpec) { ... }

getPaymentBreakdown(100, today, {endDate: whatever});
getPaymentBreakdown(100, today, {noOfMonths: 4});
getPaymentBreakdown(100, today, {monthlyPayment: 20});

6
Cũng giống như các mô hình xây dựng dưới đây, điều này làm cho các cuộc gọi dễ đọc hơn (và làm tăng xác suất của các Nhận ra người dùng mà nó không có ý nghĩa), nhưng đặt tên cho các thông số không ngăn chặn người dùng vẫn đi qua 3 giá trị cùng một lúc - ví dụ getPaymentBreakdown(100, today, {endDate: whatever, noOfMonths: 4, monthlyPayment: 20}).
Bergi

1
Không nên :thay thế =?
Barmar

Tôi đoán bạn có thể kiểm tra rằng chỉ một trong các tham số là không null (hoặc không có trong từ điển).
Mateen Ulhaq

1
@Bergi - Bản thân cú pháp không ngăn người dùng chuyển các tham số vô nghĩa nhưng bạn chỉ cần thực hiện một số xác thực và ném lỗi
slebetman

@Bergi Tôi không có nghĩa là một chuyên gia Javascript, nhưng tôi nghĩ Nhiệm vụ hủy cấu trúc trong ES6 có thể giúp đỡ ở đây, mặc dù tôi rất hiểu kiến ​​thức về vấn đề này.
Gregory Currie

1

Thay vào đó, bạn cũng có thể phá vỡ trách nhiệm chỉ định số tháng và loại bỏ chức năng này:

getPaymentBreakdown(420, numberOfMonths(3))
getPaymentBreakdown(420, dateRage(a, b))
getPaymentBreakdown(420, paymentAmount(350))

Và getpaymentBreakdown sẽ nhận được một đối tượng cung cấp số tháng cơ sở

Những hàm này sẽ trả về hàm bậc cao hơn, ví dụ hàm.

function numberOfMonths(months) {
  return {months: (total) => months};
}

function dateRange(startDate, endDate) {
  return {months: (total) => convertToMonths(endDate - startDate)}
}

function monthlyPayment(amount) {
  return {months: (total) => total / amount}
}


function getPaymentBreakdown(total, {months}) {
  const numMonths= months(total);
  return {
    numMonths, 
    monthlyPayment: total / numMonths,
    endDate: addMonths(startDate, numMonths)
  };
}

Điều gì đã xảy ra với totalstartDatecác tham số?
Bergi

Đây có vẻ là một API đẹp, nhưng bạn có thể vui lòng thêm cách bạn tưởng tượng bốn chức năng đó sẽ được thực hiện không? (Với các loại biến thể và giao diện chung, điều này có thể khá thanh lịch, nhưng không rõ bạn nghĩ gì).
Bergi

@Bergi đã chỉnh sửa bài đăng của tôi
Vinz243

0

Và nếu bạn đang làm việc với một hệ thống với các loại dữ liệu đại số / đại số bị phân biệt đối xử, bạn có thể chuyển nó vào như, giả sử, a TimePeriodSpecification.

type TimePeriodSpecification =
    | DateRange of startDate : DateTime * endDate : DateTime
    | MonthCount of startDate : DateTime * monthCount : int
    | MonthlyPayment of startDate : DateTime * monthlyAmount : float

và sau đó không có vấn đề nào xảy ra khi bạn không thể thực hiện một và cứ thế.


Đây chắc chắn là cách tôi sẽ tiếp cận điều này trong một ngôn ngữ có sẵn các loại như thế này. Tôi đã cố gắng giữ cho câu hỏi của mình không liên quan đến ngôn ngữ nhưng có lẽ nó nên tính đến ngôn ngữ được sử dụng bởi vì cách tiếp cận như thế này trở nên khả thi trong một số trường hợp.
CalMlynarchot
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.