Çakışan işlev parametrelerini işlemek için bir kalıp var mı?


38

Verilen başlangıç ​​ve bitiş tarihlerine göre toplam tutarı aylık tutarlara ayıran bir API fonksiyonuna sahibiz.

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

Son zamanlarda, bu API için bir tüketici tarih aralığını başka yollarla belirtmek istedi: 1) bitiş tarihi yerine ay sayısını vererek veya 2) aylık ödemeyi sağlayarak ve bitiş tarihini hesaplayarak. Buna cevaben, API ekibi işlevi şu şekilde değiştirdi:

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

Bu değişikliğin API'yi karmaşıklaştırdığını hissediyorum. Şimdi Arayan parametreleri (öncelik sırasına göre, yani tarih aralığını hesaplamak için kullanılan içinde tercihini aldığı belirlenmesinde işlevin uygulanması ile gizli sezgisel hakkında endişe gerekiyor monthlyPayment, numMonths, endDate). Arayan, işlev imzasına dikkat etmezse, isteğe bağlı parametrelerin çoğunu gönderebilir ve neden endDategöz ardı edildiğine ilişkin olarak kafaları karışabilir . Bu davranışı işlev belgelerinde belirtiyoruz.

Ayrıca kendisinin kötü bir emsal teşkil ettiğini ve API'ya kendisiyle ilgilenmemesi gereken sorumluluklar eklediğini hissediyorum (yani SRP'yi ihlal ediyor). Varsayalım ek tüketiciler fonksiyonu böyle hesaplanması gibi daha kullanım durumlarını, desteklemek istiyorsanız totalden numMonthsve monthlyPaymentparametreler. Bu işlev zamanla daha karmaşık hale gelecektir.

Tercihim, işlevi olduğu gibi tutmak ve bunun yerine arayanın endDatekendilerini hesaplamasını gerektiriyor . Ancak yanılıyor olabilirim ve yaptıkları değişikliklerin bir API işlevi tasarlamanın kabul edilebilir bir yolu olup olmadığını merak ediyorum.

Alternatif olarak, böyle senaryoları ele almak için ortak bir örnek var mı? API’mizde orijinal işlevi saran daha yüksek dereceli işlevler sağlayabiliriz, ancak bu API'yi engeller. Belki de, işlevin içinde hangi yaklaşımı kullanacağını belirten ek bir flag parametresi ekleyebiliriz.


79
"Son zamanlarda, bu API için bir tüketici, bitiş tarihi yerine ay sayısını belirtmek istedi" - Bu anlamsız bir istek. Ay sayısını, bir satırdaki veya bir koddaki iki satırdaki uygun bir bitiş tarihine dönüştürebilirler.
Graham

12
Flag Argument anti-
pattern'e

2
Bir yan not olarak, orada olan parametrelerin aynı tür ve numarayı kabul edip dayanan çok farklı sonuçlar üretebilir fonksiyonlar - bkz Date- Bir dize sağlayabilirsiniz ve tarihini belirlemek için çözümlenebilir. Bununla birlikte, bu şekilde işlem parametreleri de çok hassastır ve güvenilir olmayan sonuçlar doğurabilir. DateTekrar gör . Doğru yapmak imkansız değildir - Moment daha iyi işler ancak ne olursa olsun kullanmak çok can sıkıcıdır.
VLAZ

Hafif bir teğet üzerinde monthlyPayment, verilen davayı nasıl ele alacağınız hakkında düşünmek isteyebilirsiniz, ancak totaltamsayı değildir. Ayrıca, değerlerin tamsayı olmaları garanti edilmiyorsa, olası kayan nokta yuvarlama hatalarıyla nasıl başa çıkılacağı (örn . total = 0.3Ve ile deneyin monthlyPayment = 0.1).
Ilmari Karonen

@Graham Buna tepki vermedim ... Bir sonraki ifadeye tepki gösterdim "Buna cevaben, API ekibi işlevi değiştirdi ..." - cenin pozisyonuna dönüyor ve sallanmaya başlıyor - Nerede olduğu önemli değil bu satır veya iki kod, farklı bir formata sahip yeni bir API çağrısı veya arayan ucunda yapılır. Sadece çalışan bir API çağrısını böyle değiştirmeyin!
Baldrickk

Yanıtlar:


99

Uygulamayı görünce, bana burada gerçekten ihtiyacınız olan şey, biri yerine 3 farklı fonksiyon:

Orijinal olanı:

function getPaymentBreakdown(total, startDate, endDate) 

Bitiş tarihi yerine ay sayısını sağlayan kişi:

function getPaymentBreakdownByNoOfMonths(total, startDate, noOfMonths) 

ve aylık ödemeyi sağlayan ve bitiş tarihini hesaplayan:

function getPaymentBreakdownByMonthlyPayment(total, startDate, monthlyPayment) 

Şimdi, artık isteğe bağlı parametreler yoktur ve hangi işlevin nasıl ve hangi amaç için adlandırıldığı oldukça açık olmalıdır. Yorumlarda da belirtildiği gibi, kesinlikle yazılmış bir dilde, biri amaçlarına uymamak kaydıyla 3 farklı işlevi, adlarıyla değil, imzalarıyla ayırt ederek, fonksiyon aşırı yüklemesi de kullanılabilir.

Farklı işlevlerin herhangi bir mantığı çoğaltmanız gerektiği anlamına gelmediğine dikkat edin - dahili olarak, bu işlevler ortak bir algoritmayı paylaşırsa, "özel" bir işleve yeniden yansıtılmalıdır.

Bu gibi senaryoları işlemek için ortak bir desen var mı

İyi API tasarımını tanımlayan bir “kalıp” (GoF tasarım kalıpları anlamında) olduğunu sanmıyorum. Kendini tarif eden isimler kullanarak, daha az parametreli fonksiyonlar, dik (= bağımsız) parametreli fonksiyonlar, okunabilir, bakım yapılabilir ve geliştirilebilir kod yaratmanın temel ilkeleridir. Programlamadaki her iyi fikir mutlaka bir "tasarım deseni" değildir.


24
Aslında kodun "ortak" uygulaması basitçe getPaymentBreakdown(veya gerçekten bu 3'ten herhangi biri) olabilir ve diğer iki işlev de sadece argümanları dönüştürür ve bunu söyler. Neden bu 3 taneden birinin mükemmel bir kopyası olan özel bir işlev eklemelisiniz?
Giacomo Alzetta

@GiacomoAlzetta: bu mümkün. Ama oldukça emin uygulama OP işlevinin sadece "dönüş" bölümünü içeren ortak bir işlev sağlayarak daha basit hale gelir ve kamu 3 fonksiyonlar parametrelerle bu işlevi diyelim olacak duyuyorum innerNumMonths, totalve startDate. 3 parametreli bir işlev de işi yapacaksa neden 3 parametrenin neredeyse 3 isteğe bağlı olduğu (birinin ayarlanması gerekmeksizin) olduğu 5 parametreli aşırı karmaşık bir işlev kalsın?
Doktor Brown

3
"5 argüman işlevini tut" demek istemedim. Sadece ortak bir mantığınız olduğunda bu mantığın özel olması gerekmediğini söylüyorum . Bu durumda, 3 fonksiyonun tümü parametrelerin basitçe başlangıç-bitiş tarihlerine göre düzenlenmesi için yeniden yapılandırılabilir, böylece ortak getPaymentBreakdown(total, startDate, endDate)işlevi ortak uygulama olarak kullanabilirsiniz , diğer araç sadece uygun toplam / başlangıç ​​/ bitiş tarihlerini hesaplar ve çağırır.
Giacomo Alzetta

@GiacomoAlzetta: tamam, bir yanlış anlaşılma getPaymentBreakdownoldu, soruyu ikinci uygulamasından bahsettiğinizi düşündüm .
Doktor Brown

Bunları sağlamak istiyorsanız, açıkça 'getPaymentBreakdownByStartAndEnd' adlı orijinal yöntemin yeni bir sürümünü eklemeye ve orijinal yöntemi kullanımdan kaldırmaya kadar giderim.
Erik

20

Ayrıca kendisinin kötü bir emsal teşkil ettiğini ve API'ya kendisiyle ilgilenmemesi gereken sorumluluklar eklediğini hissediyorum (yani SRP'yi ihlal ediyor). Varsayalım ek tüketiciler fonksiyonu böyle hesaplanması gibi daha kullanım durumlarını, desteklemek istiyorsanız totalden numMonthsve monthlyPaymentparametreler. Bu işlev zamanla daha karmaşık hale gelecektir.

Tamamen haklısın.

Tercihim, işlevi olduğu gibi tutmak ve yerine arayanın bitiş tarihini hesaplamasını gerektiriyor. Ancak yanılıyor olabilirim ve yaptıkları değişikliklerin bir API işlevi tasarlamanın kabul edilebilir bir yolu olup olmadığını merak ediyordum.

Bu da ideal değil, çünkü arayan kodu ilgisiz kazan plakasıyla kirlenecek.

Alternatif olarak, böyle senaryoları ele almak için ortak bir örnek var mı?

Gibi yeni bir tür tanıtın DateInterval. Yapıcıların ne anlama geldiğini ekleyin (başlangıç ​​tarihi + bitiş tarihi, başlangıç ​​tarihi + num ay, ne olursa olsun). Bunu, sisteminizdeki tarih / saat aralıklarını ifade etmek için kullanılan ortak para birimi türleri olarak kabul edin.


3
@DocBrown Yep. Bu gibi durumlarda (Ruby, Python, JS), sadece statik / sınıf yöntemlerini kullanmak gelenekseldir. Ancak bu bir uygulama detayıdır, özellikle de cevabımın konuyla ilgili olduğunu sanmıyorum ("bir tür kullanın").
Alexander

2
Ve bu fikir ne yazık ki üçüncü şartla sınırlarına ulaşıyor: Başlangıç ​​Tarihi, toplam Ödeme ve aylık Ödeme - ve fonksiyon DateInterval değerini para parametrelerinden hesaplayacak - ve parasal tutarları tarih aralığınıza koymamalısınız ...
Falco

3
@DocBrown "sorunu yalnızca varolan işlevden, türünün yapıcısına kaydır" "Evet, zaman kodunun nereye gitmesi gerektiği zaman kodunu koyar, böylece para kodu para kodunun nereye gitmesi gerektiğidir. Çok basit bir SRP, bu yüzden "sadece" derken sorunu çözdüğünüzde neyin elde edildiğinden emin değilim. Tüm işlevler böyle yapar. Kodları ortadan kaldırmazlar, daha uygun yerlere taşırlar. Bununla ilgili derdin ne? "ama tebriklerim, en az 5 işgalci yem aldı" Bu düşündüğümden (umut) düşündüğümden çok daha fazla salakça geliyor.
Alexander

@Falco Bana yeni bir yöntem gibi geliyor (bu hesap makinesi dersinde, değil DateInterval):calculatePayPeriod(startData, totalPayment, monthlyPayment)
Alexander

7

Bazen akıcı ifadeler bu konuda yardımcı olur:

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

Tasarım için yeterli zaman verildiğinde, etki alanına özgü bir dile benzeyen sağlam bir API ile karşılaşabilirsiniz.

Diğer büyük avantaj, otomatik tamamlama özelliğine sahip IDE'lerin kendi kendini keşfedilebilen yeteneklerinden dolayı sezgisel olduğu gibi API belgelerini okumak için neredeyse hiç tereddüt etmemesidir.

Bu konuda https://nikas.praninskas.com/javascript/2015/04/26/fluent-javascript/ veya https://github.com/nikaspran/fluent.js gibi kaynaklar var .

Örnek (ilk kaynak bağlantısından alınmıştır):

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
Akıcı arayüz kendi başına herhangi bir görevi daha kolay veya daha zor yapmaz. Bu, Oluşturucu deseni gibi görünüyor.
VLAZ

8
Gibi yanlış çağrıları önlemeniz gerekiyorsa, uygulama oldukça karmaşık olurduforTotalAmount(1234).breakIntoPayments().byPeriod(2).monthly().withPaymentsOf(12.34).byDateRange(saleStart, saleEnd);
Bergi

4
Geliştiriciler gerçekten ayaklarının üzerinde ateş etmek isterlerse, @Bergi'nin daha kolay yolları vardır. Yine de, koyduğunuz örnek çok daha okunaklıforTotalAmountAndBreakIntoPaymentsByPeriodThenMonthlyWithPaymentsOfButByDateRange(1234, 2, 12.34, saleStart, saleEnd);
DanielCuadra

5
@DanielCuadra Yapmaya çalıştığım nokta, cevabınızın gerçekten birbirini dışlayan 3 parametreye sahip olmanın OPS sorununu çözmediğidir. Oluşturucu düzenini kullanmak aramayı daha okunabilir hale getirebilir (ve kullanıcının anlam ifade etmediğini fark etme olasılığını artırabilir), ancak yapıcı düzenini tek başına kullanmak, aynı anda 3 değeri geçmelerini engellemez.
Bergi

2
@Falco Olacak mı? Evet, mümkün, ama daha karmaşık ve cevap bundan hiç bahsetmedi. Gördüğüm en yaygın inşaatçılar sadece bir sınıftan oluşuyordu. Cevap, oluşturucunun (kodların) kodunu içerecek şekilde düzenlenirse, mutlu bir şekilde onaylayacağım ve düşük oyumu kaldıracağım.
Bergi

2

Diğer dillerde, adlandırılmış parametreler kullanırsınız . Bu Javscript'te öykünebilir:

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

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

6
Aşağıdaki üretici modeli gibi, bu da çağrıyı daha okunaklı hale getirir (ve kullanıcının mantıklı olmadığını fark etme olasılığını arttırır), ancak parametrelerin isimlendirilmesi kullanıcının aynı anda 3 değeri geçmesini engellemez - örneğin getPaymentBreakdown(100, today, {endDate: whatever, noOfMonths: 4, monthlyPayment: 20}).
Bergi

1
Bunun :yerine olmamalı mı =?
Barmar

Sanırım parametrelerden yalnızca birinin boş olmadığını (veya sözlükte olmadığını) kontrol edebilirsiniz.
Mateen Ulhaq

1
@Bergi - Sözdiziminin kendisi, kullanıcıların saçma parametreleri geçirmesini engellemez, ancak yalnızca bazı doğrulama ve hata işlemleri yapabilirsiniz
slebetman

@Bergi Ben hiçbir şekilde bir Javascript uzmanı değilim, ancak ES6'daki Atanmayı Yok Etmenin bu konuda yardımcı olabileceğimi düşünüyorum.
Gregory Currie

1

Alternatif olarak, ay sayısını belirleme sorumluluğunu da kırabilir ve onu işlevinizden çıkarabilirsiniz:

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

Ve getpaymentBreakdown, ayın temel sayısını sağlayacak bir nesne alacaktı

Bunlar, örneğin bir işlev döndüren daha yüksek dereceli işlev olacaktır.

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

totalVe startDateparametrelerine ne oldu ?
Bergi

Bu hoş bir API gibi görünüyor, ancak bu dört işlevi yerine getirmeyi nasıl hayal edeceğinizi ekler misiniz? (Varyant türleri ve ortak bir arayüz ile bu oldukça zarif olabilir, ancak aklınızdakiler net değil).
Bergi

@Bergi yazımı düzenledi
Vinz243

0

Ve eğer ayrımcı sendikalar / cebirsel veri türlerine sahip bir sistemle çalışıyor olsaydınız, a TimePeriodSpecification.

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

ve sonra gerçekte birini ve benzeri şeyleri yerine getirmede başarısız olabileceğiniz hiçbir problem yaşanmaz.


Bu kesinlikle buna benzer türleri olan bir dilde nasıl yaklaşacağım. Soru dilimi agnostik tutmaya çalıştım ama belki de kullanılan dili hesaba katmalı çünkü bu gibi yaklaşımlar bazı durumlarda mümkün olabilir.
CalMlynarczyk
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.