การบันทึกถัดจากการใช้งานเป็นการละเมิด SRP หรือไม่


19

เมื่อคิดถึงการพัฒนาซอฟต์แวร์ที่คล่องตัวและหลักการทั้งหมด (SRP, OCP, ... ) ฉันถามตัวเองว่าจะจัดการการบันทึกอย่างไร

การบันทึกถัดจากการใช้งานเป็นการละเมิด SRP หรือไม่

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

สมมติว่าเรามีส่วนประกอบจำนวนมากโดยไม่มีการละเมิด SRP จากนั้นเราต้องการเพิ่มการบันทึก

  • องค์ประกอบ A
  • องค์ประกอบ B ใช้ A

เราต้องการบันทึกสำหรับ A ดังนั้นเราจึงสร้างส่วนประกอบอีก D ที่ตกแต่งด้วย A ทั้งสองใช้อินเตอร์เฟส I

  • อินเตอร์เฟส I
  • component L (บันทึกองค์ประกอบของระบบ)
  • องค์ประกอบ A ใช้ I
  • component D ใช้ I, ตกแต่ง / ใช้ A, ใช้ L สำหรับการบันทึก
  • องค์ประกอบ B ใช้ I

ข้อดี: - ฉันสามารถใช้ A โดยไม่ต้องบันทึก - การทดสอบ A หมายความว่าฉันไม่ต้องการ mocks การบันทึกใด ๆ - การทดสอบนั้นง่ายกว่า

ข้อเสีย: - ส่วนประกอบเพิ่มเติมและการทดสอบเพิ่มเติม

ฉันรู้ว่านี่เป็นคำถามเปิดการอภิปราย แต่ฉันต้องการทราบว่ามีคนใช้กลยุทธ์การบันทึกข้อมูลที่ดีกว่ามัณฑนากรหรือการละเมิด SRP สิ่งที่เกี่ยวกับคนตัดไม้ซิงเกิลตันแบบคงที่ซึ่งเป็นค่าเริ่มต้น NullLogger และถ้าต้องการ syslog- เข้าสู่ระบบหนึ่งเปลี่ยนวัตถุการใช้งานที่รันไทม์?



ฉันได้อ่านไปแล้วและคำตอบนั้นไม่น่าพอใจขอโทษ
Aitch


@ MarkRogers ขอบคุณสำหรับการแบ่งปันบทความที่น่าสนใจ ลุงบ๊อบกล่าวใน 'รหัสสะอาด' ว่าองค์ประกอบ SRP ที่ดีกำลังจัดการกับส่วนประกอบอื่น ๆ ในระดับที่เป็นนามธรรม สำหรับฉันคำอธิบายนั้นง่ายต่อการเข้าใจเนื่องจากบริบทอาจใหญ่เกินไป แต่ฉันไม่สามารถตอบคำถามได้เพราะบริบทหรือสิ่งที่เป็นนามธรรมของคนตัดไม้คืออะไร?
Aitch

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

คำตอบ:


-1

ใช่เป็นการละเมิด SRPเนื่องจากการบันทึกเป็นข้อกังวลที่ตัดขวาง

วิธีที่ถูกต้องคือมอบสิทธิ์การบันทึกไปยังคลาสตัวบันทึก (การสกัดกั้น) ซึ่งมีวัตถุประสงค์เพื่อการเข้าสู่ระบบโดย SRP เท่านั้น

ดูลิงค์นี้สำหรับตัวอย่างที่ดี: https://msdn.microsoft.com/en-us/library/dn178467%28v=pandp.30%29.aspx

นี่เป็นตัวอย่างสั้น ๆ :

public interface ITenantStore
{
    Tenant GetTenant(string tenant);
    void SaveTenant(Tenant tenant);
}

public class TenantStore : ITenantStore
{
    public Tenant GetTenant(string tenant)
    {....}

    public void SaveTenant(Tenant tenant)
    {....}
} 

public class TenantStoreLogger : ITenantStore
{
    private readonly ILogger _logger; //dep inj
    private readonly ITenantStore _tenantStore;

    public TenantStoreLogger(ITenantStore tenantStore)
    {
        _tenantStore = tenantStore;
    }

    public Tenant GetTenant(string tenant)
    {
        _logger.Log("reading tenant " + tenant.id);
        return _tenantStore.GetTenant(tenant);
    }

    public void SaveTenant(Tenant tenant)
    {
        _tenantStore.SaveTenant(tenant);
        _logger.Log("saving tenant " + tenant.id);
    }
}

ประโยชน์ที่ได้รับ

  • คุณสามารถทดสอบได้โดยไม่ต้องเข้าสู่ระบบ - การทดสอบหน่วยที่แท้จริง
  • คุณสามารถสลับการเปิด / ปิดการบันทึกได้อย่างง่ายดายแม้ในขณะใช้งานจริง
  • คุณสามารถทดแทนการบันทึกสำหรับการบันทึกในรูปแบบอื่น ๆ โดยไม่ต้องเปลี่ยนไฟล์ TenantStore

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

2
ฉันไม่เห็นคุณกำหนดตัวแปร _logger ที่ใดก็ได้ คุณวางแผนที่จะใช้การฉีดคอนสตรัคเตอร์แล้วลืมไปใช่ไหม ถ้าเป็นเช่นนั้นคุณอาจได้รับคำเตือนของคอมไพเลอร์
user2023861

27
แทน TenantStore ที่ถูก DIPed ด้วย Logger เอนกประสงค์ซึ่งต้องใช้คลาส N + 1 (เมื่อคุณเพิ่ม LandlordStore, FooStore, BarStore และอื่น ๆ ) คุณมี TenantStoreLogger ที่ถูก DIPed กับ TenantStore ซึ่งเป็น FooStoreLogger และอื่น ๆ ... ต้องการคลาส 2N เท่าที่ฉันสามารถบอกได้เพื่อผลประโยชน์เป็นศูนย์ เมื่อคุณต้องการทำการทดสอบที่ไม่มีการบันทึกคุณจะต้อง rejigger คลาส N แทนการกำหนดค่า NullLogger IMO นี่เป็นวิธีการที่แย่มาก
user949300

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

9
downvoted คุณลบข้อกังวลเกี่ยวกับการบันทึกจากคลาสผู้เช่า แต่ตอนนี้คุณTenantStoreLoggerจะเปลี่ยนทุกครั้งที่มีTenantStoreการเปลี่ยนแปลง คุณไม่ได้แยกข้อกังวลออกไปมากกว่าในวิธีแก้ปัญหาเบื้องต้น
Laurent LA RIZZA

61

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

จุดของ SRP คือการหลีกเลี่ยงรหัสสปาเก็ตตี้ที่น่ากลัวซึ่งรหัสที่ทำสิ่งที่แตกต่างกันทั้งหมดเข้าด้วยกัน การผสมการบันทึกด้วยรหัสการทำงานไม่ได้ทำให้เสียงสัญญาณเตือนดังขึ้นสำหรับฉัน


19
@Aitch: ทางเลือกของคุณคือการจดบันทึกการเรียนในชั้นเรียนของคุณส่งผ่านหมายเลขอ้างอิงไปยังตัวบันทึกหรือไม่บันทึกสิ่งใดเลย หากคุณจะเข้มงวดเป็นพิเศษเกี่ยวกับ SRP ด้วยค่าใช้จ่ายของทุกอย่างอื่นฉันขอแนะนำไม่ให้บันทึกอะไรเลย สิ่งที่คุณจำเป็นต้องรู้เกี่ยวกับสิ่งที่ซอฟต์แวร์ของคุณกำลังทำอยู่นั้นสามารถแก้ไขได้โดยใช้ดีบักเกอร์ P ใน SRP หมายถึง "หลักการ" ไม่ใช่ "กฎทางกายภาพของธรรมชาติซึ่งจะต้องไม่ถูกทำลาย"
Blrfl

3
@Aitch: คุณควรจะสามารถติดตามการบันทึกในชั้นเรียนของคุณกลับไปเป็นข้อกำหนดบางอย่างมิฉะนั้นคุณจะละเมิด YAGNI หากการบันทึกอยู่บนโต๊ะคุณให้ในตัวจัดการคนตัดไม้ที่ถูกต้องเช่นเดียวกับที่คุณต้องการสิ่งอื่นใดในชั้นเรียนโดยเฉพาะอย่างยิ่งหนึ่งจากชั้นเรียนที่ผ่านการทดสอบแล้ว ไม่ว่าจะเป็นรายการที่สร้างรายการบันทึกจริงหรือทิ้งลงในที่ฝากข้อมูลบิตเป็นข้อกังวลของอินสแตนซ์ของคลาสของคุณ ชั้นเรียนไม่ควรสนใจ
Blrfl

3
@Aitch เพื่อตอบคำถามของคุณเกี่ยวกับการทดสอบหน่วย: Do you mock the logger?นั่นคือสิ่งที่คุณทำ คุณควรมีILoggerอินเทอร์เฟซที่กำหนดสิ่งที่คนตัดไม้ทำ รหัสภายใต้การทดสอบจะถูกฉีดด้วยILoggerที่คุณระบุ class TestLogger : ILoggerสำหรับการทดสอบคุณมี สิ่งที่ดีเกี่ยวกับเรื่องนี้คือTestLoggerสามารถเปิดเผยสิ่งต่าง ๆ เช่นสายอักขระสุดท้ายหรือบันทึกข้อผิดพลาด การทดสอบสามารถตรวจสอบว่ารหัสภายใต้การทดสอบกำลังบันทึกอย่างถูกต้อง ตัวอย่างเช่นการทดสอบอาจเป็นUserSignInTimeGetsLogged()ที่ทดสอบจะตรวจสอบTestLoggerบันทึก
CurtisHx

5
99% ดูเหมือนจะน้อยไปหน่อย คุณอาจดีกว่า 100% ของโปรแกรมเมอร์ทั้งหมด
พอลเดรเปอร์

2
+1 เพื่อสติ เราต้องการความคิดแบบนี้มากขึ้น: ให้ความสำคัญกับคำและหลักการเชิงนามธรรมน้อยลงและให้ความสำคัญกับการมีฐานรหัสที่สามารถบำรุงรักษาได้มากขึ้น
jpmc26

15

ไม่ไม่ใช่การละเมิด SRP

ข้อความที่คุณส่งไปยังบันทึกควรเปลี่ยนด้วยเหตุผลเดียวกับรหัสโดยรอบ

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

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

NullLoggerการดำเนินงานของคุณแล้วก็ควรที่จะไม่ทราบว่าคนตัดไม้มันจะส่งข้อความไปยังเป็น

ที่กล่าวว่า

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

อะไรคือสิ่งที่เป็นข้อกังวลข้าม OTOH คือการติดตามการดำเนินการ : การบันทึกเข้าและออกในแต่ละวิธี AOP อยู่ในตำแหน่งที่ดีที่สุดในการทำเช่นนี้


สมมติว่าข้อความตัวบันทึกคือ 'ผู้ใช้ล็อกอิน xyz' ซึ่งถูกส่งไปยังตัวบันทึกซึ่งเป็นการเตรียมการบันทึกเวลา ฯลฯ คุณรู้หรือไม่ว่า 'การเข้าสู่ระบบ' หมายถึงการใช้งานอย่างไร มันเริ่มเซสชันด้วยคุกกี้หรือกลไกอื่นหรือไม่? ฉันคิดว่ามีหลายวิธีในการใช้การเข้าสู่ระบบดังนั้นการเปลี่ยนแปลงการใช้งานจึงไม่มีเหตุผลเกี่ยวกับความจริงที่ว่าผู้ใช้เข้าสู่ระบบนั่นเป็นอีกตัวอย่างที่ดีของการตกแต่งส่วนประกอบต่าง ๆ (พูด OAuthLogin, SessionLogin, BasicAuthorizationLogin) เป็นLogin-interface ตกแต่งด้วยคนตัดไม้เดียวกัน
Aitch

ขึ้นอยู่กับข้อความ "login user xyz" หมายถึง หากทำเครื่องหมายว่าการเข้าสู่ระบบสำเร็จการส่งข้อความไปยังบันทึกนั้นจะอยู่ในกรณีที่ใช้การเข้าสู่ระบบ วิธีเฉพาะในการแสดงข้อมูลการเข้าสู่ระบบเป็นสตริง (OAuth, Session, LDAP, NTLM, ลายนิ้วมือ, ล้อแฮมสเตอร์) เป็นสมาชิกของคลาสเฉพาะที่แสดงถึงข้อมูลรับรองหรือกลยุทธ์การเข้าสู่ระบบ ไม่จำเป็นต้องลบออกที่น่าสนใจ กรณีที่เน้นเรื่องนี้ไม่ได้เป็นเรื่องที่น่ากังวล มันเป็นเฉพาะสำหรับกรณีการใช้งานเข้าสู่ระบบ
Laurent LA RIZZA

7

เนื่องจากการบันทึกมักจะถูกพิจารณาว่าเป็นข้อกังวลข้ามกันฉันขอแนะนำให้ใช้ AOP เพื่อแยกการบันทึกจากการนำไปใช้

ขึ้นอยู่กับภาษาที่คุณใช้ Interceptor หรือกรอบ AOP (เช่น AspectJ ใน Java) เพื่อทำสิ่งนี้

คำถามคือถ้ามันคุ้มค่ากับความยุ่งยาก โปรดทราบว่าการแยกนี้จะเพิ่มความซับซ้อนของโครงการของคุณในขณะที่ให้ประโยชน์น้อยมาก


2
รหัส AOP ส่วนใหญ่ที่ฉันเห็นคือเกี่ยวกับการบันทึกทุกขั้นตอนการเข้าและออกของทุกวิธี ฉันต้องการบันทึกเฉพาะบางส่วนของตรรกะทางธุรกิจ ดังนั้นอาจเป็นไปได้ที่จะบันทึกวิธีการใส่คำอธิบายประกอบเท่านั้น แต่ AOP สามารถมีได้เฉพาะในภาษาสคริปต์และสภาพแวดล้อมของเครื่องเสมือนเท่านั้นใช่ไหม ในเช่น C ++ มันเป็นไปไม่ได้ ฉันยอมรับว่าฉันไม่พอใจกับวิธีการของ AOP แต่อาจไม่มีวิธีแก้ปัญหาที่สะอาดกว่านี้อีก
Aitch

1
@Aitch "C ++ เป็นไปไม่ได้" : ถ้าคุณ google สำหรับ "aop c ++" คุณจะพบบทความเกี่ยวกับเรื่องนี้ "... รหัส AOP ที่ฉันเห็นนั้นเกี่ยวกับการบันทึกทุกขั้นตอนการเข้าและออกของทุกวิธีฉันต้องการบันทึกเฉพาะบางส่วนของตรรกะทางธุรกิจ" Aop ช่วยให้คุณกำหนดรูปแบบเพื่อค้นหาวิธีการแก้ไข เช่นวิธีการทั้งหมดจาก namespace "my.busininess. *"
k3b

1
การบันทึกมักจะไม่ใช่เรื่องที่ต้องกังวลโดยเฉพาะเมื่อคุณต้องการให้บันทึกของคุณมีข้อมูลที่น่าสนใจเช่นมีข้อมูลมากกว่าที่อยู่ในการติดตามสแต็กข้อยกเว้น
Laurent LA RIZZA

5

ฟังดูดีนะ คุณกำลังอธิบายมัณฑนากรการบันทึกข้อมูลมาตรฐานที่ค่อนข้างยุติธรรม คุณมี:

component L (บันทึกองค์ประกอบของระบบ)

สิ่งนี้มีหน้าที่รับผิดชอบอย่างหนึ่งคือการบันทึกข้อมูลที่ส่งผ่านไป

องค์ประกอบ A ใช้ I

สิ่งนี้มีความรับผิดชอบอย่างหนึ่ง: การนำเสนอการดำเนินการของส่วนต่อประสานที่ฉันใช้

นี่คือส่วนสำคัญ:

component D ใช้ I, ตกแต่ง / ใช้ A, ใช้ L สำหรับการบันทึก

เมื่อระบุด้วยวิธีนี้ฟังดูซับซ้อน แต่ดูด้วยวิธีนี้: Component D ทำสิ่งหนึ่ง : นำ A และ L มารวมกัน

  • คอมโพเนนต์ D ไม่บันทึก มันมอบให้กับแอล
  • คอมโพเนนต์ D ไม่ได้ใช้ตัวเอง มันมอบให้กับ A

เพียงความรับผิดชอบว่าองค์ประกอบที่มี D คือการทำให้แน่ใจว่า L จะได้รับแจ้งเมื่อจะใช้ การใช้งานของ A และ L มีทั้งที่อื่น นี่เป็นไปตาม SRP อย่างสมบูรณ์รวมถึงเป็นตัวอย่างที่เรียบร้อยของ OCP และการใช้งานที่เป็นเรื่องธรรมดาของนักตกแต่ง

ข้อแม้ที่สำคัญ:เมื่อ D ใช้องค์ประกอบการบันทึกของคุณ L มันควรทำในลักษณะที่ช่วยให้คุณเปลี่ยนวิธีการบันทึกของคุณ วิธีที่ง่ายที่สุดในการทำเช่นนี้คือการมีส่วนต่อประสาน IL ซึ่งถูกใช้งานโดย L. แล้ว:

  • คอมโพเนนต์ D ใช้ IL เพื่อบันทึก ตัวอย่างของ L มีให้
  • Component D ใช้ I ในการจัดเตรียมการทำงาน ตัวอย่างของ A มีให้
  • องค์ประกอบ B ใช้ I; ตัวอย่างของ D มีให้

ด้วยวิธีนี้ไม่มีอะไรขึ้นกับสิ่งอื่นโดยตรงทำให้ง่ายต่อการสลับออก สิ่งนี้ทำให้ง่ายต่อการปรับเปลี่ยนและง่ายต่อการเยาะเย้ยส่วนต่าง ๆ ของระบบเพื่อให้คุณสามารถทดสอบหน่วยได้


จริง ๆ แล้วฉันรู้เพียง C # ซึ่งมีการสนับสนุนตัวแทนท้องถิ่น D implements Iนั่นเป็นเหตุผลที่ผมเขียน ขอบคุณสำหรับคำตอบ.
Aitch

1

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

ตัวอย่าง:

class Logger {
   ActuallLogger logger;
   public Action ComposeLog(string msg, Action action) {
      return () => {
          logger.debug(msg);
          action();
      };
   }
}

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

"การเรียกวิธีการหาลำดับ" หรือองค์ประกอบการทำงานไม่ใช่ประเด็นที่ต้องคำนึงถึง ความรับผิดชอบของฟังก์ชั่นที่ฉันสร้างขึ้นคือการเขียนคำสั่งบันทึกด้วยการกระทำ ฉันไม่จำเป็นต้องใช้คำว่า "และ" เพื่ออธิบายสิ่งที่ฟังก์ชั่นนี้ทำ
Paul Nikonowicz

มันไม่ใช่รายละเอียดการใช้งาน มันมีผลกระทบอย่างลึกซึ้งต่อรูปร่างของรหัสของคุณ
Laurent LA RIZZA

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