Apakah ada pola untuk menangani parameter fungsi yang saling bertentangan?

38

Kami memiliki fungsi API yang memecah jumlah total menjadi jumlah bulanan berdasarkan tanggal mulai dan berakhir yang diberikan.

// 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,
  };
}

Baru-baru ini, konsumen untuk API ini ingin menentukan rentang tanggal dengan cara lain: 1) dengan memberikan jumlah bulan alih-alih tanggal akhir, atau 2) dengan memberikan pembayaran bulanan dan menghitung tanggal akhir. Menanggapi hal ini, tim API mengubah fungsi menjadi sebagai berikut:

// 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),
  };
}

Saya merasa perubahan ini menyulitkan API. Sekarang penelepon perlu khawatir tentang heuristik yang tersembunyi dengan pelaksanaan fungsi dalam menentukan parameter mengambil preferensi dalam yang digunakan untuk menghitung rentang tanggal (yaitu dengan urutan prioritas monthlyPayment, numMonths, endDate). Jika seorang penelepon tidak memperhatikan tanda tangan fungsi, mereka mungkin mengirim beberapa parameter opsional dan bingung mengapa endDatediabaikan. Kami menentukan perilaku ini dalam dokumentasi fungsi.

Selain itu saya merasa itu menetapkan preseden buruk dan menambahkan tanggung jawab ke API yang seharusnya tidak menjadi perhatiannya (yaitu melanggar SRP). Misalkan konsumen menginginkan fungsi untuk mendukung lebih banyak kasus penggunaan, seperti menghitung totaldari numMonthsdan monthlyPaymentparameter. Fungsi ini akan menjadi semakin rumit dari waktu ke waktu.

Preferensi saya adalah untuk menjaga fungsinya seperti semula dan sebaliknya meminta penelepon untuk menghitung endDatesendiri. Namun, saya mungkin salah dan bertanya-tanya apakah perubahan yang mereka lakukan merupakan cara yang dapat diterima untuk merancang fungsi API.

Atau, apakah ada pola umum untuk menangani skenario seperti ini? Kami dapat menyediakan fungsi tingkat tinggi tambahan di API kami yang membungkus fungsi asli, tetapi ini membengkak API. Mungkin kita bisa menambahkan parameter flag tambahan yang menentukan pendekatan mana yang akan digunakan di dalam fungsi.

CalMlynarczyk
sumber
79
"Baru-baru ini, konsumen untuk API ini ingin [memberikan] jumlah bulan alih-alih tanggal akhir" - Ini adalah permintaan sembrono. Mereka dapat mengubah # bulan menjadi tanggal akhir yang tepat dalam satu atau dua baris kode pada akhirnya.
Graham
12
yang terlihat seperti Flag Argument anti-pattern, dan saya juga akan merekomendasikan pemisahan menjadi beberapa fungsi
njzk2
2
Sebagai catatan tambahan, ada fungsi - fungsi yang dapat menerima jenis dan jumlah parameter yang sama dan menghasilkan hasil yang sangat berbeda berdasarkan yang - lihat Date- Anda dapat menyediakan string dan dapat diurai untuk menentukan tanggal. Namun, cara ini oh menangani parameter juga bisa sangat rewel dan mungkin menghasilkan hasil yang tidak dapat diandalkan. Lihat Datelagi. Ini tidak mungkin dilakukan dengan benar - Momen menanganinya dengan lebih baik tetapi sangat menjengkelkan untuk digunakan.
VLAZ
Pada sedikit singgung, Anda mungkin ingin berpikir tentang bagaimana menangani kasus di mana monthlyPaymentdiberikan tetapi totalbukan kelipatan bilangan bulat dari itu. Dan juga bagaimana menghadapi kemungkinan kesalahan roundoff floating point jika nilainya tidak dijamin bilangan bulat (mis. Coba dengan total = 0.3dan monthlyPayment = 0.1).
Ilmari Karonen
@ Bahraham Saya tidak bereaksi terhadap hal itu ... Saya bereaksi terhadap pernyataan berikutnya "Menanggapi hal ini, tim API mengubah fungsi ..." - menggulung ke posisi janin dan mulai bergoyang - Tidak masalah di mana baris atau dua kode tersebut berjalan, baik panggilan API baru dengan format yang berbeda, atau dilakukan di sisi pemanggil. Hanya saja, jangan mengubah panggilan API yang berfungsi seperti ini!
Baldrickk

Jawaban:

99

Melihat implementasinya, menurut saya apa yang sebenarnya Anda perlukan di sini adalah 3 fungsi berbeda, bukan satu:

Yang asli:

function getPaymentBreakdown(total, startDate, endDate) 

Yang memberikan jumlah bulan, bukan tanggal akhir:

function getPaymentBreakdownByNoOfMonths(total, startDate, noOfMonths) 

dan yang memberikan pembayaran bulanan dan menghitung tanggal akhir:

function getPaymentBreakdownByMonthlyPayment(total, startDate, monthlyPayment) 

Sekarang, tidak ada parameter opsional lagi, dan itu harus cukup jelas fungsi mana yang disebut bagaimana dan untuk tujuan apa. Seperti yang disebutkan dalam komentar, dalam bahasa yang diketik dengan ketat, orang juga dapat menggunakan fungsi yang berlebihan, membedakan 3 fungsi yang berbeda tidak harus dengan nama mereka, tetapi dengan tanda tangan mereka, jika hal ini tidak mengaburkan tujuan mereka.

Perhatikan fungsi-fungsi yang berbeda tidak berarti Anda harus menduplikasi logika apa pun - secara internal, jika fungsi-fungsi ini memiliki algoritma yang sama, itu harus di refactored ke fungsi "pribadi".

apakah ada pola umum untuk menangani skenario seperti ini

Saya tidak berpikir ada "pola" (dalam arti pola desain GoF) yang menggambarkan desain API yang baik. Menggunakan nama yang menggambarkan diri sendiri, fungsi dengan parameter lebih sedikit, fungsi dengan parameter ortogonal (= independen), hanyalah prinsip dasar untuk membuat kode yang dapat dibaca, dipelihara, dan dikembangkan. Tidak setiap ide bagus dalam pemrograman selalu merupakan "pola desain".

Doc Brown
sumber
24
Sebenarnya implementasi "umum" dari kode bisa saja getPaymentBreakdown(atau benar-benar salah satu dari 3) dan dua fungsi lainnya hanya mengkonversi argumen dan menyebutnya. Mengapa menambahkan fungsi pribadi yang merupakan salinan sempurna dari salah satu dari 3 ini?
Giacomo Alzetta
@ GiacomoAlzetta: itu mungkin. Tapi saya cukup yakin implementasinya akan menjadi lebih sederhana dengan menyediakan fungsi umum yang hanya berisi bagian "kembali" dari fungsi OPs, dan biarkan fungsi publik memanggil fungsi ini dengan parameter innerNumMonths, totaldan startDate. Mengapa menyimpan fungsi yang terlalu rumit dengan 5 parameter, di mana 3 hampir opsional (kecuali satu harus ditetapkan), ketika fungsi 3-parameter akan melakukan pekerjaan dengan baik?
Doc Brown
3
Saya tidak bermaksud mengatakan "pertahankan fungsi 5 argumen". Saya hanya mengatakan bahwa ketika Anda memiliki beberapa logika umum, logika ini tidak perlu bersifat pribadi . Dalam hal ini ketiga fungsi dapat dire-reforasi hanya untuk mentransformasikan parameter ke tanggal mulai-akhir, sehingga Anda dapat menggunakan getPaymentBreakdown(total, startDate, endDate)fungsi publik sebagai implementasi umum, alat lainnya hanya akan menghitung total / mulai / tanggal akhir yang sesuai dan menyebutnya.
Giacomo Alzetta
@ GiacomoAlzetta: ok, ada kesalahpahaman, saya pikir Anda berbicara tentang implementasi kedua getPaymentBreakdowndalam pertanyaan.
Doc Brown
Saya akan menambahkan versi baru dari metode asli yang secara eksplisit disebut 'getPaymentBreakdownByStartAndEnd' dan mencabut metode asli, jika Anda ingin menyediakan semua ini.
Erik
20

Selain itu saya merasa itu menetapkan preseden buruk dan menambahkan tanggung jawab ke API yang seharusnya tidak menjadi perhatiannya (yaitu melanggar SRP). Misalkan konsumen menginginkan fungsi untuk mendukung lebih banyak kasus penggunaan, seperti menghitung totaldari numMonthsdan monthlyPaymentparameter. Fungsi ini akan menjadi semakin rumit dari waktu ke waktu.

Anda benar sekali.

Preferensi saya adalah untuk mempertahankan fungsinya seperti semula dan sebaliknya meminta penelepon untuk menghitung sendiri tanggal akhir. Namun, saya mungkin salah dan bertanya-tanya apakah perubahan yang mereka lakukan merupakan cara yang dapat diterima untuk merancang fungsi API.

Ini juga tidak ideal, karena kode pemanggil akan tercemar dengan pelat ketel yang tidak terkait.

Atau, apakah ada pola umum untuk menangani skenario seperti ini?

Memperkenalkan tipe baru, seperti DateInterval. Tambahkan konstruktor apa pun yang masuk akal (tanggal mulai + tanggal akhir, tanggal mulai + num bulan, apa pun.). Adopsi ini sebagai jenis mata uang umum untuk mengekspresikan interval tanggal / waktu di seluruh sistem Anda.

Alexander - Pasang kembali Monica
sumber
3
@DocBrown Yap. Dalam kasus seperti itu (Ruby, Python, JS), biasanya menggunakan metode statis / kelas saja. Tapi itu detail implementasi, yang menurut saya tidak relevan dengan poin jawaban saya ("gunakan tipe").
Alexander - Pasang kembali Monica
2
Dan ide ini dengan sedih mencapai batasnya dengan persyaratan ketiga: Tanggal Mulai, Pembayaran total dan Pembayaran bulanan - dan fungsinya akan menghitung DateInterval dari parameter uang - dan Anda tidak boleh memasukkan jumlah uang ke dalam rentang tanggal Anda ...
Falco
3
@DocBrown "hanya menggeser masalah dari fungsi yang ada ke konstruktor tipe" Ya, itu meletakkan kode waktu di mana kode waktu harus pergi, sehingga kode uang dapat menjadi tempat kode uang harus pergi. Ini SRP sederhana, jadi saya tidak yakin apa yang Anda maksud ketika Anda mengatakannya "hanya" menggeser masalah. Itulah yang dilakukan semua fungsi. Mereka tidak membuat kode hilang, mereka memindahkannya ke tempat yang lebih tepat. Apa masalah Anda dengan itu? "tapi selamat saya, setidaknya 5 upvoters mengambil umpan" Ini terdengar jauh lebih bajingan daripada yang saya kira (harapan) yang Anda maksudkan.
Alexander - Pasang kembali Monica
@ Falco Kedengarannya seperti metode baru bagi saya (pada kelas kalkulator pembayaran ini, tidak DateInterval):calculatePayPeriod(startData, totalPayment, monthlyPayment)
Alexander - Reinstate Monica
7

Terkadang ekspresi lancar membantu dalam hal ini:

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

Diberi cukup waktu untuk mendesain, Anda dapat membuat API solid yang bertindak serupa dengan bahasa khusus domain.

Keuntungan besar lainnya adalah bahwa IDE dengan autocomplete membuat hampir tidak menarik untuk membaca dokumentasi API, seperti intuitif karena kemampuannya yang dapat ditemukan sendiri.

Ada sumber daya di luar sana seperti https://nikas.praninskas.com/javascript/2015/04/26/fluent-javascript/ atau https://github.com/nikaspran/fluent.js tentang topik ini.

Contoh (diambil dari tautan sumber daya pertama):

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]
DanielCuadra
sumber
8
Antarmuka yang lancar dengan sendirinya tidak membuat tugas tertentu lebih mudah atau sulit. Ini lebih mirip pola Builder.
VLAZ
8
Implementasinya akan agak rumit meskipun jika Anda perlu mencegah panggilan yang salah sepertiforTotalAmount(1234).breakIntoPayments().byPeriod(2).monthly().withPaymentsOf(12.34).byDateRange(saleStart, saleEnd);
Bergi
4
Jika pengembang benar-benar ingin menembak dengan kaki mereka, ada cara yang lebih mudah @Bergi. Namun, contoh yang Anda berikan jauh lebih mudah dibaca daripadaforTotalAmountAndBreakIntoPaymentsByPeriodThenMonthlyWithPaymentsOfButByDateRange(1234, 2, 12.34, saleStart, saleEnd);
DanielCuadra
5
@DanielCuadra Poin yang saya coba buat adalah bahwa jawaban Anda tidak benar-benar memecahkan masalah OPs memiliki 3 parameter yang saling eksklusif. Menggunakan pola builder mungkin membuat panggilan lebih mudah dibaca (dan meningkatkan kemungkinan pengguna memperhatikan bahwa itu tidak masuk akal), tetapi menggunakan pola builder saja tidak mencegah mereka dari masih melewati 3 nilai sekaligus.
Bergi
2
@ Falco Akankah? Ya, itu mungkin, tetapi lebih rumit, dan jawabannya tidak menyebutkan hal ini. Pembangun yang lebih umum yang saya lihat hanya terdiri dari satu kelas. Jika jawabannya diedit untuk memasukkan kode pembuat, saya dengan senang hati akan menyetujuinya dan menghapus downvote saya.
Bergi
2

Nah, dalam bahasa lain, Anda akan menggunakan parameter bernama . Ini dapat ditiru dalam Javscript:

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

getPaymentBreakdown(100, today, {endDate: whatever});
getPaymentBreakdown(100, today, {noOfMonths: 4});
getPaymentBreakdown(100, today, {monthlyPayment: 20});
Gregory Currie
sumber
6
Seperti pola pembuat di bawah ini, ini membuat panggilan lebih mudah dibaca (dan meningkatkan kemungkinan pengguna memperhatikan bahwa itu tidak masuk akal), tetapi penamaan parameter tidak mencegah pengguna dari masih melewati 3 nilai sekaligus - misalnya getPaymentBreakdown(100, today, {endDate: whatever, noOfMonths: 4, monthlyPayment: 20}).
Bergi
1
Bukankah seharusnya :bukan =?
Barmar
Saya kira Anda dapat memeriksa bahwa hanya salah satu parameternya yang bukan nol (atau tidak ada dalam kamus).
Mateen Ulhaq
1
@Bergi - Sintaksnya sendiri tidak mencegah pengguna melewati parameter yang tidak masuk akal tetapi Anda cukup melakukan beberapa validasi dan melempar kesalahan
slebetman
@Bergi Saya bukan ahli Javascript, tapi saya pikir Penugasan Destrukturisasi di ES6 dapat membantu di sini, meskipun saya sangat sedikit pengetahuan tentang ini.
Gregory Currie
1

Sebagai alternatif, Anda juga dapat memutuskan tanggung jawab menentukan jumlah bulan dan meninggalkannya di luar fungsi Anda:

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

Dan getpaymentBreakdown akan menerima objek yang akan memberikan jumlah bulan dasar

Mereka akan urutan fungsi yang lebih tinggi mengembalikan misalnya fungsi.

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)
  };
}
Vinz243
sumber
Apa yang terjadi pada parameter totaldan startDate?
Bergi
Ini sepertinya API yang bagus, tetapi bisakah Anda menambahkan bagaimana Anda membayangkan keempat fungsi itu untuk diimplementasikan? (Dengan tipe varian dan antarmuka umum ini bisa sangat elegan, tetapi tidak jelas apa yang ada dalam pikiran Anda).
Bergi
@Bergi mengedit posting saya
Vinz243
0

Dan jika Anda bekerja dengan sistem dengan tipe data serikat / aljabar yang dibedakan, Anda dapat meneruskannya sebagai, katakanlah, a TimePeriodSpecification.

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

dan kemudian tidak ada masalah akan terjadi di mana Anda bisa gagal untuk benar-benar mengimplementasikannya dan seterusnya.

NiklasJ
sumber
Ini jelas bagaimana saya akan mendekati ini dalam bahasa yang memiliki jenis seperti ini tersedia. Saya mencoba untuk menjaga bahasa agnostik pertanyaan saya tetapi mungkin harus mempertimbangkan bahasa yang digunakan karena pendekatan seperti ini menjadi mungkin dalam beberapa kasus.
CalMlynarczyk