จำเป็นต้องทำให้โค้ดของฉันอ่านได้ง่ายขึ้นสำหรับโปรแกรมเมอร์คนอื่น ๆ ในทีมของฉัน


11

ฉันกำลังทำงานโครงการในเดลฟายและฉันกำลังสร้างตัวติดตั้งสำหรับแอปพลิเคชันมีสามส่วนหลัก

  1. การติดตั้ง / ถอนการติดตั้งPostgreSQL
  2. myapplication (การตั้งค่า myapplication ถูกสร้างขึ้นโดยใช้ nsi) การติดตั้ง / ถอนการติดตั้ง
  3. การสร้างตารางใน Postgres ผ่านสคริปต์ (แบตช์ไฟล์)

ทุกสิ่งทำงานได้อย่างราบรื่นและราบรื่น แต่หากสิ่งที่ล้มเหลวฉันได้สร้าง LogToFileger ซึ่งจะ LogToFile ทุกขั้นตอนของกระบวนการ
เช่นนี้

LogToFileToFile.LogToFile('[DatabaseInstallation]  :  [ACTION]:Postgres installation started');

ฟังก์ชั่นLogToFileToFile.LogToFile()นี้จะเขียนเนื้อหาไปยังไฟล์ มันใช้งานได้ดี แต่ปัญหาก็คือมันทำให้โค้ดยุ่งเหยิงเพราะมันยากที่จะอ่านโค้ดเพราะคนคนหนึ่งเห็นการLogToFileToFile.LogToFile()เรียกใช้ฟังก์ชั่นได้ทุกที่ในโค้ด

ตัวอย่าง

 if Not FileExists(SystemDrive+'\FileName.txt') then
 begin
    if CopyFile(PChar(FilePathBase+'FileName.txt'), PChar(SystemDrive+'\FileName.txt'), False) then
       LogToFileToFile.LogToFile('[DatabaseInstallation] :  copying FileName.txt to '+SystemDrive+'\ done')
       else
       LogToFileToFile.LogToFile('[DatabaseInstallation] :  copying FileName.txt to '+SystemDrive+'\ Failed');
 end;
 if Not FileExists(SystemDrive+'\SecondFileName.txt')      then
   begin
     if CopyFile(PChar(FilePathBase+'SecondFileName.txt'), PChar('c:\SecondFileName.txt'), False) then
       LogToFileToFile.LogToFile('[DatabaseInstallation] : copying SecondFileName.txt to '+SystemDrive+'\ done')
   else
       LogToFileToFile.LogToFile('[DatabaseInstallation] :  copying SecondFileName.txt to '+SystemDrive+'\ Failed');
 end;

อย่างที่คุณเห็นมีการLogToFileToFile.LogToFile()โทรจำนวนมาก
ก่อนหน้านี้

 if Not FileExists(SystemDrive+'\FileName.txt') then
    CopyFile(PChar(FilePathBase+'FileName.txt'), PChar(SystemDrive+'\FileName.txt'), False) 
 if Not FileExists(SystemDrive+'\SecondFileName.txt')      then
   CopyFile(PChar(FilePathBase+'SecondFileName.txt'), PChar('c:\SecondFileName.txt'), False)

นี่เป็นกรณีในรหัสทั้งหมดของฉันตอนนี้
มันยากที่จะอ่าน

ผู้ใดสามารถแนะนำให้ฉันเป็นวิธีที่ดีในการกระจายการโทรไปที่ LogToFile?

ชอบ

  1. เยื้องการเรียก 'LogToFileToFile.LogToFile () `
    เช่นนี้

       if Not FileExists(SystemDrive+'\FileName.txt') then
         begin
             if CopyFile(PChar(FilePathBase+'FileName.txt'), PChar(SystemDrive+'\FileName.txt'), False) then
            {Far away--->>}                   LogToFileToFile.LogToFile(2,'[DatabaseInstallation] :  [ACTION]:copying FileName.txt to '+SystemDrive+'\ sucessful')
       else
            {Far away--->>}                   LogToFileToFile.LogToFile(2,'[DatabaseInstallation] :  [ACTION]:copying FileName.txt to '+SystemDrive+'\ Failed');
       end;
    
  2. หน่วยแยกเช่นหน่วยLogToFileger
    นี้จะมีข้อความ LogToFile ทั้งหมดในswitch caseลักษณะนี้

     Function LogToFilegingMyMessage(LogToFilegMessage : integer)
    
     begin
    case  LogToFilegMessage of
    
    1         :  LogToFileToFile.LogToFile(2,'[DatabaseInstallation] :  [ACTION]:copying FileName.txt to '+SystemDrive+'\ sucessful');
    2         :  LogToFileToFile.LogToFile(2,'[DatabaseInstallation] :  [ACTION]:copying FileName.txt to '+SystemDrive+'\ Failed');
       150        :  LogToFileToFile.LogToFile(2,'[somthing] :  [ACTION]: somthing important);
    
    end;
    

ดังนั้นฉันสามารถเรียก LogToFilegingMyMessage (1) เมื่อต้องการ

ใครช่วยบอกฉันว่าวิธีไหนดีกว่าและสะอาดกว่าในการใช้ LogToFileging ในวิธีนี้?


5
เพื่อตอบหัวข้อของคุณ: คุณได้ลองถามทีมของคุณหรือไม่ว่าพวกเขาเข้าใจหรือไม่ ถ้าใช่ก็ควรอ่านได้ "เพียงพอ"
Spoike

@Spike: ฉันได้ถามมันยากที่จะอ่านเล็กน้อยตามที่logBook.log()พบทุกที่
PresleyDias

"มีสองส่วนหลัก" หมายเลข 1 ถึง 3 ฉันคิดว่าฉันเห็นว่าทำไมคุณมีคำถามเกี่ยวกับการอ่าน คุณอาจต้องการหาคนที่สามารถ "แก้ไข" เพื่อความมั่นคง
S.Lott

@ S.Lott ฉันแก้ไข 'สอง' เป็น 'สาม' ..ory สำหรับความผิดพลาด
PresleyDias

คุณอาจต้องการลองcodereview.stackexchange.com
Kirk Broadhurst

คำตอบ:


11

เมื่อคุณเพิ่มการบันทึกคุณแนะนำสองสิ่ง:

  1. รหัสใหญ่ขึ้นเพราะเกือบทุกการกระทำคุณเพิ่มบรรทัดที่บันทึกการกระทำนั้น (หรือความล้มเหลว)
  2. ดูเหมือนว่าท่อนซุงจะบวมและไม่อ่านง่ายเพราะใช้เนื้อที่มาก

แต่ละปัญหาเหล่านี้มีวิธีการแก้ปัญหาที่ค่อนข้างง่าย:

  1. แยกรหัสออกเป็นฟังก์ชันที่เล็กลง แทนที่จะมีฟังก์ชั่นยักษ์หนึ่งตัวที่มีสำเนาทั้งหมดของคุณรวมทั้งข้อความบันทึกข้อผิดพลาด / ความสำเร็จคุณสามารถแนะนำฟังก์ชั่น "CopyFile" ที่จะคัดลอกไฟล์เดียวและบันทึกผลลัพธ์ของตัวเอง ด้วยวิธีนี้รหัสหลักของคุณจะประกอบด้วยการโทร CopyFile และจะยังคงอ่านง่าย

  2. คุณสามารถทำให้คนตัดไม้ของคุณฉลาดขึ้น แทนที่จะส่งผ่านสตริงขนาดยักษ์ที่มีข้อมูลซ้ำ ๆ มากมายคุณสามารถส่งผ่านค่าการแจกแจงที่จะทำให้สิ่งต่าง ๆ ชัดเจนขึ้น หรือคุณสามารถกำหนดฟังก์ชั่นพิเศษของ Log () เช่น LogFileCopy, LogDbInsert ... ไม่ว่าคุณจะทำอะไรซ้ำ ๆ อีกมากมายให้พิจารณาแฟคตอริ่งนั้นลงในฟังก์ชั่นของตัวเอง

หากคุณติดตาม (1) คุณอาจมีรหัสที่มีลักษณะดังนี้:

CopyFile( sOSDrive, 'Mapannotation.txt' )
CopyFile( sOSDrive, 'Mappoints.txt' )
CopyFile( sOSDrive, 'Mapsomethingelse.txt' )
. . . .

จากนั้น CopyFile ของคุณ () เพียงแค่ต้องการโค้ดไม่กี่บรรทัดเพื่อทำการกระทำและบันทึกผลลัพธ์ของมันดังนั้นโค้ดทั้งหมดของคุณจึงกระชับและอ่านง่าย

ฉันจะอยู่ห่างจากวิธีที่คุณ # 2 ในขณะที่คุณกำลังถอดข้อมูลที่ควรอยู่ร่วมกันในโมดูลที่แตกต่างกัน คุณเพียงแค่ขอรหัสหลักของคุณให้ซิงค์กับคำสั่งบันทึกของคุณ แต่เมื่อดูที่ LogMyMessage (5) คุณจะไม่มีทางรู้ว่า

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

ฉันคิดว่านี่เป็นสิ่งที่ MainMa แนะนำ แทนที่จะผ่านสตริงที่แท้จริงกำหนดค่าคงที่ (ใน C / C ++ / C # พวกเขาจะเป็นส่วนหนึ่งของประเภทการแจงนับ enum) ตัวอย่างเช่นสำหรับส่วนประกอบคุณอาจมี: DbInstall, AppFiles, Registry, ทางลัด ... อะไรก็ตามที่ทำให้โค้ดมีขนาดเล็กลงจะทำให้อ่านง่าย

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

ดังนั้นบรรทัดการคัดลอกไฟล์ของคุณจะมีลักษณะดังนี้:

Bool isSuccess = CopyFile(PChar(sTxtpath+'Mapannotation.txt'), PChar(sOSdrive+'\Mapannotation.txt'), False)
LogBook.Log( DbInstall, FileCopy, isSuccess, 'Mapannotation.txt', sOSDrive )

* หมายเหตุไม่มีเหตุผลที่จะคัดลอก / วางสายบันทึกสองครั้งหากคุณสามารถเก็บผลลัพธ์ของการดำเนินการในตัวแปรเฉพาะที่แยกต่างหากและเพียงส่งตัวแปรนั้นไปที่ Log ()

คุณเห็นชุดรูปแบบที่นี่ใช่ไหม รหัสซ้ำซ้ำน้อยกว่า -> รหัสที่อ่านได้มากกว่า


+1 คุณสามารถบอกฉันเพิ่มเติมเกี่ยวกับเรื่องyou could pass in enumerations values นี้ได้ไหม
PresleyDias

@PresleyDias: อัปเดตโพสต์แล้ว
DXM

ตกลงได้แล้วใช่ซ้ำ ๆ น้อยกว่า -> รหัสที่อ่านได้มากขึ้น
PresleyDias

2
+1 "แบ่งรหัสออกเป็นฟังก์ชันที่เล็กลง" คุณไม่สามารถความเครียดที่เพียงพอ มันทำให้ปัญหามากมายหายไป
Oliver Weiler

10

ดูเหมือนว่าคุณจะต้องสรุปแนวคิดของ "LoggableAction" ฉันเห็นรูปแบบในตัวอย่างของคุณที่การโทรทั้งหมดส่งคืนบูลเพื่อระบุว่าสำเร็จหรือล้มเหลวและความแตกต่างเพียงอย่างเดียวคือข้อความบันทึก

เป็นเวลาหลายปีแล้วที่ฉันเขียนเดลฟายดังนั้นมันจึงเป็นรหัสหลอก c # ที่ได้แรงบันดาลใจ แต่ฉันคิดว่าคุณต้องการอะไรแบบนี้

void LoggableAction(FunctionToCallPointer, string logMessage)
{
    if(!FunctionToCallPointer)
    {  
        Log(logMessage).
    }
}

จากนั้นรหัสโทรของคุณจะกลายเป็น

if Not FileExists(sOSdrive+'\Mapannotation.txt') then
    LoggableAction(CopyFile(PChar(sTxtpath+'Mapannotation.txt'), "Oops, it went wrong")

ฉันไม่สามารถจำไวยากรณ์ Delphi สำหรับพอยน์เตอร์ของฟังก์ชั่นได้


ฉันอาจจะไปทางนี้ด้วยตัวเอง แต่ถ้าไม่ทราบเพิ่มเติมเกี่ยวกับวิธีการจัดโครงสร้างรหัสของ OP มันเป็นการยากที่จะบอกว่ามันจะดีกว่าเพียงแค่กำหนดวิธีการโทรสองสามวิธีโดยไม่เพิ่มความสับสนที่อาจเกิดขึ้นของตัวชี้วิธี ว่า OP รู้เกี่ยวกับเรื่องดังกล่าวมาก
แค่ไหน

+1 LoggableAction()นี่เป็นสิ่งที่ดีฉันสามารถเขียนค่าที่ส่งคืนโดยตรงแทนการตรวจสอบและการเขียน
PresleyDias

ฉันต้องการ 100 คำตอบที่ดี แต่ฉันสามารถตอบได้เพียงคำตอบเดียว :( .. ฉันจะลองใช้คำแนะนำนี้ในแอปพลิเคชันถัดไปของฉันขอบคุณสำหรับแนวคิด
PresleyDias

3

วิธีหนึ่งที่เป็นไปได้คือลดรหัสโดยใช้ค่าคงที่

if CopyFile(PChar(sTxtpath+'Mapannotation.txt'), PChar(sOSdrive+'\Mapannotation.txt'), False) then
   LogBook.Log(2,'[POSTGRESQL INSTALLATION] :  [ACTION]:copying Mapannotation.txt to '+sOSdrive+'\ sucessful')
   else
   LogBook.Log(2,'[POSTGRESQL INSTALLATION] :  [ACTION]:copying Mapannotation.txt to '+sOSdrive+'\ Failed');

จะกลายเป็น:

if CopyFile(PChar(sTxtpath+'Mapannotation.txt'), PChar(sOSdrive+'\Mapannotation.txt'), False) then
   Log(2, SqlInstal, Action, CopyMapSuccess, sOSdrive)
   else
   Log(2, SqlInstal, Action, CopyMapFailure, sOSdrive)

ซึ่งมีรหัสบันทึกที่ดีกว่า / อัตราส่วนรหัสอื่น ๆ เมื่อนับจำนวนตัวอักษรบนหน้าจอ

นี่ใกล้กับสิ่งที่คุณแนะนำในคำถามที่ 2 ของคุณยกเว้นว่าฉันจะไม่ไปไกล: Log(9257)เห็นได้ชัดว่าสั้นกว่าLog(2, SqlInstal, Action, CopyMapSuccess, sOSdrive)แต่ก็อ่านยาก 9257 คืออะไร มันสำเร็จหรือไม่? การกระทำ? มันเกี่ยวข้องกับ SQL หรือไม่ หากคุณทำงานกับ codebase นี้ในช่วงสิบปีที่ผ่านมาคุณจะได้เรียนรู้ตัวเลขเหล่านั้นด้วยใจ (หากมีเหตุผลเช่น 9xxx เป็นรหัสสำเร็จ, x2xx เกี่ยวข้องกับ SQL, ฯลฯ ) แต่สำหรับนักพัฒนาใหม่ที่ค้นพบ codebase รหัสสั้น ๆ จะเป็นฝันร้าย

คุณสามารถไปต่อได้โดยผสมสองวิธีเข้าด้วยกัน: ใช้ค่าคงที่เดียว ส่วนตัวฉันจะไม่ทำอย่างนั้น ค่าคงที่ของคุณจะมีขนาดโตขึ้น:

Log(Type2SuccessSqlInstallCopyMapSuccess, sOSdrive) // Can you read this? Really?

มิฉะนั้นค่าคงที่จะสั้น แต่ไม่ชัดเจนมาก:

Log(T2SSQ_CopyMapSuccess, sOSdrive) // What's T2? What's SSQ? Or is it S, followed by SQ?
// or
Log(CopyMapSuccess, sOSdrive) // Is it an action? Is it related to SQL?

สิ่งนี้ยังมีข้อเสียสองประการ คุณจะต้อง:

  • เก็บรายการแยกต่างหากที่เชื่อมโยงข้อมูลบันทึกกับค่าคงที่ที่เกี่ยวข้อง ด้วยค่าคงที่เดียวมันจะเติบโตอย่างรวดเร็ว

  • ค้นหาวิธีบังคับใช้รูปแบบเดียวในทีมของคุณ ตัวอย่างเช่นสิ่งที่ถ้าแทนของT2SSQคนที่จะตัดสินใจที่จะเขียนST2SQL?


+1 สำหรับการlogโทรที่สะอาดแต่คุณช่วยอธิบายเพิ่มเติมให้ฉันฟังได้ไม่เข้าใจLog(2, SqlInstal, Action, CopyMapFailure, sOSdrive)คุณหมายถึงว่าSqlInstalจะเป็นตัวแปรที่กำหนดไว้ของฉันSqlInstal:=[POSTGRESQL INSTALLATION] หรือไม่
PresleyDias

@PresleyDias: สามารถเป็นอะไรก็ได้เช่นค่าSqlInstal 3จากนั้นในLog()ค่านี้จะถูกแปลอย่างมีประสิทธิภาพ[POSTGRESQL INSTALLATION]ก่อนที่จะต่อกับส่วนอื่น ๆ ของข้อความบันทึก
Arseni Mourzenko

single format in your teamเป็นตัวเลือกที่ดี / ดี
PresleyDias

3

ลองแยกฟังก์ชั่นชุดเล็ก ๆ ออกมาเพื่อจัดการกับสิ่งที่ดูยุ่งเหยิง มีรหัสซ้ำจำนวนมากที่สามารถทำได้อย่างง่ายดายทั้งหมดในที่เดียว ตัวอย่างเช่น:

procedure CopyIfFileDoesNotExist(filename: string);
var
   success: boolean;
begin
   if Not FileExists(sOSdrive+'\'+filename') then
   begin
      success := CopyFile(PChar(sTxtpath+filename), PChar(sOSdrive+filename), False);

      Log(filename, success);
   end;
end;

procedure Log(filename: string; isSuccess: boolean)
var
   state: string;
begin
   if isSuccess then
   begin
      state := 'success';
   end
   else
   begin
      state := 'failed';
   end;

   LogBook.Log(2,'[POSTGRESQL INSTALLATION] : [ACTION]:copying ' + filename + ' to '+sOSdrive+'\ ' + state);
end;

เคล็ดลับคือการดูข้อมูลที่ซ้ำกันในรหัสของคุณและค้นหาวิธีที่จะลบออก ใช้ช่องว่างจำนวนมากและใช้จุดเริ่มต้น / สิ้นสุดเพื่อประโยชน์ของคุณ (ช่องว่างมากขึ้นและง่ายต่อการค้นหา / พับบล็อกรหัส) ไม่ควรยากเกินไปจริงๆ วิธีการเหล่านี้อาจเป็นส่วนหนึ่งของคนตัดไม้ของคุณ ... ขึ้นอยู่กับคุณจริงๆ แต่นั่นดูเหมือนจะเป็นจุดเริ่มต้นที่ดี


+1, ช่องว่างสีขาวเป็นวิธีที่ดี .. success := CopyFile()ขอบคุณสำหรับแนวคิดนี้จะช่วยลดรหัสบรรทัดที่ไม่จำเป็นในกรณีของฉัน
PresleyDias

@ S.Robins ฉันอ่านรหัสของคุณถูกต้องหรือไม่ วิธีการของคุณเรียกว่าLogIfFileDoesNotExistคัดลอกไฟล์?
João Portela

1
@ JoãoPortelaใช่ ... มันไม่สวยมากและไม่ยึดติดอยู่กับหลักการความรับผิดชอบเดียว โปรดจำไว้ว่านี่เป็นครั้งแรกที่มีการปรับเปลี่ยนใหม่บนหัวของฉันและมุ่งที่จะช่วยให้ OP บรรลุเป้าหมายของเขาเพื่อลดความยุ่งเหยิงในรหัสของเขา อาจเป็นตัวเลือกที่ไม่ดีของชื่อสำหรับวิธีการในตอนแรก ฉันจะปรับแต่งเล็กน้อยเพื่อปรับปรุง :)
S.Robins

ดีที่เห็นว่าคุณใช้เวลาในการแก้ไขปัญหานั้น +1
João Portela

2

ฉันจะบอกว่าความคิดที่อยู่เบื้องหลังตัวเลือกที่ 2 นั้นดีที่สุด อย่างไรก็ตามฉันคิดว่าทิศทางที่คุณทำมันทำให้เรื่องแย่ลง จำนวนเต็มไม่ได้หมายความว่าอะไร เมื่อคุณดูรหัสคุณจะเห็นบางสิ่งกำลังถูกบันทึก แต่คุณไม่รู้อะไร

ฉันจะทำสิ่งนี้แทน:

void logHelper(String phase, String message) {
   LogBook.Log(2, "[" + phase + "] :  [Action]: " + message);
}

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

if (!FileExists(sOSdrive+'\Mapannotation.txt')) {
    if (CopyFile(PChar(sTxtpath+'Mapannotation.txt'), PChar(sOSdrive+'\Mapannotation.txt'), False)) {
       logHelper(POSTGRESQL, 'copying Mapannotation.txt to '+ sOSdrive +'\ sucessful')
    } else {
       logHelper(POSTGRESQL, 'copying Mapannotation.txt to '+ sOSdrive +'\ Failed');
    }
}

นี่ไม่ใช่สิ่งที่คุณพูดถึงในคำถามของคุณ แต่ฉันสังเกตเห็นเกี่ยวกับรหัสของคุณ การเยื้องของคุณไม่สอดคล้องกัน ครั้งแรกที่คุณใช้beginจะไม่เยื้อง แต่ครั้งที่สองคือ elseคุณทำสิ่งที่คล้ายกันกับ ฉันจะบอกว่าสิ่งนี้สำคัญกว่าสายบันทึก เมื่อการเยื้องไม่สอดคล้องกันจะทำให้ยากในการสแกนรหัสและติดตามการไหล บรรทัดบันทึกซ้ำ ๆ จำนวนมากนั้นง่ายต่อการกรองเมื่อสแกน


1

สิ่งที่เกี่ยวกับสายนี้:

LogBook.NewEntry( 2,'POSTGRESQL INSTALLATION', 'copying Mapannotation.txt to '+sOSdrive);

if CopyFile(PChar(sTxtpath+'Mapannotation.txt'), PChar(sOSdrive+'\Mapannotation.txt'), False) then
    LogBook.Success()
else
    LogBook.Failed();

NewEntry () วิธีการจะสร้างบรรทัดของข้อความ (รวมถึงการเพิ่ม [&] รอบรายการที่เหมาะสม) และถือว่าในการรอจนกว่าจะมีการเรียกวิธีการที่ประสบความสำเร็จ () หรือความล้มเหลว () ซึ่งผนวกสายกับ 'ความสำเร็จ' หรือ 'ความล้มเหลว' จากนั้นส่งบรรทัดไปยังบันทึก คุณยังสามารถสร้างวิธีอื่นเช่น info () เมื่อรายการบันทึกใช้สำหรับสิ่งอื่นที่ไม่ใช่ความสำเร็จ / ล้มเหลว

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