Initial commit
This commit is contained in:
562
skills/odoo-connector-module-creator/references/patterns.md
Normal file
562
skills/odoo-connector-module-creator/references/patterns.md
Normal file
@@ -0,0 +1,562 @@
|
||||
# Design Patterns in Odoo Connectors
|
||||
|
||||
## 1. Template Method Pattern
|
||||
|
||||
**Usage**: Backend model orchestration
|
||||
|
||||
**Implementation**:
|
||||
```python
|
||||
# generic_connector provides template methods
|
||||
class GenericBackend(models.Model):
|
||||
def sync_orders(self):
|
||||
"""Template method."""
|
||||
self._pre_sync_validation()
|
||||
result = self._sync_orders_implementation() # Hook
|
||||
self._post_sync_actions()
|
||||
return result
|
||||
|
||||
def _sync_orders_implementation(self):
|
||||
"""Override this in concrete implementations."""
|
||||
raise NotImplementedError()
|
||||
|
||||
# Concrete connector overrides the hook
|
||||
class ShopifyBackend(models.Model):
|
||||
_inherit = 'generic.backend'
|
||||
|
||||
def _sync_orders_implementation(self):
|
||||
"""Shopify-specific implementation."""
|
||||
# Actual sync logic here
|
||||
pass
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- Enforces consistent workflow
|
||||
- Allows customization at specific points
|
||||
- Reduces code duplication
|
||||
|
||||
## 2. Adapter Pattern
|
||||
|
||||
**Usage**: API communication abstraction
|
||||
|
||||
**Implementation**:
|
||||
```python
|
||||
class ShopifyAdapter(GenericAdapter):
|
||||
"""Adapts Shopify API to generic interface."""
|
||||
|
||||
def get_orders(self, filters=None):
|
||||
"""Translate to Shopify API call."""
|
||||
# Shopify uses /admin/api/2024-01/orders.json
|
||||
endpoint = '/admin/api/2024-01/orders.json'
|
||||
response = self.get(endpoint, params=self._build_params(filters))
|
||||
|
||||
# Shopify wraps response in 'orders' key
|
||||
return response.get('orders', [])
|
||||
|
||||
def _build_params(self, filters):
|
||||
"""Transform generic filters to Shopify params."""
|
||||
params = {}
|
||||
if filters and 'created_after' in filters:
|
||||
params['created_at_min'] = filters['created_after']
|
||||
return params
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- Hides API differences
|
||||
- Provides consistent interface
|
||||
- Simplifies testing (mock adapter)
|
||||
|
||||
## 3. Strategy Pattern
|
||||
|
||||
**Usage**: Different sync strategies per backend
|
||||
|
||||
**Implementation**:
|
||||
```python
|
||||
class GenericImporter(Component):
|
||||
_name = 'generic.importer'
|
||||
|
||||
def run(self, external_id, force=False):
|
||||
"""Import strategy can vary."""
|
||||
if self._should_skip_import(external_id, force):
|
||||
return None
|
||||
|
||||
# Different strategies:
|
||||
# - Direct import
|
||||
# - Delayed import
|
||||
# - Batch import
|
||||
return self._import_record(external_id)
|
||||
|
||||
class RealtimeImporter(GenericImporter):
|
||||
"""Strategy: Import immediately."""
|
||||
_name = 'shopify.realtime.importer'
|
||||
|
||||
def _import_record(self, external_id):
|
||||
# Import synchronously
|
||||
pass
|
||||
|
||||
class BatchImporter(GenericImporter):
|
||||
"""Strategy: Queue for batch processing."""
|
||||
_name = 'shopify.batch.importer'
|
||||
|
||||
def _import_record(self, external_id):
|
||||
# Queue for later
|
||||
self.with_delay().import_record(external_id)
|
||||
```
|
||||
|
||||
## 4. Factory Pattern
|
||||
|
||||
**Usage**: Component selection based on context
|
||||
|
||||
**Implementation**:
|
||||
```python
|
||||
# Component framework acts as factory
|
||||
with backend.work_on('shopify.product.template') as work:
|
||||
# Factory automatically selects appropriate components
|
||||
adapter = work.component(usage='backend.adapter')
|
||||
# Returns ShopifyProductAdapter
|
||||
|
||||
importer = work.component(usage='record.importer')
|
||||
# Returns ShopifyProductImporter
|
||||
|
||||
mapper = work.component(usage='import.mapper')
|
||||
# Returns ShopifyProductImportMapper
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- Automatic component selection
|
||||
- Decoupled component creation
|
||||
- Easy to extend with new components
|
||||
|
||||
## 5. Observer Pattern
|
||||
|
||||
**Usage**: Webhook event handling
|
||||
|
||||
**Implementation**:
|
||||
```python
|
||||
# Event source (webhook controller)
|
||||
class WebhookController(http.Controller):
|
||||
@http.route('/shopify/webhook', type='json', auth='none')
|
||||
def webhook_handler(self):
|
||||
payload = request.jsonrequest
|
||||
event_type = request.httprequest.headers.get('X-Shopify-Topic')
|
||||
|
||||
# Notify observers
|
||||
webhook = request.env['generic.webhook'].sudo().create({
|
||||
'event_type': event_type,
|
||||
'payload': json.dumps(payload),
|
||||
'processing_status': 'pending'
|
||||
})
|
||||
webhook.with_delay().process_webhook()
|
||||
|
||||
# Observer (backend model)
|
||||
class ShopifyBackend(models.Model):
|
||||
def process_webhook(self, webhook):
|
||||
"""Observe and handle webhook events."""
|
||||
handlers = {
|
||||
'orders/create': self._handle_order_created,
|
||||
'products/update': self._handle_product_updated,
|
||||
}
|
||||
|
||||
handler = handlers.get(webhook.event_type)
|
||||
if handler:
|
||||
handler(webhook)
|
||||
```
|
||||
|
||||
## 6. Delegation Pattern
|
||||
|
||||
**Usage**: Multi-level model inheritance
|
||||
|
||||
**Implementation**:
|
||||
```python
|
||||
class GenericBackend(models.Model):
|
||||
_name = 'generic.backend'
|
||||
_inherits = {'connector.base.backend': 'connector_backend_id'}
|
||||
|
||||
connector_backend_id = fields.Many2one(
|
||||
'connector.base.backend',
|
||||
required=True,
|
||||
ondelete='cascade'
|
||||
)
|
||||
|
||||
# Delegates fields: name, version, etc.
|
||||
# Accessing backend.name actually accesses backend.connector_backend_id.name
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- Reuse existing model functionality
|
||||
- Avoid deep inheritance hierarchies
|
||||
- Maintain database normalization
|
||||
|
||||
## 7. Mapper Pattern (Data Transfer Object)
|
||||
|
||||
**Usage**: Data transformation between systems
|
||||
|
||||
**Implementation**:
|
||||
```python
|
||||
class ProductImportMapper(GenericImportMapper):
|
||||
_name = 'shopify.product.import.mapper'
|
||||
|
||||
# Simple mappings
|
||||
direct = [
|
||||
('title', 'name'),
|
||||
('vendor', 'manufacturer'),
|
||||
]
|
||||
|
||||
# Complex mapping
|
||||
@mapping
|
||||
def description(self, record):
|
||||
"""Transform HTML description to plain text."""
|
||||
html_desc = record.get('body_html', '')
|
||||
return {'description': self._strip_html(html_desc)}
|
||||
|
||||
@mapping
|
||||
def price(self, record):
|
||||
"""Extract price from variants."""
|
||||
variants = record.get('variants', [])
|
||||
if variants:
|
||||
return {'list_price': float(variants[0].get('price', 0))}
|
||||
return {}
|
||||
|
||||
@only_create
|
||||
def default_code(self, record):
|
||||
"""Set SKU only when creating."""
|
||||
return {'default_code': record.get('sku')}
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- Centralized data transformation
|
||||
- Declarative mapping definitions
|
||||
- Reusable transformations
|
||||
|
||||
## 8. Retry Pattern
|
||||
|
||||
**Usage**: Handling transient failures
|
||||
|
||||
**Implementation**:
|
||||
```python
|
||||
class GenericBinding(models.AbstractModel):
|
||||
retry_count = fields.Integer(default=0)
|
||||
max_retries = fields.Integer(default=3)
|
||||
|
||||
def can_retry_sync(self):
|
||||
"""Check if retry is allowed."""
|
||||
return self.retry_count < self.max_retries
|
||||
|
||||
def export_with_retry(self):
|
||||
"""Export with automatic retry."""
|
||||
try:
|
||||
self.export_record()
|
||||
self.mark_sync_success()
|
||||
except Exception as e:
|
||||
self.retry_count += 1
|
||||
self.last_error = str(e)
|
||||
|
||||
if self.can_retry_sync():
|
||||
# Retry with exponential backoff
|
||||
delay = 60 * (2 ** self.retry_count)
|
||||
self.with_delay(eta=delay).export_with_retry()
|
||||
else:
|
||||
self.mark_sync_failed(str(e))
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- Resilient to temporary failures
|
||||
- Configurable retry behavior
|
||||
- Exponential backoff prevents API overload
|
||||
|
||||
## 9. Rate Limiting Pattern
|
||||
|
||||
**Usage**: Respect API rate limits
|
||||
|
||||
**Implementation**:
|
||||
```python
|
||||
from datetime import datetime, timedelta
|
||||
from cachetools import TTLCache
|
||||
|
||||
class RateLimitedAdapter(GenericAdapter):
|
||||
# Class-level cache (shared across instances)
|
||||
_rate_limit_cache = TTLCache(maxsize=100, ttl=60)
|
||||
|
||||
def make_request(self, method, endpoint, **kwargs):
|
||||
"""Make request with rate limiting."""
|
||||
cache_key = f"{self.backend_record.id}:requests"
|
||||
|
||||
# Get request count in current window
|
||||
request_count = self._rate_limit_cache.get(cache_key, 0)
|
||||
max_requests = self.backend_record.rate_limit_calls or 100
|
||||
|
||||
if request_count >= max_requests:
|
||||
# Wait for next window
|
||||
raise RateLimitExceeded(f"Rate limit of {max_requests}/min exceeded")
|
||||
|
||||
# Make request
|
||||
response = super().make_request(method, endpoint, **kwargs)
|
||||
|
||||
# Increment counter
|
||||
self._rate_limit_cache[cache_key] = request_count + 1
|
||||
|
||||
return response
|
||||
```
|
||||
|
||||
**Variants**:
|
||||
- **Token Bucket**: For bursty traffic
|
||||
- **Leaky Bucket**: For steady rate
|
||||
- **Sliding Window**: For precise limits
|
||||
|
||||
## 10. Circuit Breaker Pattern
|
||||
|
||||
**Usage**: Prevent cascading failures
|
||||
|
||||
**Implementation**:
|
||||
```python
|
||||
class CircuitBreaker:
|
||||
def __init__(self, failure_threshold=5, timeout=60):
|
||||
self.failure_threshold = failure_threshold
|
||||
self.timeout = timeout
|
||||
self.failures = 0
|
||||
self.last_failure_time = None
|
||||
self.state = 'CLOSED' # CLOSED, OPEN, HALF_OPEN
|
||||
|
||||
def call(self, func, *args, **kwargs):
|
||||
if self.state == 'OPEN':
|
||||
if datetime.now() - self.last_failure_time > timedelta(seconds=self.timeout):
|
||||
self.state = 'HALF_OPEN'
|
||||
else:
|
||||
raise CircuitBreakerOpen("Circuit breaker is OPEN")
|
||||
|
||||
try:
|
||||
result = func(*args, **kwargs)
|
||||
if self.state == 'HALF_OPEN':
|
||||
self.state = 'CLOSED'
|
||||
self.failures = 0
|
||||
return result
|
||||
except Exception as e:
|
||||
self.failures += 1
|
||||
self.last_failure_time = datetime.now()
|
||||
|
||||
if self.failures >= self.failure_threshold:
|
||||
self.state = 'OPEN'
|
||||
raise
|
||||
|
||||
class ResilientAdapter(GenericAdapter):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.circuit_breaker = CircuitBreaker()
|
||||
|
||||
def make_request(self, *args, **kwargs):
|
||||
return self.circuit_breaker.call(
|
||||
super().make_request,
|
||||
*args,
|
||||
**kwargs
|
||||
)
|
||||
```
|
||||
|
||||
## 11. Saga Pattern
|
||||
|
||||
**Usage**: Distributed transaction management
|
||||
|
||||
**Implementation**:
|
||||
```python
|
||||
class OrderImportSaga:
|
||||
"""Multi-step order import with compensation."""
|
||||
|
||||
def __init__(self, backend, external_order_id):
|
||||
self.backend = backend
|
||||
self.external_order_id = external_order_id
|
||||
self.steps_completed = []
|
||||
|
||||
def execute(self):
|
||||
"""Execute saga steps."""
|
||||
try:
|
||||
# Step 1: Import customer
|
||||
customer = self._import_customer()
|
||||
self.steps_completed.append(('customer', customer.id))
|
||||
|
||||
# Step 2: Import order
|
||||
order = self._import_order(customer)
|
||||
self.steps_completed.append(('order', order.id))
|
||||
|
||||
# Step 3: Import order lines
|
||||
lines = self._import_order_lines(order)
|
||||
self.steps_completed.append(('lines', [l.id for l in lines]))
|
||||
|
||||
# Step 4: Confirm order
|
||||
order.action_confirm()
|
||||
|
||||
return order
|
||||
|
||||
except Exception as e:
|
||||
# Compensate (rollback completed steps)
|
||||
self._compensate()
|
||||
raise
|
||||
|
||||
def _compensate(self):
|
||||
"""Rollback completed steps."""
|
||||
for step_type, record_ids in reversed(self.steps_completed):
|
||||
if step_type == 'order':
|
||||
self.env['sale.order'].browse(record_ids).action_cancel()
|
||||
elif step_type == 'customer':
|
||||
# Mark as not synced (don't delete)
|
||||
partner = self.env['res.partner'].browse(record_ids)
|
||||
partner.write({'active': False})
|
||||
```
|
||||
|
||||
## 12. Repository Pattern
|
||||
|
||||
**Usage**: Centralize data access logic
|
||||
|
||||
**Implementation**:
|
||||
```python
|
||||
class ProductBindingRepository:
|
||||
"""Repository for product bindings."""
|
||||
|
||||
def __init__(self, env, backend):
|
||||
self.env = env
|
||||
self.backend = backend
|
||||
self.model = env['shopify.product.template']
|
||||
|
||||
def find_by_external_id(self, external_id):
|
||||
"""Find binding by external ID."""
|
||||
return self.model.search([
|
||||
('backend_id', '=', self.backend.id),
|
||||
('external_id', '=', str(external_id))
|
||||
], limit=1)
|
||||
|
||||
def find_by_sku(self, sku):
|
||||
"""Find binding by SKU."""
|
||||
return self.model.search([
|
||||
('backend_id', '=', self.backend.id),
|
||||
('default_code', '=', sku)
|
||||
], limit=1)
|
||||
|
||||
def find_or_create(self, external_id, defaults=None):
|
||||
"""Find existing or create new binding."""
|
||||
binding = self.find_by_external_id(external_id)
|
||||
if not binding:
|
||||
values = {'backend_id': self.backend.id, 'external_id': str(external_id)}
|
||||
if defaults:
|
||||
values.update(defaults)
|
||||
binding = self.model.create(values)
|
||||
return binding
|
||||
|
||||
def find_pending_export(self, limit=100):
|
||||
"""Find bindings pending export."""
|
||||
return self.model.search([
|
||||
('backend_id', '=', self.backend.id),
|
||||
('sync_status', '=', 'pending'),
|
||||
('external_id', '=', False)
|
||||
], limit=limit)
|
||||
```
|
||||
|
||||
## Pattern Selection Guidelines
|
||||
|
||||
| Use Case | Pattern | Reason |
|
||||
|----------|---------|--------|
|
||||
| Define sync workflow | Template Method | Consistent process, extensible hooks |
|
||||
| API communication | Adapter | Abstract API differences |
|
||||
| Different import modes | Strategy | Pluggable algorithms |
|
||||
| Select components | Factory | Automatic selection based on context |
|
||||
| Handle webhooks | Observer | Event-driven architecture |
|
||||
| Extend core models | Delegation | Reuse without deep inheritance |
|
||||
| Transform data | Mapper | Declarative transformations |
|
||||
| Handle failures | Retry + Circuit Breaker | Resilient operations |
|
||||
| Respect API limits | Rate Limiting | Prevent API throttling |
|
||||
| Multi-step operations | Saga | Rollback on failure |
|
||||
| Data access | Repository | Centralized queries |
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
### ❌ Direct Odoo Record Modification
|
||||
```python
|
||||
# BAD: Directly modify product
|
||||
product = self.env['product.template'].browse(product_id)
|
||||
product.write({'name': external_data['title']})
|
||||
```
|
||||
|
||||
```python
|
||||
# GOOD: Use binding
|
||||
binding = self.env['shopify.product.template'].search([
|
||||
('odoo_id', '=', product_id)
|
||||
])
|
||||
binding.write({'name': external_data['title']})
|
||||
```
|
||||
|
||||
### ❌ Synchronous Long Operations
|
||||
```python
|
||||
# BAD: Block user while importing 1000 products
|
||||
def import_all_products(self):
|
||||
for product_id in range(1, 1000):
|
||||
self.import_product(product_id) # Takes 30 minutes!
|
||||
```
|
||||
|
||||
```python
|
||||
# GOOD: Queue async job
|
||||
def import_all_products(self):
|
||||
self.with_delay().import_products_batch()
|
||||
```
|
||||
|
||||
### ❌ No Error Handling
|
||||
```python
|
||||
# BAD: Unhandled API errors crash sync
|
||||
def sync_orders(self):
|
||||
response = adapter.get_orders() # What if API is down?
|
||||
for order in response:
|
||||
self.import_order(order)
|
||||
```
|
||||
|
||||
```python
|
||||
# GOOD: Graceful error handling
|
||||
def sync_orders(self):
|
||||
try:
|
||||
response = adapter.get_orders()
|
||||
except requests.HTTPError as e:
|
||||
_logger.error("Failed to fetch orders: %s", e)
|
||||
return False
|
||||
|
||||
for order in response:
|
||||
try:
|
||||
self.import_order(order)
|
||||
except Exception as e:
|
||||
_logger.error("Failed to import order %s: %s", order['id'], e)
|
||||
continue # Continue with next order
|
||||
```
|
||||
|
||||
### ❌ Hardcoded Configuration
|
||||
```python
|
||||
# BAD: Hardcoded values
|
||||
API_URL = 'https://api.shopify.com'
|
||||
API_KEY = 'hardcoded-key-123'
|
||||
```
|
||||
|
||||
```python
|
||||
# GOOD: Backend configuration
|
||||
api_url = self.backend_record.api_url
|
||||
api_key = self.backend_record.api_key
|
||||
```
|
||||
|
||||
### ❌ God Object
|
||||
```python
|
||||
# BAD: Backend does everything
|
||||
class ShopifyBackend(models.Model):
|
||||
def sync_orders(self):
|
||||
# 500 lines of code doing:
|
||||
# - API calls
|
||||
# - Data transformation
|
||||
# - Validation
|
||||
# - Record creation
|
||||
# - Email notifications
|
||||
# etc.
|
||||
```
|
||||
|
||||
```python
|
||||
# GOOD: Separated concerns
|
||||
class ShopifyBackend(models.Model):
|
||||
def sync_orders(self):
|
||||
# Orchestration only
|
||||
with self.work_on('shopify.sale.order') as work:
|
||||
importer = work.component(usage='batch.importer')
|
||||
return importer.run()
|
||||
|
||||
# Adapter handles API
|
||||
# Mapper handles transformation
|
||||
# Importer handles record creation
|
||||
```
|
||||
Reference in New Issue
Block a user