模拟热力学过程及可扩展烧瓶混合物热力学性质计算Python库的最佳架构选型咨询
Great question! When building a Python library for thermodynamic property calculations—similar to how RDKit operates—you don’t need to go all-in on heavy architectures like full Domain-Driven Design (DDD) or hexagonal, but you can adapt their core ideas to create something flexible, extensible, and focused on computation. Here’s a practical, battle-tested approach tailored to your needs:
1. Core: Strategy Pattern for Swappable Calculation Methods
The strategy pattern is perfect here—it lets you define a common interface for each thermodynamic property, then implement different calculation methods as separate "strategies" (classes) that conform to that interface. This makes adding new methods trivial without modifying existing code.
For example, start with an abstract base class for a property like enthalpy:
from abc import ABC, abstractmethod from your_library.core import Mixture, ThermodynamicResult class EnthalpyCalculator(ABC): @abstractmethod def calculate(self, mixture: Mixture) -> ThermodynamicResult: """Calculate enthalpy for the given mixture.""" pass # Implement a specific method (e.g., ideal gas law) class IdealGasEnthalpyCalculator(EnthalpyCalculator): def calculate(self, mixture: Mixture) -> ThermodynamicResult: # Ideal gas enthalpy calculation logic here result_value = ... # Compute based on mixture temp, components, etc. return ThermodynamicResult(value=result_value, unit="kJ/mol", method="ideal_gas") # Add another method later (e.g., Peng-Robinson equation of state) class PengRobinsonEnthalpyCalculator(EnthalpyCalculator): def calculate(self, mixture: Mixture) -> ThermodynamicResult: # Real gas enthalpy calculation logic here pass
2. Modular Organization by Thermodynamic Property
Structure your codebase to group related calculators by property, making it easy to navigate and extend. A sample directory layout might look like this:
thermo_lib/ ├── core/ │ ├── mixture.py # Encapsulates mixture state (components, T, P, etc.) │ ├── result.py # Wraps calculation outputs (value, unit, method) │ └── units.py # Unit conversion utilities ├── properties/ │ ├── enthalpy/ │ │ ├── base.py # Abstract EnthalpyCalculator class │ │ ├── ideal_gas.py # Ideal gas implementation │ │ └── peng_robinson.py # Real gas implementation │ ├── entropy/ │ │ ├── base.py │ │ └── ideal_gas.py │ └── density/ │ ├── base.py │ └── liquid_density.py └── registry.py # Dynamic lookup for calculators
Adding a new property (e.g., viscosity) just requires creating a new properties/viscosity/ directory with its own base class and implementations.
3. Dynamic Calculator Registry
Add a lightweight registry to let users (or your own code) look up calculators by property type and method name. Use a decorator to register new calculators automatically:
from typing import Dict, Type class CalculatorRegistry: _registry: Dict[str, Dict[str, Type[ABC]]] = {} @classmethod def register(cls, property_type: str, method_name: str): def decorator(calculator_cls: Type[ABC]): if property_type not in cls._registry: cls._registry[property_type] = {} cls._registry[property_type][method_name] = calculator_cls return calculator_cls return decorator @classmethod def get_calculator(cls, property_type: str, method_name: str) -> Type[ABC]: return cls._registry[property_type][method_name] # Register your calculators like this @CalculatorRegistry.register("enthalpy", "ideal_gas") class IdealGasEnthalpyCalculator(EnthalpyCalculator): # ... implementation ...
Now users can fetch the right calculator with a simple call:
calculator = CalculatorRegistry.get_calculator("enthalpy", "peng_robinson") result = calculator().calculate(my_mixture)
4. Borrow DDD's Domain Modeling (Skip the Overhead)
You don’t need full DDD with aggregates and repositories, but defining clear domain objects will keep your code clean:
Mixture: Encapsulates all state data (components, temperature, pressure, phase) and provides helper methods (e.g.,get_mole_fractions(),convert_temperature()).ThermodynamicResult: Wraps the calculated value, units, and method used—making it easy to return consistent, self-documenting results.
This separation ensures calculators only focus on computation, not data validation or formatting.
5. Lightweight Ports & Adapters (Simplified Hexagonal)
If your library relies on external data (e.g., component critical properties from a database) or third-party tools, encapsulate those dependencies in separate modules. For example:
- Create a
data_sources/component_database.pyclass that handles fetching component data. - Calculators use this class instead of directly querying the database or API.
This way, if you ever switch from a local CSV database to an external API, you only need to update the ComponentDatabase class—no changes to your calculator logic required.
Final Thoughts
This approach mirrors RDKit’s philosophy: modular, focused on computation, and easy to extend. It avoids the overengineering of full DDD/hexagonal architectures while borrowing their most useful ideas for maintainability and flexibility. You’ll be able to add new properties and calculation methods quickly without disrupting existing code.
内容的提问来源于stack exchange,提问作者Pablo Sánchez González

