SiamCafe.net Blog
Technology

SonarQube Analysis Hexagonal Architecture

sonarqube analysis hexagonal architecture
SonarQube Analysis Hexagonal Architecture | SiamCafe Blog
2025-11-26· อ. บอม — SiamCafe.net· 9,501 คำ

SonarQube Code Quality Analysis

SonarQube วิเคราะห์ Code Quality ตรวจจับ Bugs, Code Smells, Vulnerabilities รองรับ 30+ ภาษา ใช้ Quality Gates กำหนดมาตรฐาน ใช้ร่วมกับ CI/CD Pipeline

Hexagonal Architecture แยก Domain ออกจาก External Systems ใช้ Ports and Adapters Pattern ทำให้ Code สะอาด ทดสอบง่าย SonarQube ช่วยตรวจสอบว่า Code ยังคง Clean

SonarQube Setup และ Configuration

# === SonarQube Setup ===

# 1. Docker Compose
# docker-compose.yml
# version: '3.8'
# services:
#   sonarqube:
#     image: sonarqube:community
#     ports:
#       - "9000:9000"
#     environment:
#       SONAR_JDBC_URL: jdbc:postgresql://db:5432/sonar
#       SONAR_JDBC_USERNAME: sonar
#       SONAR_JDBC_PASSWORD: sonar
#     volumes:
#       - sonarqube_data:/opt/sonarqube/data
#       - sonarqube_extensions:/opt/sonarqube/extensions
#     depends_on:
#       - db
#
#   db:
#     image: postgres:16
#     environment:
#       POSTGRES_USER: sonar
#       POSTGRES_PASSWORD: sonar
#       POSTGRES_DB: sonar
#     volumes:
#       - postgresql_data:/var/lib/postgresql/data

# docker compose up -d
# เข้า http://localhost:9000 (admin/admin)

# 2. SonarScanner CLI
# npm install -g sonarqube-scanner
# หรือ
# brew install sonar-scanner

# 3. sonar-project.properties
cat > sonar-project.properties << 'EOF'
sonar.projectKey=my-hexagonal-app
sonar.projectName=My Hexagonal App
sonar.projectVersion=1.0

# Source
sonar.sources=src
sonar.tests=tests
sonar.sourceEncoding=UTF-8

# Language
sonar.language=py
sonar.python.version=3.11

# Coverage
sonar.python.coverage.reportPaths=coverage.xml
sonar.python.xunit.reportPath=test-results.xml

# Exclusions
sonar.exclusions=**/migrations/**,**/tests/**,**/__pycache__/**
sonar.test.exclusions=**/migrations/**

# Quality Gate
sonar.qualitygate.wait=true
EOF

# 4. รัน Analysis
# sonar-scanner \
#   -Dsonar.host.url=http://localhost:9000 \
#   -Dsonar.login=your-token

# 5. GitHub Actions Integration
# .github/workflows/sonar.yml
# name: SonarQube Analysis
# on: [push, pull_request]
# jobs:
#   sonar:
#     runs-on: ubuntu-latest
#     steps:
#       - uses: actions/checkout@v4
#         with:
#           fetch-depth: 0
#       - uses: actions/setup-python@v5
#         with:
#           python-version: '3.11'
#       - run: |
#           pip install -r requirements.txt
#           pytest --cov=src --cov-report=xml tests/
#       - uses: SonarSource/sonarqube-scan-action@master
#         env:
#           SONAR_TOKEN: }
#           SONAR_HOST_URL: }

echo "SonarQube configured"
echo "  URL: http://localhost:9000"
echo "  Scanner: sonar-scanner CLI"
echo "  CI/CD: GitHub Actions integration"

Hexagonal Architecture Implementation

# === Hexagonal Architecture ด้วย Python ===
# Project Structure:
# src/
#   domain/         # Business Logic (ไม่พึ่ง External)
#     models.py
#     services.py
#     ports.py      # Interfaces
#   adapters/       # External Implementations
#     repositories/
#       postgres_user_repo.py
#       inmemory_user_repo.py
#     notifications/
#       email_service.py
#       slack_service.py
#   api/            # Input Adapters (REST, GraphQL)
#     routes.py
#   config/
#     container.py  # Dependency Injection

# === domain/models.py ===
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional
from enum import Enum

class UserRole(Enum):
    ADMIN = "admin"
    MEMBER = "member"
    VIEWER = "viewer"

@dataclass
class User:
    id: Optional[str] = None
    email: str = ""
    name: str = ""
    role: UserRole = UserRole.MEMBER
    is_active: bool = True
    created_at: datetime = field(default_factory=datetime.now)

    def activate(self):
        self.is_active = True

    def deactivate(self):
        self.is_active = False

    def promote(self, role: UserRole):
        if role == UserRole.ADMIN and self.role != UserRole.MEMBER:
            raise ValueError("Only members can be promoted to admin")
        self.role = role

# === domain/ports.py (Interfaces) ===
from abc import ABC, abstractmethod
from typing import List, Optional

class UserRepository(ABC):
    """Port สำหรับ User Data Access"""

    @abstractmethod
    def save(self, user: User) -> User:
        pass

    @abstractmethod
    def find_by_id(self, user_id: str) -> Optional[User]:
        pass

    @abstractmethod
    def find_by_email(self, email: str) -> Optional[User]:
        pass

    @abstractmethod
    def find_all(self, limit: int = 100, offset: int = 0) -> List[User]:
        pass

    @abstractmethod
    def delete(self, user_id: str) -> bool:
        pass

class NotificationService(ABC):
    """Port สำหรับ Notifications"""

    @abstractmethod
    def send(self, to: str, subject: str, body: str) -> bool:
        pass

class EventPublisher(ABC):
    """Port สำหรับ Domain Events"""

    @abstractmethod
    def publish(self, event_type: str, data: dict) -> None:
        pass

# === domain/services.py (Business Logic) ===
class UserService:
    """Domain Service — Business Logic เท่านั้น"""

    def __init__(self, repo: UserRepository, notifier: NotificationService,
                 publisher: EventPublisher):
        self.repo = repo
        self.notifier = notifier
        self.publisher = publisher

    def register_user(self, email: str, name: str) -> User:
        existing = self.repo.find_by_email(email)
        if existing:
            raise ValueError(f"Email already registered: {email}")

        user = User(email=email, name=name)
        saved = self.repo.save(user)

        self.notifier.send(email, "Welcome!", f"สวัสดี {name}")
        self.publisher.publish("user.registered", {"user_id": saved.id})

        return saved

    def get_user(self, user_id: str) -> User:
        user = self.repo.find_by_id(user_id)
        if not user:
            raise ValueError(f"User not found: {user_id}")
        return user

    def deactivate_user(self, user_id: str) -> User:
        user = self.get_user(user_id)
        user.deactivate()
        self.repo.save(user)
        self.publisher.publish("user.deactivated", {"user_id": user_id})
        return user

print("Hexagonal Architecture:")
print("  Domain: Models, Services, Ports (no external deps)")
print("  Adapters: Repositories, Notifications, API")
print("  Ports: UserRepository, NotificationService, EventPublisher")

Adapters และ Tests

# === Adapters (Implementations) ===

# adapters/repositories/inmemory_user_repo.py
import uuid
from typing import List, Optional

class InMemoryUserRepository:
    """In-memory Implementation สำหรับ Testing"""

    def __init__(self):
        self.users = {}

    def save(self, user):
        if not user.id:
            user.id = str(uuid.uuid4())
        self.users[user.id] = user
        return user

    def find_by_id(self, user_id):
        return self.users.get(user_id)

    def find_by_email(self, email):
        for user in self.users.values():
            if user.email == email:
                return user
        return None

    def find_all(self, limit=100, offset=0):
        all_users = list(self.users.values())
        return all_users[offset:offset + limit]

    def delete(self, user_id):
        if user_id in self.users:
            del self.users[user_id]
            return True
        return False

# adapters/notifications/fake_notifier.py
class FakeNotificationService:
    """Fake Notification สำหรับ Testing"""

    def __init__(self):
        self.sent = []

    def send(self, to, subject, body):
        self.sent.append({"to": to, "subject": subject, "body": body})
        return True

# adapters/events/fake_publisher.py
class FakeEventPublisher:
    """Fake Event Publisher สำหรับ Testing"""

    def __init__(self):
        self.events = []

    def publish(self, event_type, data):
        self.events.append({"type": event_type, "data": data})

# === Tests ===
# tests/test_user_service.py

def test_register_user():
    """ทดสอบ Register User"""
    repo = InMemoryUserRepository()
    notifier = FakeNotificationService()
    publisher = FakeEventPublisher()
    service = UserService(repo, notifier, publisher)

    user = service.register_user("test@example.com", "Test User")

    assert user.id is not None
    assert user.email == "test@example.com"
    assert user.name == "Test User"
    assert user.is_active is True
    assert len(notifier.sent) == 1
    assert len(publisher.events) == 1
    assert publisher.events[0]["type"] == "user.registered"
    print("  PASS: test_register_user")

def test_register_duplicate_email():
    """ทดสอบ Register Email ซ้ำ"""
    repo = InMemoryUserRepository()
    notifier = FakeNotificationService()
    publisher = FakeEventPublisher()
    service = UserService(repo, notifier, publisher)

    service.register_user("test@example.com", "User 1")

    try:
        service.register_user("test@example.com", "User 2")
        assert False, "Should raise ValueError"
    except ValueError:
        pass
    print("  PASS: test_register_duplicate_email")

def test_deactivate_user():
    """ทดสอบ Deactivate User"""
    repo = InMemoryUserRepository()
    notifier = FakeNotificationService()
    publisher = FakeEventPublisher()
    service = UserService(repo, notifier, publisher)

    user = service.register_user("test@example.com", "Test")
    deactivated = service.deactivate_user(user.id)

    assert deactivated.is_active is False
    print("  PASS: test_deactivate_user")

# รัน Tests
print("\nRunning Tests:")
test_register_user()
test_register_duplicate_email()
test_deactivate_user()
print("\nAll tests passed!")

Best Practices

SonarQube คืออะไร

Open-source Platform วิเคราะห์ Code Quality ตรวจจับ Bugs Code Smells Vulnerabilities Duplications รองรับ 30+ ภาษา Quality Gates มาตรฐาน CI/CD Community Edition ฟรี

Hexagonal Architecture คืออะไร

Ports and Adapters แยก Business Logic ออกจาก External Systems ใช้ Ports Interfaces เป็นตัวกลาง Adapters เชื่อม External ทดสอบง่าย เปลี่ยน Technology ไม่กระทบ Domain

Quality Gate คืออะไร

เกณฑ์ว่า Code ผ่านมาตรฐานหรือไม่ Coverage 80%+ ไม่มี Critical Bugs Code Smells ไม่เกิน 5 Duplications ต่ำกว่า 3% ไม่ผ่าน Block Deploy CI/CD

Ports and Adapters Pattern ทำงานอย่างไร

Port เป็น Interface ที่ Domain กำหนด เช่น UserRepository Adapter เป็น Implementation เชื่อม External เช่น PostgresUserRepository Domain ใช้แค่ Port เปลี่ยน Adapter ได้ง่าย

สรุป

SonarQube ร่วมกับ Hexagonal Architecture ให้ Code ที่มีคุณภาพและ Architecture ที่สะอาด Quality Gates กำหนดมาตรฐาน Domain Isolation แยก Business Logic Ports and Adapters เชื่อม External Systems Test with Fakes สำหรับ Unit Tests CI/CD Gate Block Deploy ถ้าไม่ผ่าน

อ. บ

อ. บอมกิตติทัศน์เจริญพนาสิทธิ์

IT Infrastructure Expert 30+ ปี | ผู้ก่อตั้ง SiamCafe.net (1997)

siamcafe.net ·

📖 บทความที่เกี่ยวข้อง

SonarQube Analysis Disaster Recovery Planอ่านบทความ → SonarQube Analysis Cache Strategy Redisอ่านบทความ → SonarQube Analysis Message Queue Designอ่านบทความ → BGP Routing Advanced Hexagonal Architectureอ่านบทความ → Ansible Collection Hexagonal Architectureอ่านบทความ →

📚 ดูบทความทั้งหมด →