封装、Active Record等OOP概念:优劣场景与操作归属技术问询
OOP核心概念与实践疑问解析
大家常说面向对象编程(OOP)的核心是**封装(encapsulation)**与数据隐藏。以下通过示例说明相关设计问题:
有问题的Rectangle实现
class Rectangle { private int $a, $b; public function __construct(int $a, int $b) { $this->a = $a; $this->b = $b; } public function getA(): int { return $this->a; } public function getB(): int { return $this->b; } } $r = new Rectangle(3, 4); $area = $r->getA() * $r->getB();
上述代码的问题在于:外部代码需要依赖Rectangle的内部属性计算面积,破坏了封装原则,也让面积计算的逻辑散落在外部。
重构后的Rectangle实现
class Rectangle { private int $a, $b; public function __construct(int $a, int $b) { $this->a = $a; $this->b = $b; } public function getArea(): int { return $this->a * $this->b; } } $r = new Rectangle(3, 4); $area = $r->getArea();
重构后的代码真正实现了数据隐藏,将面积计算的逻辑归置到Rectangle类内部,外部只需调用getArea()方法即可,无需关心内部属性细节。
Active Record模式的两种实现与争议
Doctrine风格的写法
class Record { private int $ID; private string $username; public function __construct(int $ID, string $username) { $this->ID = $ID; $this->username = $username; } public function getID(): int { return $this->ID; } public function getUsername(): string { return $this->username; } } $r = new Record(1, 'test'); dbEngine->save($r);
这种写法的问题在于:通过Getter方法暴露了内部数据,相当于把对象变成了“数据容器”,违背了封装的核心——只暴露必要的行为而非数据。
Propel风格的写法
class Record { private int $ID; private string $username; public function __construct(int $ID, string $username) { $this->ID = $ID; $this->username = $username; } public function save() { dbEngine->save([$this->ID, $this->username]); } } $r = new Record(1, 'test'); $r->save();
这种写法虽然避免了Getter暴露数据,但Active Record模式本身被不少开发者视为不良实践,因为它让模型类同时承担了业务实体和数据持久化的职责,违反了关注点分离原则。
核心技术疑问解答
1. 各OOP概念何时成为不良实践?
- 特性依恋(Feature Envy):当一段代码更关心另一个对象的内部数据,而非调用该对象提供的行为来完成逻辑时,就属于不良实践。比如最初的Rectangle示例中,外部代码直接用
getA()和getB()计算面积,就是对Rectangle的特性依恋,完全忽略了对象本身应该具备的行为能力。 - 封装:过度封装或封装错误内容时会出问题。比如为每个私有字段都添加Getter/Setter,把对象变成纯粹的数据容器;或者把本该对外提供的核心行为隐藏起来,强迫外部做冗余操作,反而降低了代码的易用性。
- Active Record:仅在业务逻辑复杂的场景下才是不良实践。小项目或简单CRUD应用中,Active Record能快速实现需求,效率很高;但当业务逻辑增多,模型类既要处理数据存储又要承载业务规则,会导致类臃肿不堪,难以维护和测试。
- 关注点分离:过度分离导致代码碎片化时会变味。比如把一个简单的计算逻辑拆成多个零散的类,调用链路变长,开发者需要在多个文件间跳转才能理解逻辑,反而降低了可读性和开发效率。
2. 操作(如getArea、save)应置于对象内部还是外部?
- 与对象核心属性强相关的操作(如getArea):必须放在对象内部。这类操作是对象固有行为的一部分,只有对象自己最清楚如何基于内部属性完成计算,放在内部能保证封装性,后续修改逻辑时也不会影响外部代码。
- 数据持久化操作(如save):分场景决定:
- 简单场景(小项目、单CRUD):放在对象内部(Active Record模式)没问题,快速高效;
- 复杂业务场景:应该分离到专门的仓储(Repository)类中,让模型类专注于业务逻辑,仓储类负责数据读写,符合关注点分离原则,也方便后续替换数据库、编写单元测试。
内容的提问来源于stack exchange,提问作者John Smith
相关产品推荐
相关产品推荐

