บทที่ 10 โครงสร้างโค้ด
ทำไมต้องจัดโครงสร้าง
เมื่อ script ยาวขึ้น main.py ไฟล์เดียวจะบำรุงรักษายากมาก
แนะนำแบ่งเป็น module แยกตามหน้าที่:
%%{init: {
"theme": "base",
"flowchart": {
"curve": "basis",
"htmlLabels": true,
"nodeSpacing": 42,
"rankSpacing": 56,
"padding": 16
}
}}%%
flowchart TD
M["main.py<br/>Entry point"]
C["core/<br/>EtabsSession"]
MO["models/<br/>Data classes"]
S["services/<br/>Business logic"]
M --> C
M --> S
S --> C
S --> MO
โครงสร้างที่แนะนำ
MyEtabsApp/├── main.py ← Entry point├── requirements.txt│├── core/│ ├── __init__.py│ ├── session.py ← EtabsSession (COM lifecycle)│ └── helpers.py ← check(), utils│├── models/│ ├── __init__.py│ ├── frame_result.py ← FrameForceResult dataclass│ ├── joint_info.py ← JointInfo dataclass│ └── story_info.py ← StoryInfo dataclass│├── services/│ ├── __init__.py│ ├── model_reader.py ← อ่านข้อมูลโมเดล│ ├── analyzer.py ← รัน analysis│ └── result_exporter.py ← export results → CSV/Excel│├── tests/│ ├── __init__.py│ ├── test_helpers.py│ └── test_result_exporter.py│└── output/ ← CSV, Excel outputLayer 1: core/ (COM Session)
core/session.py
"""จัดการ ETABS COM connection และ lifecycle"""import comtypes.clientimport gc
class EtabsSession: """Context manager สำหรับ ETABS COM session"""
_PROG_ID = "CSI.ETABS.API.ETABSObject"
def __init__(self, mode: str = "attach"): self.mode = mode self._etabs = None self._sap_model = None
@property def sap_model(self): if self._sap_model is None: raise ConnectionError("ยังไม่ได้ connect — ใช้ with statement") return self._sap_model
def connect(self): if self.mode == "attach": self._etabs = comtypes.client.GetActiveObject(self._PROG_ID) else: self._etabs = comtypes.client.CreateObject(self._PROG_ID) self._etabs.ApplicationStart()
if self._etabs is None: raise ConnectionError("ไม่สามารถเชื่อม ETABS ได้") self._sap_model = self._etabs.SapModel
def close(self): if self._sap_model is not None: del self._sap_model self._sap_model = None if self._etabs is not None: del self._etabs self._etabs = None gc.collect()
def __enter__(self): self.connect() return self
def __exit__(self, exc_type, exc_val, exc_tb): self.close() return Falsecore/helpers.py
"""Helper functions สำหรับ ETABS API"""
def check(ret: int, method_name: str) -> int: """ตรวจ return code จาก API call""" if ret != 0: raise RuntimeError(f"{method_name} failed (ret={ret})") return retLayer 2: models/ (Data Classes)
ใช้ @dataclass แทน plain dict เพื่อ:
- Type hints ชัดเจน
- IDE auto-complete
- ง่ายต่อการ serialize
models/frame_result.py
from dataclasses import dataclass
@dataclassclass FrameForceResult: """ผลลัพธ์ Frame Force สำหรับ 1 station""" frame: str station: float load_case: str p: float # axial force (kN) v2: float # shear V2 (kN) v3: float # shear V3 (kN) t: float # torsion (kN·m) m2: float # moment M2 (kN·m) m3: float # moment M3 (kN·m)
@property def max_abs_moment(self) -> float: return max(abs(self.m2), abs(self.m3))models/joint_info.py
from dataclasses import dataclass
@dataclassclass JointInfo: """ข้อมูล Joint""" name: str x: float y: float z: floatLayer 3: services/ (Business Logic)
services/model_reader.py
"""บริการอ่านข้อมูลจากโมเดล ETABS"""from core.helpers import checkfrom models.joint_info import JointInfo
class ModelReader: """อ่านข้อมูลจาก ETABS model"""
def __init__(self, sap_model): self._model = sap_model
def get_frame_names(self) -> list[str]: """อ่านรายชื่อ frame ทั้งหมด""" ret, _, names = self._model.FrameObj.GetNameList() check(ret, "FrameObj.GetNameList") return list(names)
def get_joints(self) -> list[JointInfo]: """อ่านพิกัด joints ทั้งหมด""" ret, _, point_names = self._model.PointObj.GetNameList() check(ret, "PointObj.GetNameList")
joints = [] for name in point_names: ret, x, y, z = self._model.PointObj.GetCoordCartesian(name) check(ret, f"GetCoordCartesian({name})") joints.append(JointInfo(name=name, x=x, y=y, z=z)) return jointsservices/result_exporter.py
"""Export results เป็น CSV/Excel"""import pandas as pdfrom models.frame_result import FrameForceResult
class ResultExporter: """Export ETABS results ผ่าน pandas"""
@staticmethod def to_csv(results: list[FrameForceResult], filepath: str): """Export FrameForceResult list เป็น CSV""" df = pd.DataFrame([r.__dict__ for r in results]) df.to_csv(filepath, index=False) print(f"✅ Export {filepath} ({len(results)} rows)")
@staticmethod def to_excel( results: list[FrameForceResult], filepath: str, sheet_name: str = "FrameForces" ): """Export เป็น Excel (.xlsx)""" df = pd.DataFrame([r.__dict__ for r in results]) df.to_excel(filepath, sheet_name=sheet_name, index=False) print(f"✅ Export {filepath}")Layer 0: main.py (Entry Point)
"""Entry point — ทุกอย่างเริ่มจากที่นี่"""from core.session import EtabsSessionfrom services.model_reader import ModelReaderfrom services.result_exporter import ResultExporter
def main(): with EtabsSession() as session: model = session.sap_model
# Set units model.SetPresentUnits(6)
# อ่านข้อมูล reader = ModelReader(model) frame_names = reader.get_frame_names() joints = reader.get_joints()
print(f"🏗️ Frames: {len(frame_names)}") print(f"📍 Joints: {len(joints)}")
# Analyze model.Analyze.RunAnalysis()
# Export # ... (ดูบทที่ 7 สำหรับการอ่าน results)
print("✅ เสร็จ!")
if __name__ == "__main__": main()Type Hints
ใช้ type hints ทุกที่เพื่อให้ IDE ช่วยตรวจ error:
# ❌ ไม่มี type hintsdef get_frames(model): ret, count, names = model.FrameObj.GetNameList() return names
# ✅ มี type hintsdef get_frames(model) -> list[str]: ret: int count: int names: tuple[str, ...] ret, count, names = model.FrameObj.GetNameList() return list(names)Testing ด้วย pytest
ติดตั้ง
pip install pytesttests/test_helpers.py
"""Test helper functions"""import pytestfrom core.helpers import check
def test_check_success(): """ret = 0 ต้องผ่าน""" assert check(0, "TestMethod") == 0
def test_check_failure(): """ret != 0 ต้อง raise RuntimeError""" with pytest.raises(RuntimeError, match="TestMethod failed"): check(1, "TestMethod")tests/test_result_exporter.py
"""Test result exporter (ไม่ต้องเชื่อม ETABS)"""import osimport pytestfrom models.frame_result import FrameForceResultfrom services.result_exporter import ResultExporter
@pytest.fixturedef sample_results() -> list[FrameForceResult]: return [ FrameForceResult("B1", 0.0, "DEAD", 0, 25.5, 0, 0, 0, -12.3), FrameForceResult("B1", 0.5, "DEAD", 0, 0, 0, 0, 0, 15.8), FrameForceResult("B1", 1.0, "DEAD", 0, -25.5, 0, 0, 0, -12.3), ]
def test_export_csv(sample_results, tmp_path): filepath = str(tmp_path / "test.csv") ResultExporter.to_csv(sample_results, filepath) assert os.path.exists(filepath)
def test_max_abs_moment(): r = FrameForceResult("B1", 0, "DEAD", 0, 0, 0, 0, -5.0, 10.0) assert r.max_abs_moment == 10.0รัน Test
# รัน test ทั้งหมดpytest tests/ -v
# รัน test เฉพาะไฟล์pytest tests/test_helpers.py -vDependency Diagram
%%{init: {
"theme": "base",
"flowchart": {
"curve": "basis",
"htmlLabels": true,
"nodeSpacing": 42,
"rankSpacing": 56,
"padding": 16
}
}}%%
flowchart TD
subgraph Entry["Entry Point"]
MAIN["main.py"]
end
subgraph Services["services/"]
MR["model_reader.py"]
RE["result_exporter.py"]
AN["analyzer.py"]
end
subgraph Core["core/"]
SS["session.py"]
HP["helpers.py"]
end
subgraph Models["models/"]
FR["frame_result.py"]
JI["joint_info.py"]
end
MAIN --> SS
MAIN --> MR
MAIN --> RE
MR --> HP
MR --> JI
RE --> FR
AN --> HP
สรุป
| ชั้น | หน้าที่ | ตัวอย่างไฟล์ |
|---|---|---|
main.py | Entry point, orchestration | main.py |
core/ | COM session, helpers | session.py, helpers.py |
models/ | Data classes (@dataclass) | frame_result.py |
services/ | Business logic | model_reader.py, result_exporter.py |
tests/ | Unit tests (pytest) | test_helpers.py |