Post
JA EN

Explaining 'Balancing Coupling in Software Design': New Design Principles Beyond Loose Coupling Supremacy

Explaining 'Balancing Coupling in Software Design': New Design Principles Beyond Loose Coupling Supremacy
  • Target Audience: Software engineers, architects, technical leads
  • Prerequisites: Object-oriented programming, basic software design concepts
  • Reading Time: 25 minutes

Overview

“High cohesion, loose coupling” has been passed down as the golden rule of software design for over 50 years. However, in the modern era where microservices, distributed systems, and cloud-native architecture are mainstream, situations where this slogan alone cannot lead to appropriate design decisions are increasing.

Vlad Khononov’s “Balancing Coupling in Software Design”1 (published 2024) proposes a new approach beyond traditional “loose coupling supremacy”: Balancing Coupling.

This article explains the core concepts of this book and covers the following topics:

  • History and limitations of traditional coupling and cohesion concepts
  • The three dimensions of coupling (strength, distance, volatility)
  • Connascence: A refined measure of coupling
  • Cynefin theory and understanding complexity
  • Practical application of the coupling balance model

Note:

This article is not a book review based on reading the entire text of Vlad Khononov’s “Balancing Coupling in Software Design,” but rather a systematic explanation of core concepts of coupling balance (three dimensions of coupling, Connascence, Cynefin theory) researched from public information such as the official site (coupling.dev), book reviews, technical community explanations, and author interviews. It may not fully reflect the detailed nuances of the book or the author’s intentions.

Additionally, the code examples in this article are illustrative examples for explanation purposes and have not been executed to verify their operation. When using them in actual projects, please conduct appropriate testing and verification.

Why “Coupling Balance” Is Important Now

Limitations of the “Loose Coupling” Slogan

The slogan “loose coupling, high cohesion” was first proposed by Stevens, Myers, and Constantine in their 1974 paper “Structured Design”2. Even after more than 50 years, this principle is still widely supported.

However, in modern software development, problems that cannot be solved by this slogan alone are becoming apparent:

1. The Microservices Dilemma

Microservices architecture aims to reduce coupling between services, but excessive division causes the following problems:

  • Increased network latency
  • Complexity of distributed transactions
  • Difficulty in debugging and monitoring
  • Increased operational costs

2. The Cost of Abstraction

Aiming for complete loose coupling creates excessive abstraction layers:

  • Multi-layer architecture that’s difficult to understand
  • Unnecessary indirection
  • Performance overhead

3. Misjudging Change Costs

Not all coupling is equally bad. Coupling to stable components with low change frequency may actually have lower costs.

Khononov’s Proposal: Using Coupling as a Tool

Khononov redefines coupling not as “evil to avoid” but as “a tool to manage”13:

“Coupling can drive a software system toward complexity or toward modularity”

The key is not to aim for zero coupling, but to achieve appropriate balance according to the situation.

Traditional Coupling and Cohesion Concepts and Their Limitations

Structured Design (1974): The Birth of Coupling and Cohesion

Stevens, Myers, and Constantine’s paper2 introduced the following concepts:

Main Types of Coupling (weak → strong):

  1. Data Coupling: Passing simple data types as arguments
  2. Stamp Coupling: Passing structures but using only some fields
  3. Control Coupling: Passing control information to control behavior
  4. Common Coupling: Dependency on global variables
  5. Content Coupling: Direct dependency on internal implementation of other modules

Levels of Cohesion (low → high):

  1. Coincidental Cohesion: Collection of unrelated functions
  2. Logical Cohesion: Aggregating similar functions in one place
  3. Temporal Cohesion: Functions executed at the same time
  4. Procedural Cohesion: Functions executed in a specific order
  5. Communicational Cohesion: Functions operating on the same data
  6. Sequential Cohesion: Functions where output becomes the next input
  7. Functional Cohesion: Functions with a single clear purpose

Limitations of Traditional Classification

Structured Design’s classification was groundbreaking, but it has the following limitations:

1. Qualitative and Ambiguous

Expressions like “weak coupling” and “strong coupling” are qualitative, and specific judgment criteria are unclear.

2. One-dimensional Evaluation

It only evaluates coupling strength and doesn’t consider other important aspects (distance, volatility, etc.).

3. Doesn’t Address Modern Patterns

It cannot adequately evaluate modern patterns like Dependency Injection, event-driven architecture, and microservices.

The Three Dimensions of Coupling: Khononov’s Innovative Framework

Khononov proposes an innovative framework that evaluates coupling in three dimensions13.

1. Strength (Integration Strength)

Represents the density of coupling and degree of dependency.

Evaluation Criteria:

  • Weak coupling: Depends only on simple data types (int, string, etc.)
  • Moderate coupling: Depends on structures or interfaces
  • Strong coupling: Depends on specific implementation classes, global variables, or internal implementation

Example: Coupling Strength in Database Access

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Note: The following is pseudocode example to illustrate concepts

# Strong coupling: Direct dependency on specific database implementation
class OrderService:
    def __init__(self):
        self.db = MySQLConnection("localhost", "user", "pass")

    def create_order(self, order_data):
        self.db.execute("INSERT INTO orders ...")

# Weak coupling: Depends on abstraction (interface)
class OrderService:
    def __init__(self, repository: OrderRepository):
        self.repository = repository

    def create_order(self, order_data):
        self.repository.save(order_data)

2. Distance (Locality)

Represents the physical and logical separation between modules.

Levels of Distance (close → far):

  1. Within the same function/method
  2. Within the same class
  3. Within the same package/module
  4. Within the same application
  5. Between different services (microservices)
  6. Between systems of different organizations

Khononov’s Principle3:

“High strength coupling should have shortened distance”

Bad Example: Strong Coupling × Far Distance

1
2
3
4
5
6
7
8
9
10
11
12
import requests

# Microservice A (Order Service)
class OrderService:
    def process_order(self, order):
        # Depends on specific implementation of Microservice B
        user = requests.get(
            f"http://user-service/api/users/{order.user_id}"
        ).json()
        # Depends on internal data structure of user-service
        if user["subscription"]["tier"] == "premium":
            discount = 0.2

Problems:

  • Strong dependency on internal data structure of Microservice B (user-service) (subscription.tier)
  • If user-service changes its data structure, order-service breaks

Improved Example: Weak Coupling × Far Distance

1
2
3
4
5
6
7
8
9
import requests

# Microservice A (Order Service)
class OrderService:
    def process_order(self, order):
        # Depends on abstracted API
        user_discount = requests.get(
            f"http://user-service/api/users/{order.user_id}/discount"
        ).json()["discount_rate"]

3. Volatility

Represents changeability and scope of impact.

Levels of Volatility (stable → unstable):

  1. Standard library: Extremely low change frequency (e.g., Python standard library, Java SE API)
  2. Third-party library: Stable between major versions (e.g., Django, React)
  3. Internal shared library: Shared across projects, updated periodically
  4. Application-specific code: Frequently changed
  5. Business rules: Frequently changed due to market or regulatory changes

Khononov’s Principle3:

“Low volatility can tolerate high strength”

Example: Design Decisions Considering Volatility

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Acceptable: Strong coupling to standard library
import json
data = json.loads(json_string)  # Low change risk since it's standard library

# Problematic: Strong coupling to highly volatile business rules
class DiscountCalculator:
    def calculate(self, order):
        # Business rules directly hardcoded
        if order.total > 10000:
            return order.total * 0.1
        return 0

# Improved: Externalize business rules
class DiscountCalculator:
    def __init__(self, discount_rules: DiscountRulesRepository):
        self.rules = discount_rules

    def calculate(self, order):
        applicable_rules = self.rules.get_applicable_rules(order)
        return self._apply_rules(order, applicable_rules)

Three-Dimensional Coupling Evaluation Matrix

Coupling TypeStrengthDistanceVolatilityEvaluation
Dependency on standard libraryMed-HighCloseVery Low✅ Acceptable
Inter-class dependency within same module (via abstraction)Low-MedCloseMed✅ Acceptable
Abstracted API dependency between microservicesLowFarMed✅ Acceptable
Specific implementation dependency between microservicesHighFarHigh❌ Problem
Direct dependency on highly volatile business logic from multiple modulesHighMed-FarHigh❌ Problem

Connascence: A Refined Measure of Coupling

What Is Connascence?

Connascence is a concept proposed by Meilir Page-Jones in 19924, and is also explained in detail in Chapter 6 of Khononov’s book5.

Definition4:

Connascence between two software elements A and B exists when A requires a change (or careful checking) due to a change in B, or when both A and B need to be changed simultaneously.

Connascence is a modern reconstruction of Structured Design concepts as a unified metric for evaluating coupling and cohesion6.

The Three Dimensions of Connascence

Connascence itself is also evaluated in three dimensions5:

  1. Strength: Difficulty and cost of change
  2. Degree: Number of couplings (how many elements depend on it)
  3. Locality: Proximity between related elements

Types of Connascence (weak → strong)

Static Connascence

Connascence detectable at the source code level.

1. Connascence of Name (CoN)

Needs to reference the same name.

1
2
3
4
5
6
# Connascence of function name
def calculate_total(items):
    return sum(item.price for item in items)

# The caller depends on the name "calculate_total"
result = calculate_total(my_items)

The weakest connascence, easily changed with refactoring tools.

2. Connascence of Type (CoT)

Needs to use the same type.

1
2
3
def process_order(order: Order) -> OrderResult:
    # Depends on Order and OrderResult types
    ...

Detected by type checkers in statically typed languages.

3. Connascence of Meaning (CoM)

Specific values have specific meanings (magic numbers, magic strings).

1
2
3
4
5
6
7
# Bad example: Connascence of Meaning
if user.status == 1:
    send_email(user)

# Improved: To Connascence of Name
if user.status == UserStatus.ACTIVE:
    send_email(user)

4. Connascence of Position (CoP)

Order of elements matters.

1
2
3
4
5
6
7
8
9
10
# Bad example: Connascence of Position
create_user("John", "Doe", 30, "[email protected]")

# Improved: To Connascence of Name
create_user(
    first_name="John",
    last_name="Doe",
    age=30,
    email="[email protected]"
)

5. Connascence of Algorithm (CoA)

Needs to use the same algorithm.

1
2
3
# Encryption and decryption depend on the same algorithm
encrypted = encrypt(data, algorithm="AES-256")
decrypted = decrypt(encrypted, algorithm="AES-256")

Dynamic Connascence

Connascence detectable only at runtime. Stronger than static connascence.

6. Connascence of Execution (CoE)

Execution order matters.

1
2
3
4
5
6
7
8
# Bad example: Depends on execution order
open_connection()
execute_query()
close_connection()

# Improved: Guarantee order with context manager
with DatabaseConnection() as conn:
    conn.execute_query()

7. Connascence of Timing (CoTi)

Execution timing matters (race conditions).

1
2
3
4
5
6
7
8
9
10
# Bad example: Race condition
balance = account.get_balance()
if balance >= amount:
    account.withdraw(amount)  # Another thread may withdraw simultaneously

# Improved: Transaction
with transaction():
    balance = account.get_balance(lock=True)
    if balance >= amount:
        account.withdraw(amount)

8. Connascence of Value (CoV)

Multiple values need to maintain a specific relationship.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Bad example: Manually managing value consistency
class ShoppingCart:
    def __init__(self):
        self.items = []
        self.total = 0  # Managing items and total separately

    def add_item(self, item):
        self.items.append(item)
        self.total += item.price  # Manual sync

# Improved: Derive value
class ShoppingCart:
    def __init__(self):
        self.items = []

    @property
    def total(self):
        return sum(item.price for item in self.items)

9. Connascence of Identity (CoI)

Multiple elements need to reference the same object instance.

1
2
# Dependency on the same singleton instance
config = ConfigManager.get_instance()

Connascence Refactoring Principles

  1. Convert strong connascence to weak connascence
    • Connascence of Meaning → Connascence of Name
    • Connascence of Position → Connascence of Name
  2. Reduce Degree
    • If many modules depend on it, introduce abstraction
  3. Shorten Locality
    • Keep strong connascence within the same module

Cynefin Theory and Understanding Complexity

What Is the Cynefin Framework?

Chapter 2 of Khononov’s book7 explains how to use the Cynefin framework to understand complexity and make appropriate design decisions.

Cynefin (Welsh for “habitat”) is a decision-making framework developed by David J. Snowden in the late 1990s that classifies problems into four domains8:

The Four Domains of Cynefin

1. Simple Domain

Characteristics:

  • Clear and predictable cause-and-effect relationships
  • Best practices exist
  • “Sense → Categorize → Respond” approach

Software Design Examples:

  • CRUD operation implementation
  • Standard data validation
  • Applying established design patterns

Coupling Judgment:

  • Follow standard patterns
  • Apply known best practices
  • No need for excessive abstraction

2. Complicated Domain

Characteristics:

  • Cause-and-effect determined through analysis
  • Expertise required
  • Multiple correct answers possible
  • “Sense → Analyze → Respond” approach

Software Design Examples:

  • Performance optimization
  • Security implementation
  • Database schema design

Coupling Judgment:

  • Carefully evaluate three dimensions (strength, distance, volatility) based on expertise
  • Carefully consider trade-offs

3. Complex Domain

Characteristics:

  • Complex cause-and-effect, unpredictable in advance
  • Exploration and experimentation required
  • Emergent solutions
  • “Probe → Sense → Respond” approach

Software Design Examples:

  • Modeling new domains
  • Determining microservice boundaries
  • Innovative architecture patterns

Coupling Judgment:

  • Start small and improve iteratively
  • Early feedback loops
  • Don’t make excessive assumptions

4. Chaotic Domain

Characteristics:

  • Unclear cause-and-effect
  • Immediate action required
  • “Act → Sense → Respond” approach

Software Design Examples:

  • Production incident response
  • Security incident response

Coupling Judgment:

  • Stabilization is top priority
  • Improve later (first make it work)

Relationship Between Cynefin and Coupling Balance

Khononov uses Cynefin theory to propose appropriate coupling strategies based on system complexity levels7:

DomainRecommended Coupling Strategy
SimpleFollow standard patterns, avoid excessive abstraction
ComplicatedCarefully perform 3D evaluation, balance with expertise
ComplexPrioritize flexibility, design for easy early feedback
ChaoticStabilize first, refactor later

Practical Example: Determining Microservice Boundaries

Determining microservice boundaries is a Complex domain problem:

  1. Probe: Analyze existing codebase, business capabilities, team structure
  2. Sense: Experimentally split with small-scale boundaries
  3. Respond: Adjust boundaries based on feedback

Throughout this process, continuously evaluate and adjust the balance of the three dimensions of coupling.

Practicing the Coupling Balance Model

Basic Principles of Balancing

Khononov presents the following practical judgment criteria3:

Acceptable Coupling:

  1. Low strength and low volatility
    • Example: Dependency on standard library
    • Reason: Extremely low change risk
  2. High strength but close distance
    • Example: Inter-method dependency within the same class
    • Reason: Limited scope of change impact
  3. Far distance but low strength and low volatility
    • Example: Abstracted API dependency between microservices
    • Reason: Contract is stable

Problematic Coupling:

  1. High strength, far distance, and high volatility
    • Example: Dependency on specific implementation between microservices
    • Reason: Change impact spreads widely

Case Study: Refactoring Layered Architecture

Let’s look at practicing coupling balance using a typical 3-tier architecture (Presentation, Business Logic, Data Access) as an example.

Problematic Design: Tight Coupling

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Presentation Layer
class OrderController:
    def create_order(self, request):
        # Direct dependency on specific class in Business Logic layer
        service = OrderService()
        # Direct dependency on specific class in Data Access layer
        service.repository = MySQLOrderRepository()
        return service.process_order(request.data)

# Business Logic Layer
class OrderService:
    def __init__(self):
        # Direct dependency on specific implementation in Data Access layer
        self.repository = MySQLOrderRepository()
        self.email_sender = SMTPEmailSender()

    def process_order(self, order_data):
        order = self.repository.save(order_data)
        self.email_sender.send_confirmation(order)
        return order

Problems:

  • Presentation layer directly depends on Data Access layer (layer boundary violation)
  • Strongly coupled to specific implementation classes (MySQL, SMTP)
  • Difficult to test (requires actual database and email server)

3D Evaluation:

CouplingStrengthDistanceVolatilityProblem
Controller → MySQLOrderRepositoryHighFarMed
OrderService → MySQLOrderRepositoryHighMedMed
OrderService → SMTPEmailSenderHighMedMed

Improved Design: Appropriate Balance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
from abc import ABC, abstractmethod

# Domain Layer (Abstraction)
class OrderRepository(ABC):
    @abstractmethod
    def save(self, order_data) -> Order:
        pass

class EmailSender(ABC):
    @abstractmethod
    def send_confirmation(self, order: Order):
        pass

# Business Logic Layer
class OrderService:
    def __init__(
        self,
        repository: OrderRepository,
        email_sender: EmailSender
    ):
        self.repository = repository
        self.email_sender = email_sender

    def process_order(self, order_data):
        order = self.repository.save(order_data)
        self.email_sender.send_confirmation(order)
        return order

# Data Access Layer (Specific Implementation)
class MySQLOrderRepository(OrderRepository):
    def save(self, order_data) -> Order:
        # MySQL-specific implementation
        ...

class SMTPEmailSender(EmailSender):
    def send_confirmation(self, order: Order):
        # SMTP-specific implementation
        ...

# Presentation Layer
class OrderController:
    def __init__(self, order_service: OrderService):
        self.service = order_service

    def create_order(self, request):
        return self.service.process_order(request.data)

# Dependency Injection (DI Container)
def configure_dependencies():
    repository = MySQLOrderRepository()
    email_sender = SMTPEmailSender()
    service = OrderService(repository, email_sender)
    controller = OrderController(service)
    return controller

3D Evaluation After Improvement:

CouplingStrengthDistanceVolatilityEvaluation
OrderService → OrderRepository (abstract)LowCloseLow
OrderService → EmailSender (abstract)LowCloseLow
Controller → OrderServiceMedCloseMed

Benefits:

  • Clear layer boundaries
  • Reduced strength through dependency on abstractions (interfaces)
  • Easy to test (can use mocks and stubs)
  • Business Logic layer unaffected by changes to database or email server implementation

Case Study: Determining Microservice Boundaries

An example of applying coupling balance when converting an EC site’s monolithic application to microservices.

Scenario:

The following domain areas exist:

  • Product Catalog
  • Inventory
  • Order
  • Customer
  • Payment

Step 1: Cynefin Classification

Determining microservice boundaries is Complex domain. Exploration and experimentation required.

Step 2: Evaluate with Three Dimensions of Coupling

Inter-service RelationshipStrengthDistanceVolatilityEvaluation
Order ↔ CustomerMedFarMed⚠️
Order ↔ PaymentHighFarHigh
Order ↔ InventoryHighFarHigh
Product ↔ InventoryMedFarMed⚠️

Step 3: Boundary Adjustment

Option A: Integrate Order, Payment, and Inventory

Since there’s strong coupling between Order, Payment, and Inventory, combine them into a single service.

  • Pros: Clear transaction boundaries, no network latency
  • Cons: Service size becomes larger, independent deployment difficult

Option B: Introduce Event-Driven Architecture

Adopt event-driven pattern to reduce coupling strength.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Order Service
class OrderService:
    def create_order(self, order_data):
        # Create order
        order = self.repository.save(order_data)

        # Publish event (weak coupling)
        self.event_bus.publish(OrderCreatedEvent(order))

        return order

# Payment Service (Event Listener)
class PaymentService:
    def on_order_created(self, event: OrderCreatedEvent):
        # Start payment processing
        self.process_payment(event.order_id, event.amount)

# Inventory Service (Event Listener)
class InventoryService:
    def on_order_created(self, event: OrderCreatedEvent):
        # Reserve inventory
        self.reserve_inventory(event.items)

3D Evaluation After Improvement:

Inter-service RelationshipStrengthDistanceVolatilityEvaluation
Order → Event BusLowFarLow
Payment ← Event BusLowFarLow
Inventory ← Event BusLowFarLow

Trade-offs:

  • Pros: Reduced coupling strength, independent deployment possible
  • Cons: Eventual consistency, increased debugging complexity

Step 4: Iterative Improvement

Since it’s a Complex domain problem, start small and adjust based on feedback:

  1. First isolate the most volatile area (e.g., Payment)
  2. Gain operational experience
  3. Re-evaluate coupling balance
  4. Adjust boundaries as needed

Summary

Vlad Khononov’s “Balancing Coupling in Software Design”1 proposes a new approach beyond the “loose coupling supremacy” that has continued for over 50 years.

Key Points of This Article

1. Three-Dimensional Coupling Evaluation

By evaluating coupling in three dimensions—Strength, Distance, and Volatility—more refined design decisions become possible.

2. Connascence

Connascence proposed by Meilir Page-Jones classifies coupling more finely and provides guidelines for refactoring.

3. Cynefin Theory and Complexity

Select appropriate coupling strategies according to system complexity level (Simple, Complicated, Complex, Chaotic).

4. Importance of Balance

The key is not to aim for “zero coupling,” but to achieve appropriate balance according to the situation.

Practical Design Principles

Acceptable Coupling:

  • Low strength and low volatility (e.g., standard library)
  • High strength but close distance (e.g., within the same class)
  • Far distance but low strength and low volatility (e.g., stable API)

Problematic Coupling:

  • High strength, far distance, and high volatility

Refactoring Guidelines:

  1. Strong coupling should shorten distance
  2. Reduce strength for coupling to highly volatile elements
  3. Convert connascence types to weaker ones

Modern Application

In the modern era where microservices, distributed systems, and cloud-native architecture are mainstream, the concept of coupling balance is extremely important.

  • Determining microservice boundaries: Judge optimal granularity with 3D evaluation
  • Event-driven architecture: Means to reduce coupling strength
  • Dependency Injection (DI): Technique to reduce strength while maintaining distance

Next Steps

The theory explained in this article can be applied to practical scenarios such as:

  • Refactoring legacy systems
  • Designing new architectures
  • Code review perspectives
  • Establishing team design principles

Related Article:

As Khononov states, what’s important in software design is not “loose coupling supremacy” but appropriate balance according to the situation. This principle will continue to hold value as a universal truth in software development.

References

Other References (Not Numbered in Text)

Resources consulted during article creation but not directly cited in the text.

On Citation Accuracy:

The research and materials cited in this article have been verified through the following methods:

  • Confirmation in academic databases (Google Scholar, ACM Digital Library, etc.)
  • Information verification on official websites
  • Cross-verification through multiple independent sources

Full PDF access may be restricted for some books, but book information, author information, and key concepts have been confirmed through official sources and reliable reviews and explanations.

  1. Balancing Coupling in Software Design: Universal Design Principles for Architecting Modular Software Systems - Vlad Khononov (2024). Addison-Wesley. [Reliability: High] Presents the three dimensions of coupling (strength, distance, volatility) and proposes the coupling balancing approach. ↩︎ ↩︎2 ↩︎3 ↩︎4

  2. Structured Design - Stevens, W. P., Myers, G. J., Constantine, L. L., IBM Systems Journal (1974). [Reliability: High] Classic paper that first proposed the concepts of coupling and cohesion. ↩︎ ↩︎2

  3. Balancing Coupling in Software Design: Core Concepts - Vlad Khononov (2024). [Reliability: High] Detailed explanation of the three dimensions of coupling (strength, distance, volatility). Official site. ↩︎ ↩︎2 ↩︎3 ↩︎4 ↩︎5

  4. Connascence - Wikipedia. [Reliability: Medium-High] Explains the concept of connascence proposed by Meilir Page-Jones in 1992. ↩︎ ↩︎2

  5. Book review reference (details of Chapter 6 on Connascence) ↩︎ ↩︎2

  6. Connascence: Coupling, Cohesion & Connascence - Khalil Stemmler. [Reliability: Medium] Practical explanation of connascence. ↩︎

  7. Balancing Coupling in Software Design - Chapter 2: Coupling and Complexity: Cynefin - Vlad Khononov (2024). [Reliability: High] Book Chapter 2. Explains Cynefin theory and complexity. ↩︎ ↩︎2

  8. Cynefin Framework - Wikipedia. [Reliability: Medium] Explains application of Cynefin theory to software development. ↩︎

This post is licensed under CC BY 4.0 by the author.