ฉันจะจัดการชุดกฎและหมายเลขมายากลที่มีขนาดใหญ่มากในโปรแกรมของฉันได้อย่างไร


21

ฉันค่อนข้างใหม่ในการเขียนโปรแกรม (ฉันเป็นวิศวกรเครื่องกลโดยการค้า) และฉันกำลังพัฒนาโปรแกรมขนาดเล็กในช่วงเวลาหยุดทำงานของฉันที่สร้างชิ้นส่วน (solidworks) ตามการป้อนข้อมูลจากผู้คนต่าง ๆ จากโรงงาน

จากอินพุตเพียงเล็กน้อย (6 ต่อแน่นอน) ฉันต้องทำการเรียก API หลายร้อยครั้งที่สามารถใช้พารามิเตอร์มากถึงสิบตัว ทั้งหมดสร้างขึ้นโดยชุดของกฎที่ฉันรวบรวมหลังจากสัมภาษณ์ทุกคนที่จัดการส่วนนั้น ส่วนกฎและพารามิเตอร์ของรหัสของฉันคือ 250 บรรทัดและเพิ่มขึ้น

ดังนั้นวิธีที่ดีที่สุดในการรักษาและอ่านรหัสของฉันคืออะไร? ฉันจะแยกหมายเลขเวทย์มนตร์ทั้งหมดกฎทั้งหมดอัลกอริธึมและส่วนของขั้นตอนของรหัสได้อย่างไร ฉันจะจัดการกับ API ที่ละเอียดและละเอียดมากได้อย่างไร

เป้าหมายหลักของฉันคือการสามารถมอบแหล่งข้อมูลให้ใครบางคนและให้พวกเขาเข้าใจในสิ่งที่ฉันทำ


7
คุณสามารถให้ตัวอย่างของการเรียก API เหล่านี้ได้ไหม
Robert Harvey


"ปัญหาทั้งหมดในวิทยาการคอมพิวเตอร์สามารถแก้ไขได้ด้วยการเปลี่ยนทิศทางอีกระดับ" - David Wheeler
Phil Frost

... ยกเว้นทางอ้อมมากเกินไป :)
แดนลียง

1
เป็นการยากที่จะตอบคำถามของคุณโดยไม่เห็นรหัสของคุณ คุณสามารถโพสต์รหัสของคุณบนcodereview.stackexchange.comและรับคำแนะนำจากโปรแกรมเมอร์อื่น ๆ
Gilbert Le Blanc

คำตอบ:


26

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

โปรดทราบว่า 'ฐานข้อมูล' ไม่ได้แปลว่า MySQL หรือ MS-SQL เสมอไป วิธีที่คุณจัดเก็บข้อมูลจะขึ้นอยู่กับวิธีการใช้งานของโปรแกรมการเขียนเป็นต้นอาจหมายถึงฐานข้อมูลประเภท SQL หรืออาจหมายถึงไฟล์ข้อความที่จัดรูปแบบ


7
เห็นด้วยกับการประมวลผลข้อมูลในฐานข้อมูลแม้ว่าจะดูเหมือนว่าเขามีปัญหามากขึ้น
Robert Harvey

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

1
เสียงเหมือนการเข้ารหัสนุ่ม ฐานข้อมูลสำหรับสถานะที่ไม่แน่นอน หมายเลข Magic ไม่สามารถเปลี่ยนแปลงได้โดยการกำหนด
Phil

1
@PhilFrost: คุณสามารถทำให้มันไม่เปลี่ยนรูป อย่าเพิ่งเขียนถึงพวกเขาหลังจากสร้างตารางเริ่มต้น
Robert Harvey

1
@PhilFrost: ตอนนี้ฉันได้เห็นAPIที่เขาจัดการแล้ว มันน่าทึ่งสำหรับขนาดที่แท้จริงเท่านั้น เขาอาจไม่ต้องการฐานข้อมูลเลยเว้นแต่เขาจะทำ
Robert Harvey

14

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

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

เป็นการดีที่คุณจะจบลงด้วยรหัสที่ระบุว่า:

LeftBearingHoleDepth = BearingWidth + HoleDepthTolerance;
if (not CheckPartWidth(LeftBearingHoleDepth, {other parameters})
    {whatever you need to adjust}

ฉันจะถูกล่อลวงให้ประกาศในหน้าที่ที่พวกเขาใช้ในกรณีที่เป็นไปได้ มันค่อนข้างมีประโยชน์ที่จะเปลี่ยน:

SomeAPICall(10,324.5, 1, 0.02, 6857);

เข้าไป

const NumberOfOilDrainHoles = 10
const OilDrainHoleSpacing = 324.5
{etc}
SomeAPICall(NumberOfOilDrainHoles, OilDrainHoleSpacing, {etc}

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

หนึ่งเคล็ดลับสำหรับชื่อ: ใส่คำที่สำคัญที่สุดทางด้านซ้าย อาจไม่อ่านค่อนข้างดี แต่มันทำให้การค้นหาง่ายขึ้น เวลาส่วนใหญ่ที่คุณกำลังมองหาบ่อและสงสัยเกี่ยวกับสายฟ้าไม่ได้มองสายฟ้าและสงสัยว่าจะทำที่ไหนจึงเรียกมันว่า SumpBoltThreadPitch ไม่ใช่ BoltThreadPitchSump จากนั้นเรียงลำดับรายการค่าคงที่ หลังจากนั้นเพื่อแยกเธรดระยะห่างทั้งหมดคุณสามารถรับรายการในตัวแก้ไขข้อความและใช้ฟังก์ชันค้นหาหรือใช้เครื่องมือเช่น grep เพื่อส่งกลับเฉพาะบรรทัดที่มี "ThreadPitch"


1
ลองพิจารณาสร้างส่วนต่อประสาน Fluent
Ian

นี่คือบรรทัดจริงจากรหัสของฉัน มันสมเหตุสมผลหรือไม่ว่าเกิดอะไรขึ้นที่นี่ (อาร์กิวเมนต์คือ x1, y1, z1, x2, y2, z2 เป็นสองเท่า) ถ้าคุณรู้ว่าชื่อตัวแปรหมายถึงอะไร .CreateLine(m_trunion_support_spacing / 2, -((m_flask_length / 2) + m_sand_ledge_width + m_wall_thickness), -m_flange_thickness, m_trunion_support_spacing / 2, -((m_flask_length / 2) + m_sand_ledge_width + m_wall_thickness), -m_flask_height + m_flange_thickness)
2785724

คุณยังสามารถใช้ctagsพร้อมกับการรวมโปรแกรมแก้ไขเพื่อค้นหาค่าคงที่
Phil Frost

3
@ user2785724 นั่นเป็นระเบียบ มันทำอะไรอยู่? มันสร้างร่องที่มีความยาวและความลึกเป็นพิเศษหรือไม่? createGroove(length, depth)จากนั้นคุณสามารถสร้างฟังก์ชั่นที่เรียกว่า คุณต้องใช้ฟังก์ชั่นที่อธิบายถึงสิ่งที่คุณต้องการให้สำเร็จตามที่คุณต้องการให้อธิบายถึงวิศวกรเครื่องกล นั่นคือสิ่งที่เกี่ยวกับการเขียนโปรแกรมความรู้
Phil Frost

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

4

ฉันคิดว่าคำถามของคุณลดลงเป็น: ฉันจะจัดโครงสร้างการคำนวณได้อย่างไร โปรดสังเกตว่าคุณต้องการจัดการ "ชุดกฎ" ซึ่งเป็นรหัสและ "ชุดหมายเลขมายากล" ซึ่งเป็นข้อมูล (คุณสามารถเห็นพวกเขาเป็น "ข้อมูลที่ฝังอยู่ในรหัสของคุณ" แต่เป็นข้อมูลที่ยังคงอยู่)

นอกจากนี้การทำให้โค้ดของคุณ "เป็นที่เข้าใจได้ของผู้อื่น" นั้นเป็นเป้าหมายทั่วไปของกระบวนทัศน์การเขียนโปรแกรมทั้งหมด (ดูเช่น " รูปแบบการนำไปปฏิบัติ " โดย Kent Beck หรือ " Clean Code " โดย Robert C. Martin สำหรับผู้เขียนซอฟต์แวร์ที่ระบุเป้าหมายเดียวกัน สำหรับคุณสำหรับโปรแกรมใด ๆ )

คำแนะนำทั้งหมดในหนังสือเหล่านี้จะใช้กับคำถามของคุณ ให้ฉันดึงคำแนะนำบางอย่างสำหรับ "หมายเลขมายากล" และ "ชุดกฎ" โดยเฉพาะ:

  1. ใช้ค่าคงที่ที่มีชื่อและการแจกแจงเพื่อแทนที่หมายเลขมายากล

    ตัวอย่างของค่าคงที่ :

    if (partWidth > 0.625) {
        // doSomeApiCall ...
    }
    return (partWidth - 0.625)
    

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

    const double MAX_PART_WIDTH = 0.625;
    
    if (partWidth > MAX_PART_WIDTH) {
        // doSomeApiCall ...
    }
    return (partWidth - MAX_PART_WIDTH)
    

    ตัวอย่างของ Enumerations :

    การแจกแจงสามารถช่วยคุณรวบรวมข้อมูลที่อยู่ด้วยกัน หากคุณใช้ Java โปรดจำไว้ว่า Enums เป็นวัตถุ องค์ประกอบของพวกเขาสามารถเก็บข้อมูลและคุณสามารถกำหนดวิธีการที่คืนองค์ประกอบทั้งหมดหรือตรวจสอบคุณสมบัติบางอย่าง นี่คือ Enum ใช้ในการสร้าง Enum อื่น:

    public enum EnginePart {
        CYLINDER (100, Materials.STEEL),
        FLYWHEEL (120, Materials.STEEL),
        CRANKSHAFT (200, Materials.CARBON);
    
        private final double maxTemperature;
        private final Materials composition;
        private EnginePart(double maxTemperature, Materials composition) {
            this.maxTemperature = maxTemperature;
            this.composition = composition;
        }
    }
    
    public enum Materials {
        STEEL,
        CARBON
    }
    

    ข้อดีคือ: ตอนนี้ไม่มีใครสามารถกำหนด EnginePart ที่ไม่ได้ทำมาจากเหล็กหรือคาร์บอนได้อย่างไม่ถูกต้องและไม่มีใครสามารถแนะนำ EnginePart ที่เรียกว่า "asdfasdf" เช่นเดียวกับกรณีที่เป็นสตริงที่จะตรวจสอบเนื้อหา

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

    ตัวอย่างของรูปแบบวิธีการจากโรงงาน :

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

    public class EngineAssemblyLine {
        public EngineAssemblyLine() {
            EnginePart enginePart1 = makeEnginePart();
            EnginePart enginePart2 = makeEnginePart();
            enginePart1.connect(enginePart2);
            this.addEngine(engine1);
            this.addEngine(engine2);
        }
    
        protected Room makeEngine() {
            return new NormalEngine();
        }
    }
    

    แล้วในชั้นเรียนอื่น:

    public class CompressedEngineAssemblyLine extends EngineAssemblyLine {
        @Override
        protected Room makeRoom() {
            return new CompressedEngine();
        }
    }
    

    ส่วนที่น่าสนใจคือ: ตอนนี้ตัวสร้าง AssemblyLine ของคุณจะถูกแยกออกจากประเภทของเครื่องมือที่กำลังจัดการ อาจเป็นaddEngineวิธีที่เรียก API ระยะไกล ...

    ตัวอย่างของรูปแบบกลยุทธ์ :

    รูปแบบกลยุทธ์อธิบายวิธีการแนะนำฟังก์ชั่นเป็นวัตถุเพื่อเปลี่ยนพฤติกรรมของมัน ให้เราจินตนาการว่าบางครั้งคุณต้องการขัดชิ้นงานบางครั้งคุณต้องการทาสีและโดยค่าเริ่มต้นคุณต้องการตรวจสอบคุณภาพ นี่เป็นตัวอย่างของ Python ที่ดัดแปลงมาจาก Stack Overflow

    class PartWithStrategy:
    
        def __init__(self, func=None) :
            if func:
                self.execute = func
    
        def execute(self):
            # ... call API of quality review ...
            print "Part will be reviewed"
    
    
    def polish():
        # ... call API of polishing department ...
        print "Part will be polished"
    
    
    def paint():
        # ... call API of painting department ...
        print "Part will be painted"
    
    if __name__ == "__main__" :
        strat0 = PartWithStrategy()
        strat1 = PartWithStrategy(polish)
        strat2 = PartWithStrategy(paint)
    
        strat0.execute()  # output is "Part will be reviewed"
        strat1.execute()  # output is "Part will be polished"
        strat2.execute()  # output is "Part will be painted"
    

    คุณสามารถขยายสิ่งนี้เป็นรายการการกระทำที่คุณต้องการดำเนินการแล้วเรียกพวกเขากลับจากexecuteวิธีการ บางทีความเห็นทั่วไปนี้น่าจะอธิบายได้ดีกว่าในฐานะรูปแบบของตัวสร้างแต่เดี๋ยวก่อนเราไม่ต้องการเลือกใครใช่ไหม? :)


2

คุณอาจต้องการใช้เอ็นจินกฎ เอ็นจิ้นกฎช่วยให้คุณใช้ DSL (Domain Specific Language) ซึ่งออกแบบมาเพื่อจำลองเกณฑ์ที่จำเป็นสำหรับผลลัพธ์บางอย่างในวิธีที่เข้าใจได้ดังที่อธิบายไว้ในคำถามนี้

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

หากคุณโชคดีมีเอ็นจิ้นกฎที่พร้อมใช้งานสำหรับภาษาการเขียนโปรแกรมที่คุณใช้อยู่

ข้อเสียคือคุณต้องทำความคุ้นเคยกับเอ็นจินกฎซึ่งอาจเป็นเรื่องยากหากคุณเป็นผู้เริ่มต้นเขียนโปรแกรม


1

โซลูชันของฉันสำหรับปัญหานี้แตกต่างกันมาก: เลเยอร์การตั้งค่าและ LOP

ก่อนตัด API ในเลเยอร์ ค้นหาลำดับของการโทร API ที่ใช้ร่วมกันและรวมเข้ากับการโทร API ของคุณเอง ในที่สุดไม่ควรมีการโทรโดยตรงไปยัง API พื้นฐานเพียงโทรไปที่ wrappers ของคุณ การเรียกใช้ wrapper ควรเริ่มดูเหมือนภาษาเล็ก ๆ

ประการที่สองใช้ 'เครื่องมือจัดการการตั้งค่า' นี่เป็นวิธีการเชื่อมโยงชื่อกับค่าแบบไดนามิก บางสิ่งเช่นนี้ อีกภาษาเล็ก ๆ

Baseplate.name="Base plate"
Baseplate.length=1032.5
Baseplate.width=587.3

สุดท้ายใช้มินิภาษาของคุณเองเพื่อแสดงการออกแบบ (นี่คือการเขียนโปรแกรมด้วยภาษา) ภาษานี้ควรเข้าใจได้สำหรับวิศวกรและนักออกแบบที่มีส่วนร่วมในกฎและการตั้งค่า ตัวอย่างแรกของผลิตภัณฑ์ดังกล่าวที่นึกถึงคือ Gnuplot แต่มีอีกหลายอย่าง คุณสามารถใช้ Python ได้แม้ว่าโดยส่วนตัวแล้วฉันจะไม่ทำ

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


0

ฉันไม่แน่ใจว่าฉันได้รับคำถามอย่างถูกต้อง แต่ดูเหมือนว่าคุณควรจัดกลุ่มสิ่งต่าง ๆ ในโครงสร้างบางอย่าง พูดว่าถ้าคุณใช้ C ++ คุณสามารถกำหนดสิ่งต่าง ๆ เช่น:

struct SomeParametersClass
{
    int   p1;  // this is for that
    float p2;  // this is a different parameter
    ...
    SomeParametersClass() // constructor, assigns default values
    {
        p1 = 42; // the best value that some guy told me
        p2 = 3.14; // looks like a know value, but isn't
    {
};

struct SomeOtherParametersClass
{
    int   v1;  // this is for ...
    float v2;  // this is for ...
    ...
    SomeOtherParametersClass() // constructor, assigns default values
    {
        v1 = 24; // the best value 
        v2 = 1.23; // also the best value
    }
};

คุณสามารถเริ่มต้นเหล่านี้ได้ที่จุดเริ่มต้นของโปรแกรม:

int main()
{
    SomeParametersClass params1;
    SomeOtherParametersClass params2;
    ...

จากนั้นการเรียก API ของคุณจะมีลักษณะ (สมมติว่าคุณไม่สามารถเปลี่ยนลายเซ็นได้):

 SomeAPICall( params1.p1, params1.p2 );

หากคุณสามารถเปลี่ยนลายเซ็นของ API คุณสามารถผ่านโครงสร้างทั้งหมดได้:

 SomeAPICall( params1 );

คุณยังสามารถจัดกลุ่มพารามิเตอร์ทั้งหมดลงใน wrapper ที่มีขนาดใหญ่กว่า:

struct AllTheParameters
{
    SomeParametersClass      SPC;
    SomeOtherParametersClass SOPC;
};

0

ฉันประหลาดใจที่ไม่มีใครพูดถึงเรื่องนี้ ...

คุณพูดว่า:

เป้าหมายหลักของฉันคือการสามารถมอบแหล่งข้อมูลให้ใครบางคนและให้พวกเขาเข้าใจในสิ่งที่ฉันทำ

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

คำตอบอื่น ๆ ทั้งหมดมีพื้นฐานทางเทคนิคเป็นอย่างมาก แต่พวกเขาไม่สนใจพื้นฐานที่โปรแกรมเมอร์ส่วนใหญ่เรียนรู้ เนื่องจากคุณเป็นผู้ค้าขายฉันเดาว่าคุณไม่คุ้นเคยกับเอกสารประเภทนี้

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

var x = y + z;

หรือ:

//Where bandwidth, which was previously defined is (1000 * Info Rate) / FEC Rate / Modulation * carrier spacing / 1000000
float endFrequency = centerFrequency + (1/2 bandwidth);

นี่เป็นภาษาที่ค่อนข้างอิสระ ไม่ว่าคุณจะทำงานกับแพลตฟอร์ม, IDE, ภาษา, ฯลฯ อะไรเอกสารที่เหมาะสมเป็นวิธีที่สะอาดและง่ายที่สุดในการทำให้แน่ใจว่ามีคนเข้าใจโค้ดของคุณ

ต่อไปก็คือการจัดการตัวเลขเวทมนต์และข้อกังวลมากมาย แต่ฉันคิดว่าข้อคิดเห็นของ GrandmasterB นั้นใช้ได้ดีทีเดียว

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