SiamCafe · Blog
SonarQube Analysis กับ Hexagonal Architecture —
บทความ

SonarQube Analysis กับ Hexagonal Architecture —

เผยแพร่ 28 พฤษภาคม 2569

SonarQube Code Quality Analysis

SonarQube Analysis กับ Hexagonal Architecture —

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

SonarQube Analysis กับ Hexagonal Architecture —
# === 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

  • Quality Gates: ตั้ง Quality Gate ให้ Coverage > 80%, ไม่มี Critical Bugs
  • Domain Isolation: Domain Layer ต้องไม่ Import อะไรจาก Adapters
  • Port-first: ออกแบบ Port (Interface) ก่อน แล้วค่อยสร้าง Adapter
  • Test with Fakes: ใช้ In-memory Adapters สำหรับ Unit Tests เร็วและ Isolated
  • CI/CD Gate: รัน SonarQube ใน CI/CD Block Deploy ถ้าไม่ผ่าน Quality Gate
  • Dependency Rule: Dependencies ชี้เข้าใน ไม่ออกนอก Domain ไม่รู้จัก Infrastructure

SonarQube คืออะไร

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