本节摘要:单个类型会写了,多个类型怎么组织成框架?本节把两种最实用的模式搬进 Fortran:策略模式让求解算法可替换,工厂模式把对象创建收进单一入口。同时讲清多态对象的内存所有权纪律——谁分配、谁释放必须写明白。最后给出判断标准:模式是为了澄清意图,不是为了增加抽象层。
线性方程组 Ax=b 的解法没有银弹:矩阵稠密用直接法,大型稀疏用迭代法,迭代收敛慢再换预处理。同一个求解流程,算法在运行时才确定。如果主程序写死某个算法,换方法就得改代码重编译。设计模式要解决的正是这种"变化"——把算法变成可替换的对象,主逻辑不动。
策略模式的骨架:一个抽象策略类型声明接口,多个具体策略实现算法,上下文持有抽象策略的引用。用第 5 章的类型扩展就能搭起来。
module solver_mod use iso_fortran_env, only: real64 implicit none type, abstract :: solver_t contains procedure(solve_i), deferred :: solve end type solver_t abstract interface subroutine solve_i(self, a, b, x) import :: solver_t, real64 class(solver_t), intent(in) :: self real(real64), intent(in) :: a(:,:), b(:) real(real64), intent(out) :: x(:) end subroutine solve_i end interface type, extends(solver_t) :: jacobi_solver_t integer :: max_iter = 1000 contains procedure :: solve => jacobi_solve end type jacobi_solver_t type, extends(solver_t) :: gauss_seidel_t integer :: max_iter = 1000 contains procedure :: solve => gs_solve end type gauss_seidel_t contains subroutine jacobi_solve(self, a, b, x) class(jacobi_solver_t), intent(in) :: self real(real64), intent(in) :: a(:,:), b(:) real(real64), intent(out) :: x(:) integer :: n, i, it real(real64), allocatable :: xnew(:) n = size(b) allocate(xnew(n)) x = 0.0_real64 do it = 1, self%max_iter do i = 1, n xnew(i) = (b(i) - sum(a(i,:) * x) + a(i,i) * x(i)) / a(i,i) end do if (maxval(abs(xnew - x)) < 1.0e-10_real64) exit x = xnew end do end subroutine jacobi_solve subroutine gs_solve(self, a, b, x) class(gauss_seidel_t), intent(in) :: self real(real64), intent(in) :: a(:,:), b(:) real(real64), intent(out) :: x(:) integer :: n, i, it n = size(b) x = 0.0_real64 do it = 1, self%max_iter do i = 1, n x(i) = (b(i) - sum(a(i,:) * x) + a(i,i) * x(i)) / a(i,i) end do if (maxval(abs(matmul(a, x) - b)) < 1.0e-10_real64) exit end do end subroutine gs_solve end module solver_mod
调用方只持有 class(solver_t), allocatable :: s,运行期决定 allocate 哪个子类。换算法 = 换一行 allocate,主流程一行不改。这就是策略模式的收益:算法实验的成本降到最低——想对比 Jacobi 和高斯赛德尔的收敛速度,写两个 allocate 跑两遍就行。
创建对象往往不只是 allocate:还要读配置、分配工作数组、设置并行上下文。把这些散在业务里,重复又难维护。工厂函数把这些收进一个入口,按配置返回具体对象。
module solver_factory_mod use solver_mod use iso_fortran_env, only: real64 implicit none contains ! 工厂:按配置字符串创建具体求解器 function make_solver(kind, max_iter) result(s) character(*), intent(in) :: kind integer, intent(in) :: max_iter class(solver_t), allocatable :: s select case (kind) case ('jacobi') allocate(jacobi_solver_t :: s) case ('gauss_seidel') allocate(gauss_seidel_t :: s) case default error stop 'unknown solver kind' end select select type (s) type is (jacobi_solver_t) s%max_iter = max_iter type is (gauss_seidel_t) s%max_iter = max_iter end select end function make_solver end module solver_factory_mod program use_factory use solver_factory_mod, only: make_solver use solver_mod, only: solver_t use iso_fortran_env, only: real64 implicit none class(solver_t), allocatable :: s real(real64) :: a(2,2), b(2), x(2) a = reshape([4.0_real64, 1.0_real64, 1.0_real64, 3.0_real64], [2,2]) b = [1.0_real64, 2.0_real64] s = make_solver('jacobi', 200) ! 从配置字符串创建 call s%solve(a, b, x) write(*,*) 'x =', x end program use_factory
工厂的意义不在省几行代码,而在把"创建策略"集中管理:以后创建流程加参数校验、加资源预加载,只改工厂一处。调用方与具体类型彻底解耦——这也是插件式架构的雏形:第三方实现自己的 solver_t 扩展,通过工厂接入,核心代码不动。
多态对象必须用 allocatable 或 pointer 承载,这就把内存所有权摆上台面。两条铁律:一是谁分配谁负责(或委托给 finalizer);二是工厂返回的对象,调用方要清楚自己持有并最终释放。allocatable 多态在离开作用域时自动释放,比指针省心——所以能 allocatable 就别 pointer。若用指针,给类型配 finalizer 释放内部资源,否则泄漏会在长跑模拟里慢慢吃掉内存。
| 承载方式 | 自动释放 | 适合场景 |
|---|---|---|
| allocatable 多态 | 是 | 工厂返回、局部使用、生命周期与作用域一致 |
| pointer 多态 | 否,需 finalizer | 对象跨作用域共享、链式结构 |
设计模式是给"变化"准备的,没有变化就别硬套。判断标准有三条:加新实现时,主逻辑是否真的不用改?模式有没有让意图更清楚,而不是多一层看不懂的跳转?抽象的开销(多态调用、间接寻址)是否落在非热路径上?三条全过,值得用;有一条不满足,别用。Fortran 工程师对抽象有天然警惕是对的,但把"策略化求解器"这类成熟做法拒之门外,同样会错过它换来的可维护性。
阅读完本节,你应当能够:
行为差异用继承解决,类型差异呢?参数化派生类型登场。