มีรูปแบบการจัดการพารามิเตอร์ฟังก์ชันที่ขัดแย้งหรือไม่?


38

เรามีฟังก์ชัน API ที่แบ่งจำนวนเงินทั้งหมดเป็นจำนวนเงินรายเดือนตามวันที่เริ่มต้นและวันที่สิ้นสุดที่กำหนด

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

เมื่อเร็ว ๆ นี้ผู้บริโภคสำหรับ API นี้ต้องการระบุช่วงวันที่ด้วยวิธีอื่น: 1) โดยระบุจำนวนเดือนแทนที่จะเป็นวันที่สิ้นสุดหรือ 2) โดยระบุการชำระเงินรายเดือนและการคำนวณวันที่สิ้นสุด เพื่อตอบสนองต่อสิ่งนี้ทีม API ได้เปลี่ยนฟังก์ชั่นดังต่อไปนี้:

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

ฉันรู้สึกว่าการเปลี่ยนแปลงนี้ทำให้ API ซับซ้อนขึ้น ตอนนี้โทรจำเป็นต้องกังวลเกี่ยวกับการวิเคราะห์พฤติกรรมที่ซ่อนอยู่กับการดำเนินการของฟังก์ชันในการกำหนดค่าพารามิเตอร์ที่ใช้ในการตั้งค่าถูกนำมาใช้ในการคำนวณช่วงวันที่ (เช่นโดยลำดับความสำคัญmonthlyPayment, numMonths, endDate) หากผู้โทรไม่สนใจลายเซ็นของฟังก์ชั่นพวกเขาอาจส่งพารามิเตอร์ทางเลือกหลายรายการและสับสนว่าเหตุใดจึงendDateถูกเพิกเฉย เราระบุพฤติกรรมนี้ในเอกสารประกอบการใช้งาน

นอกจากนี้ฉันรู้สึกว่ามันเป็นแบบอย่างที่ไม่ดีและเพิ่มความรับผิดชอบให้กับ API ที่ไม่ควรเกี่ยวข้องกับตัวเอง (เช่นการละเมิด SRP) สมมติว่าผู้บริโภคเพิ่มเติมต้องการฟังก์ชั่นเพื่อรองรับกรณีการใช้งานที่มากขึ้นเช่นการคำนวณtotalจากnumMonthsและmonthlyPaymentพารามิเตอร์ ฟังก์ชั่นนี้จะซับซ้อนขึ้นเรื่อย ๆ เมื่อเวลาผ่านไป

การตั้งค่าของฉันคือการรักษาฟังก์ชั่นตามเดิมและต้องการให้ผู้เรียกคำนวณendDateเอง อย่างไรก็ตามฉันอาจผิดและสงสัยว่าการเปลี่ยนแปลงที่ทำนั้นเป็นวิธีที่ยอมรับได้ในการออกแบบฟังก์ชัน API หรือไม่

อีกวิธีหนึ่งมีรูปแบบทั่วไปสำหรับจัดการสถานการณ์เช่นนี้หรือไม่ เราสามารถจัดทำฟังก์ชันการสั่งซื้อเพิ่มเติมที่สูงขึ้นใน API ของเราซึ่งห่อฟังก์ชันดั้งเดิมไว้ แต่สิ่งนี้จะทำให้ API ขยายตัว บางทีเราอาจเพิ่มพารามิเตอร์การตั้งค่าสถานะเพิ่มเติมเพื่อระบุวิธีการที่จะใช้ภายในฟังก์ชัน


79
"เมื่อเร็ว ๆ นี้ผู้บริโภคสำหรับ API นี้ต้องการ [ระบุ] จำนวนเดือนแทนที่จะเป็นวันที่สิ้นสุด" - นี่เป็นคำขอที่ไม่สำคัญ พวกเขาสามารถแปลง # ของเดือนเป็นวันที่สิ้นสุดที่เหมาะสมในบรรทัดหนึ่งหรือสองของรหัสในตอนท้ายของพวกเขา
เกรแฮม

12
ที่ดูเหมือนว่ารูปแบบการต่อต้าน Flag Argument และฉันอยากจะแนะนำให้แบ่งออกเป็นหลายฟังก์ชั่น
njzk2

2
ตามบันทึกข้างมีมีฟังก์ชั่นที่สามารถรับชนิดเดียวกันและจำนวนค่าพารามิเตอร์และให้ผลลัพธ์ที่แตกต่างกันมากขึ้นอยู่กับที่ - ดูDate- คุณสามารถจัดหาสตริงและมันสามารถแยกวิเคราะห์เพื่อกำหนดวัน อย่างไรก็ตามวิธีนี้การจัดการพารามิเตอร์ยังทำได้อย่างพิถีพิถันและอาจให้ผลลัพธ์ที่ไม่น่าเชื่อถือ ดูDateอีกครั้ง มันเป็นไปไม่ได้ที่จะทำสิ่งที่ถูกต้อง - ช่วงเวลาจัดการได้ดีกว่า แต่ก็น่ารำคาญที่จะใช้โดยไม่คำนึงถึง
VLAZ

ในการสัมผัสกันเล็กน้อยคุณอาจต้องการคิดเกี่ยวกับวิธีจัดการกับกรณีที่monthlyPaymentได้รับ แต่totalไม่ใช่จำนวนเต็มหลายตัว และวิธีจัดการกับข้อผิดพลาดจุดลอยตัวที่เป็นไปได้หากค่าไม่ได้รับประกันว่าจะเป็นจำนวนเต็ม (เช่นลองด้วยtotal = 0.3และmonthlyPayment = 0.1)
Ilmari Karonen

@Graham ฉันไม่ได้ตอบสนองต่อสิ่งนั้น ... ฉันตอบสนองต่อคำสั่งต่อไป"ในการตอบสนองนี้ทีม API ได้เปลี่ยนฟังก์ชั่น ... " - ม้วนเข้าสู่ตำแหน่งของทารกในครรภ์และเริ่มโยก - มันไม่สำคัญว่าที่ไหน บรรทัดนั้นหรือโค้ดสองอันไปไม่ว่าจะเป็นการเรียก API ใหม่ด้วยรูปแบบที่แตกต่างกันหรือเมื่อสิ้นสุดการโทร อย่าเปลี่ยนการเรียก API ที่ใช้งานได้เช่นนี้!
Baldrickk

คำตอบ:


99

เห็นการใช้งานปรากฏให้ฉันสิ่งที่คุณต้องการจริงๆที่นี่เป็น 3 ฟังก์ชั่นที่แตกต่างกันแทนหนึ่ง:

ฉบับดั้งเดิม:

function getPaymentBreakdown(total, startDate, endDate) 

หนึ่งที่ให้จำนวนเดือนแทนวันที่สิ้นสุด:

function getPaymentBreakdownByNoOfMonths(total, startDate, noOfMonths) 

และอีกหนึ่งรายการที่ให้การชำระเงินรายเดือนและการคำนวณวันที่สิ้นสุด:

function getPaymentBreakdownByMonthlyPayment(total, startDate, monthlyPayment) 

ตอนนี้ไม่มีพารามิเตอร์ที่เป็นทางเลือกอีกต่อไปแล้วและควรจะค่อนข้างชัดเจนว่าฟังก์ชั่นใดเรียกว่าอย่างไรและเพื่อจุดประสงค์ใด ดังที่ได้กล่าวไว้ในความคิดเห็นในภาษาที่พิมพ์อย่างเคร่งครัดเราสามารถใช้ฟังก์ชั่นโอเวอร์โหลดได้โดยแยกฟังก์ชั่นที่แตกต่างกัน 3 อย่างออกจากกันโดยไม่จำเป็นต้องใช้ชื่อ แต่ใช้ลายเซ็นของพวกเขาในกรณีนี้

โปรดทราบว่าฟังก์ชั่นที่แตกต่างกันไม่ได้หมายความว่าคุณต้องทำซ้ำตรรกะใด ๆ - ภายในถ้าฟังก์ชั่นเหล่านี้ใช้อัลกอริทึมร่วมกันก็ควรได้รับการ refactored เพื่อฟังก์ชั่น "ส่วนตัว"

มีรูปแบบทั่วไปสำหรับจัดการสถานการณ์เช่นนี้หรือไม่

ฉันไม่คิดว่าจะมี "รูปแบบ" (ในแง่ของรูปแบบการออกแบบ GoF) ซึ่งอธิบายการออกแบบ API ที่ดี การใช้ชื่ออธิบายตนเองฟังก์ชั่นที่มีพารามิเตอร์น้อยลงฟังก์ชั่นที่มีพารามิเตอร์ orthogonal (= อิสระ) เป็นเพียงหลักการพื้นฐานของการสร้างรหัสที่อ่านง่ายบำรุงรักษาและพัฒนาได้ ไม่ใช่ทุกความคิดที่ดีในการเขียนโปรแกรมนั้นจำเป็นต้องเป็น "รูปแบบการออกแบบ"


24
ที่จริงแล้วการใช้งาน "ทั่วไป" ของรหัสอาจเป็นเพียงแค่getPaymentBreakdown(หรือหนึ่งในสามเหล่านั้น) และอีกสองฟังก์ชันเพียงแค่แปลงอาร์กิวเมนต์และเรียกสิ่งนั้น ทำไมจึงเพิ่มฟังก์ชั่นส่วนตัวที่เป็นสำเนาที่สมบูรณ์แบบของหนึ่งในสามเหล่านี้
Giacomo Alzetta

@GiacomoAlzetta: นั่นเป็นไปได้ แต่ผมค่อนข้างมั่นใจว่าการดำเนินการจะกลายเป็นง่ายโดยการให้ฟังก์ชั่นทั่วไปที่มีเพียง "กลับ" เป็นส่วนหนึ่งของฟังก์ชั่นตรวจการณ์และให้ประชาชน 3 ฟังก์ชั่นเรียกใช้ฟังก์ชันนี้มีพารามิเตอร์innerNumMonths, และtotal startDateทำไมต้องมีฟังก์ชั่นที่ซับซ้อนมากกว่า 5 พารามิเตอร์โดยที่ 3 เป็นตัวเลือกที่เกือบจะเป็นทางเลือก (ยกเว้นต้องตั้งค่าหนึ่งตัว) เมื่อฟังก์ชั่น 3 พารามิเตอร์จะทำงานเช่นกัน?
Doc Brown

3
ฉันไม่ได้ตั้งใจจะพูดว่า "เก็บฟังก์ชันอาร์กิวเมนต์ 5 ตัว" ไว้ ฉันแค่บอกว่าเมื่อคุณมีบางตรรกะทั่วไปตรรกะนี้ไม่จำเป็นต้องเป็นส่วนตัว ในกรณีนี้ทั้ง 3 ฟังก์ชั่นสามารถถูก refactored เพียงแค่เปลี่ยนพารามิเตอร์เป็นวันที่เริ่มต้นดังนั้นคุณสามารถใช้getPaymentBreakdown(total, startDate, endDate)ฟังก์ชั่นสาธารณะเป็นการใช้งานร่วมกันเครื่องมืออื่น ๆ ก็จะคำนวณวันที่รวม / เริ่มต้น / สิ้นสุดที่เหมาะสมและเรียกมัน
Giacomo Alzetta

@GiacomoAlzetta: ตกลงเป็นความเข้าใจผิดฉันคิดว่าคุณกำลังพูดถึงการดำเนินการที่สองของgetPaymentBreakdownคำถาม
Doc Brown

ฉันจะเพิ่มการใช้เวอร์ชันดั้งเดิมของวิธีการดั้งเดิมที่เรียกว่า 'getPaymentBreakdownByStartAndEnd' อย่างชัดเจนและยกเลิกวิธีการเดิมหากคุณต้องการให้สิ่งเหล่านี้ทั้งหมด
Erik

20

นอกจากนี้ฉันรู้สึกว่ามันเป็นแบบอย่างที่ไม่ดีและเพิ่มความรับผิดชอบให้กับ API ที่ไม่ควรเกี่ยวข้องกับตัวเอง (เช่นการละเมิด SRP) สมมติว่าผู้บริโภคเพิ่มเติมต้องการฟังก์ชั่นเพื่อรองรับกรณีการใช้งานที่มากขึ้นเช่นการคำนวณtotalจากnumMonthsและmonthlyPaymentพารามิเตอร์ ฟังก์ชั่นนี้จะซับซ้อนขึ้นเรื่อย ๆ เมื่อเวลาผ่านไป

คุณพูดถูก

การตั้งค่าของฉันคือการรักษาฟังก์ชั่นตามเดิมและต้องการให้ผู้เรียกคำนวณ endDate แทน อย่างไรก็ตามฉันอาจผิดและสงสัยว่าการเปลี่ยนแปลงที่ทำนั้นเป็นวิธีที่ยอมรับได้ในการออกแบบฟังก์ชัน API หรือไม่

วิธีนี้ไม่เหมาะเช่นกันเนื่องจากรหัสผู้โทรจะถูกปนเปื้อนด้วยแผ่นหม้อน้ำที่ไม่เกี่ยวข้อง

อีกวิธีหนึ่งมีรูปแบบทั่วไปสำหรับจัดการสถานการณ์เช่นนี้หรือไม่

DateIntervalแนะนำรูปแบบใหม่เช่น เพิ่มตัวสร้างสิ่งที่เหมาะสม (วันที่เริ่มต้น + วันที่สิ้นสุด, วันที่เริ่มต้น + จำนวนเดือน, อะไรก็ตาม) ใช้สิ่งนี้เป็นประเภทสกุลเงินทั่วไปสำหรับการแสดงช่วงวันที่ / เวลาทั่วทั้งระบบของคุณ


3
@DocBrown ใช่ ในกรณีเช่นนี้ (Ruby, Python, JS) เป็นธรรมเนียมที่ต้องใช้เมธอด static / class แต่นั่นเป็นรายละเอียดการใช้งานซึ่งฉันไม่คิดว่าเกี่ยวข้องกับประเด็นคำตอบของฉันโดยเฉพาะ ("ใช้ประเภท")
Alexander

2
และความคิดนี้ถึงขีด จำกัด ด้วยข้อกำหนดที่สาม: วันที่เริ่มต้นการชำระเงินทั้งหมดและการชำระเงินรายเดือน - และฟังก์ชันจะคำนวณ DateInterval จากพารามิเตอร์เงิน - และคุณไม่ควรใส่จำนวนเงินลงในช่วงวันที่ของคุณ ...
Falco

3
@DocBrown "เพียงแค่เปลี่ยนปัญหาจากฟังก์ชั่นที่มีอยู่ไปยังตัวสร้างของประเภท" ใช่มันคือการใส่รหัสเวลาที่รหัสเวลาควรจะไปเพื่อให้รหัสเงินสามารถเป็นที่ที่รหัสเงินควรไป มันง่าย SRP ดังนั้นฉันไม่แน่ใจว่าสิ่งที่คุณได้รับเมื่อคุณพูดว่า "เพียง" เปลี่ยนปัญหา นั่นคือสิ่งที่ทุกฟังก์ชั่นทำ พวกเขาไม่ทำให้รหัสหายไปพวกเขาย้ายมันไปยังที่ที่เหมาะสมกว่า ปัญหาของคุณคืออะไร "แต่ขอแสดงความยินดีอย่างน้อย 5 upvoters เอาเหยื่อ" นี่ฟังดูไอ้มากขึ้นกว่าที่ฉันคิด (หวัง) คุณตั้งใจ
Alexander

@Falco ฟังดูเหมือนวิธีการใหม่สำหรับฉัน (ในระดับเครื่องคิดเลขการชำระเงินนี้ไม่ใช่DateInterval):calculatePayPeriod(startData, totalPayment, monthlyPayment)
Alexander

7

บางครั้งการแสดงออกอย่างคล่องแคล่วช่วยในเรื่องนี้:

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

ให้เวลาในการออกแบบเพียงพอคุณสามารถสร้าง API ที่เป็นของแข็งซึ่งคล้ายกับภาษาเฉพาะโดเมน

ข้อได้เปรียบที่สำคัญอีกประการหนึ่งคือ IDE ที่มีการเติมข้อความอัตโนมัติทำให้แทบจะไม่เกี่ยวข้องกับการอ่านเอกสาร API เนื่องจากมันใช้งานง่ายเนื่องจากความสามารถในการค้นพบตัวเอง

มีแหล่งข้อมูลอยู่เช่นhttps://nikas.praninskas.com/javascript/2015/04/26/fluent-javascript/หรือhttps://github.com/nikaspran/fluent.jsในหัวข้อนี้

ตัวอย่าง (นำมาจากลิงค์ทรัพยากรแรก):

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
อินเตอร์เฟซได้อย่างคล่องแคล่วด้วยตัวเองไม่ได้ทำให้งานใด ๆ ง่ายขึ้นหรือยาก ดูเหมือนว่าจะเป็นรูปแบบตัวสร้าง
VLAZ

8
การดำเนินการจะค่อนข้างซับซ้อนแม้ว่าคุณจะต้องป้องกันการโทรผิดเช่นforTotalAmount(1234).breakIntoPayments().byPeriod(2).monthly().withPaymentsOf(12.34).byDateRange(saleStart, saleEnd);
Bergi

4
หากนักพัฒนาซอฟต์แวร์ต้องการยิงด้วยเท้าจริง ๆ มีวิธีที่ง่ายกว่า @ Bergi ตัวอย่างที่คุณใส่ยังอ่านง่ายกว่าforTotalAmountAndBreakIntoPaymentsByPeriodThenMonthlyWithPaymentsOfButByDateRange(1234, 2, 12.34, saleStart, saleEnd);
DanielCuadra

5
@DanielCuadra ประเด็นที่ฉันพยายามทำก็คือคำตอบของคุณไม่ได้แก้ปัญหา OPs ของการมี 3 พารามิเตอร์พิเศษร่วมกัน การใช้รูปแบบการสร้างอาจทำให้การโทรอ่านง่ายขึ้น (และเพิ่มความน่าจะเป็นของผู้ใช้ที่สังเกตเห็นว่ามันไม่สมเหตุสมผล) แต่การใช้รูปแบบการสร้างเพียงอย่างเดียวไม่ได้ป้องกันพวกเขาจากการผ่าน 3 ค่าในครั้งเดียว
Bergi

2
@Falco มันจะ? ใช่เป็นไปได้ แต่มีความซับซ้อนมากขึ้นและคำตอบไม่ได้กล่าวถึงเรื่องนี้ ผู้สร้างทั่วไปที่ฉันเห็นมีเพียงชั้นเดียวเท่านั้น หากคำตอบได้รับการแก้ไขเพื่อรวมรหัสของผู้สร้างฉันจะรับรองและลบ downvote ของฉันอย่างมีความสุข
Bergi

2

ดีในภาษาอื่น ๆ ที่คุณต้องการใช้ชื่อพารามิเตอร์ สิ่งนี้สามารถถูกจำลองใน Javscript:

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

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

6
เช่นเดียวกับรูปแบบการสร้างที่อยู่ด้านล่างนี้จะทำให้การเรียกอ่านได้มากขึ้น (และเพิ่มความน่าจะเป็นของผู้ใช้สังเกตเห็นว่ามันไม่ได้ทำให้ความรู้สึก) แต่การตั้งชื่อพารามิเตอร์ที่ไม่ได้ป้องกันผู้ใช้จากการยังคงผ่าน 3 ค่าในครั้งเดียว - getPaymentBreakdown(100, today, {endDate: whatever, noOfMonths: 4, monthlyPayment: 20})เช่น
Bergi

1
ไม่ควรที่จะเป็น:แทน=?
Barmar

ฉันเดาว่าคุณสามารถตรวจสอบว่ามีเพียงหนึ่งในพารามิเตอร์ที่ไม่เป็นโมฆะ (หรือไม่ได้อยู่ในพจนานุกรม)
Mateen Ulhaq

1
@Bergi - ไวยากรณ์นั้นไม่ได้ป้องกันผู้ใช้จากการส่งผ่านพารามิเตอร์ไร้สาระ แต่คุณสามารถทำการตรวจสอบและโยนข้อผิดพลาดได้
บ้าง

@Bergi ฉันไม่ได้เป็นผู้เชี่ยวชาญ Javascript แต่ฉันคิดว่า Destructuring Assignment ใน ES6 สามารถช่วยได้ที่นี่ แต่ฉันรู้เรื่องนี้น้อยมาก
Gregory Currie

1

นอกจากนี้คุณยังสามารถแบ่งความรับผิดชอบในการระบุจำนวนเดือนและแยกออกจากการทำงานของคุณ:

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

และ getpaymentBreakdown จะได้รับวัตถุซึ่งจะให้จำนวนฐานของเดือน

ฟังก์ชั่นการสั่งซื้อที่สูงขึ้นจะส่งคืนเช่นฟังก์ชัน

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

เกิดอะไรขึ้นกับพารามิเตอร์totalและstartDate?
Bergi

ดูเหมือนว่าจะเป็น API ที่ดี แต่คุณสามารถเพิ่มวิธีที่จะจินตนาการถึงสี่ฟังก์ชั่นที่จะใช้งานได้หรือไม่ (ด้วยประเภทของตัวแปรและอินเทอร์เฟซทั่วไปนี่อาจจะค่อนข้างหรูหรา แต่ก็ไม่มีความชัดเจนในสิ่งที่คุณมีในใจ)
Bergi

@Bergi แก้ไขโพสต์ของฉัน
Vinz243

0

และถ้าคุณกำลังจะทำงานร่วมกับระบบที่มีการเลือกปฏิบัติสหภาพแรงงาน / TimePeriodSpecificationชนิดข้อมูลเกี่ยวกับพีชคณิตคุณสามารถส่งต่อไปในขณะที่การพูด

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

และจากนั้นจะไม่มีปัญหาใด ๆ เกิดขึ้นในกรณีที่คุณไม่สามารถนำไปใช้งานได้จริง


นี่เป็นวิธีที่ฉันจะเข้าใกล้ในภาษาที่มีประเภทเช่นนี้ ฉันพยายามเก็บคำถามที่ไม่เชื่อเรื่องภาษาไว้ แต่อาจจะคำนึงถึงภาษาที่ใช้เพราะวิธีการเช่นนี้เป็นไปได้ในบางกรณี
CalMlynarczyk
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.