软件设计原则

单一职责原则(Single Responsibility Principle, SRP)

每个类或模块应该只有一个职责,即一个类只做一件事

反例

class User {
private $name;
private $email;
 
public function __construct($name, $email) {
$this->name = $name;
$this->email = $email;
}
 
public function getName() {
return $this->name;
}
 
public function getEmail() {
return $this->email;
}
 
// 发送邮件的方法
public function sendEmail($message) {
// 邮件发送逻辑
mail($this->email, "Subject", $message);
}
 
// 保存用户信息的方法
public function save() {
// 数据库保存逻辑
$db = new DatabaseConnection();
$db->query("INSERT INTO users (name, email) VALUES ('$this->name', '$this->email')");
}
}

在上面的例子中,User 类有多个职责:存储用户信息、发送电子邮件和保存用户信息到数据库。这违反了单一职责原则。

正例

class User {
private $name;
private $email;
 
public function __construct($name, $email) {
$this->name = $name;
$this->email = $email;
}
 
public function getName() {
return $this->name;
}
 
public function getEmail() {
return $this->email;
}
}
 
class EmailSender {
public function sendEmail(User $user, $message) {
// 邮件发送逻辑
mail($user->getEmail(), "Subject", $message);
}
}
 
class UserRepository {
public function save(User $user) {
// 数据库保存逻辑
$db = new DatabaseConnection();
$db->query("INSERT INTO users (name, email) VALUES ('{$user->getName()}', '{$user->getEmail()}')");
}
}

在这个例子中,职责被分离成三个独立的类:

  1. User 类负责存储用户信息。
  2. EmailSender 类负责发送电子邮件。
  3. UserRepository 类负责保存用户信息到数据库。

开闭原则(Open/Closed Principle, OCP)

软件实体(类、模块、函数等)应该对扩展开放,对修改封闭

反例

class Rectangle {
private $width;
private $height;
 
public function __construct($width, $height) {
$this->width = $width;
$this->height = $height;
}
 
public function getWidth() {
return $this->width;
}
 
public function getHeight() {
return $this->height;
}
}
 
class Circle {
private $radius;
 
public function __construct($radius) {
$this->radius = $radius;
}
 
public function getRadius() {
return $this->radius;
}
}
 
class AreaCalculator {
public function calculate($shapes) {
$area = 0;
foreach ($shapes as $shape) {
if ($shape instanceof Rectangle) {
$area += $shape->getWidth() * $shape->getHeight();
} elseif ($shape instanceof Circle) {
$area += pi() * pow($shape->getRadius(), 2);
}
}
return $area;
}
}
 
$shapes = [new Rectangle(2, 3), new Circle(5)];
$calculator = new AreaCalculator();
echo $calculator->calculate($shapes);

在上面的例子中,如果需要添加新的形状(如三角形),必须修改 AreaCalculator 类的 calculate 方法,这违反了开闭原则。

正例

interface Shape {
public function getArea();
}
 
class Rectangle implements Shape {
private $width;
private $height;
 
public function __construct($width, $height) {
$this->width = $width;
$this->height = $height;
}
 
public function getArea() {
return $this->width * $this->height;
}
}
 
class Circle implements Shape {
private $radius;
 
public function __construct($radius) {
$this->radius = $radius;
}
 
public function getArea() {
return pi() * pow($this->radius(), 2);
}
}
 
class Triangle implements Shape {
private $base;
private $height;
 
public function __construct($base, $height) {
$this->base = $base;
$this->height = $height;
}
 
public function getArea() {
return 0.5 * $this->base * $this->height;
}
}
 
class AreaCalculator {
public function calculate($shapes) {
$area = 0;
foreach ($shapes as $shape) {
$area += $shape->getArea();
}
return $area;
}
}
 
$shapes = [new Rectangle(2, 3), new Circle(5), new Triangle(3, 4)];
$calculator = new AreaCalculator();
echo $calculator->calculate($shapes);

在这个例子中,每个形状类都实现了 Shape 接口,并提供了 getArea 方法。AreaCalculator 类使用这些接口来计算总面积。这样,如果需要添加新的形状,只需实现 Shape 接口,并提供 getArea 方法,而不需要修改 AreaCalculator 类,从而遵循了开闭原则。

里氏替换原则(Liskov Substitution Principle, LSP)

子类对象应该可以替换父类对象,并且程序行为保持不变

反例

class Bird {
public function fly() {
echo "I'm flying!";
}
}
 
class Duck extends Bird {
public function quack() {
echo "Quack!";
}
}
 
class Ostrich extends Bird {
public function fly() {
throw new Exception("I can't fly!");
}
}
 
function makeBirdFly(Bird $bird) {
$bird->fly();
}
 
$duck = new Duck();
$ostrich = new Ostrich();
 
makeBirdFly($duck); // 输出: I'm flying!
makeBirdFly($ostrich); // 抛出异常: I can't fly!

在上面的例子中,Ostrich 类违反了里氏替换原则,因为鸵鸟不能飞。当试图用 Ostrich 对象替换 Bird 对象时,程序会抛出异常,这违背了里氏替换原则。

正例

abstract class Bird {
abstract public function move();
}
 
class FlyingBird extends Bird {
public function move() {
echo "I'm flying!";
}
}
 
class NonFlyingBird extends Bird {
public function move() {
echo "I'm running!";
}
}
 
class Duck extends FlyingBird {
public function quack() {
echo "Quack!";
}
}
 
class Ostrich extends NonFlyingBird {
// 特有的行为
}
 
function makeBirdMove(Bird $bird) {
$bird->move();
}
 
$duck = new Duck();
$ostrich = new Ostrich();
 
makeBirdMove($duck); // 输出: I'm flying!
makeBirdMove($ostrich); // 输出: I'm running!

在这个例子中,通过将 Bird 类拆分为 FlyingBird 和 NonFlyingBird 类来区分会飞和不会飞的鸟。这样,当用 Duck 或 Ostrich 对象替换 Bird 对象时,程序行为保持一致,没有异常发生,从而遵循了里氏替换原则。

接口隔离原则(Interface Segregation Principle, ISP)

应该尽量使用多个专门的接口,而不是一个总接口

反例

interface Worker {
public function work();
public function eat();
}
 
class HumanWorker implements Worker {
public function work() {
echo "Human working";
}
 
public function eat() {
echo "Human eating";
}
}
 
class RobotWorker implements Worker {
public function work() {
echo "Robot working";
}
 
// Robots don't eat, but we have to implement this method
public function eat() {
throw new Exception("Robots don't eat!");
}
}
 
function manageWorker(Worker $worker) {
$worker->work();
$worker->eat();
}
 
$humanWorker = new HumanWorker();
$robotWorker = new RobotWorker();
 
manageWorker($humanWorker); // 输出: Human working, Human eating
manageWorker($robotWorker); // 抛出异常: Robots don't eat!

在上面的例子中,RobotWorker 类必须实现它不需要的 eat 方法,这违反了接口隔离原则。

正例

interface Workable {
public function work();
}
 
interface Eatable {
public function eat();
}
 
class HumanWorker implements Workable, Eatable {
public function work() {
echo "Human working";
}
 
public function eat() {
echo "Human eating";
}
}
 
class RobotWorker implements Workable {
public function work() {
echo "Robot working";
}
}
 
function manageWork(Workable $worker) {
$worker->work();
}
 
function manageEating(Eatable $eater) {
$eater->eat();
}
 
$humanWorker = new HumanWorker();
$robotWorker = new RobotWorker();
 
manageWork($humanWorker); // 输出: Human working
manageWork($robotWorker); // 输出: Robot working
 
manageEating($humanWorker); // 输出: Human eating
// manageEating($robotWorker); // 这行代码会导致编译错误,因为 RobotWorker 不实现 Eatable 接口

在这个例子中,将 Worker 接口拆分为 Workable 和 Eatable 两个独立的接口。这样,每个类只需要实现它们真正需要的方法,从而遵循了接口隔离原则。这使得 RobotWorker 类不再需要实现它不需要的 eat 方法,同时保持了代码的灵活性和可维护性。

依赖倒置原则(Dependency Inversion Principle, DIP)

高层模块不应该依赖于低层模块,二者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象

反例

class MySQLConnection {
public function connect() {
// MySQL 连接逻辑
echo "Connecting to MySQL database";
}
}
 
class PasswordReminder {
private $dbConnection;
 
public function __construct() {
$this->dbConnection = new MySQLConnection();
}
 
public function remind() {
$this->dbConnection->connect();
// 提醒逻辑
echo "Reminding password";
}
}
 
$reminder = new PasswordReminder();
$reminder->remind();

在上面的例子中,PasswordReminder 直接依赖于 MySQLConnection 具体实现,违反了依赖倒置原则。如果想改用其他数据库连接,就需要修改 PasswordReminder 类,这增加了耦合度。

正例

interface DBConnectionInterface {
public function connect();
}
 
class MySQLConnection implements DBConnectionInterface {
public function connect() {
// MySQL 连接逻辑
echo "Connecting to MySQL database";
}
}
 
class PostgreSQLConnection implements DBConnectionInterface {
public function connect() {
// PostgreSQL 连接逻辑
echo "Connecting to PostgreSQL database";
}
}
 
class PasswordReminder {
private $dbConnection;
 
public function __construct(DBConnectionInterface $dbConnection) {
$this->dbConnection = $dbConnection;
}
 
public function remind() {
$this->dbConnection->connect();
// 提醒逻辑
echo "Reminding password";
}
}
 
// 使用 MySQL 连接
$mysqlConnection = new MySQLConnection();
$reminder = new PasswordReminder($mysqlConnection);
$reminder->remind();
 
// 使用 PostgreSQL 连接
$pgsqlConnection = new PostgreSQLConnection();
$reminder = new PasswordReminder($pgsqlConnection);
$reminder->remind();

在这个例子中,PasswordReminder 依赖于 DBConnectionInterface 接口,而不是具体的数据库连接实现。这样,当需要更换数据库连接时,只需提供不同的实现,而无需修改 PasswordReminder 类,从而遵循了依赖倒置原则。这减少了类之间的耦合,提高了系统的灵活性和可维护性。

高内聚低耦合原则(High Cohesion and Low Coupling)

类和模块内部应该保持高内聚,即内部元素紧密相关,职责明确。类和模块之间应该保持低耦合,即相互依赖少,交互简单

反例

class UserManager {
private $db;
 
public function __construct() {
$this->db = new MySQLDatabase(); // 直接依赖具体实现
}
 
public function createUser($name, $email) {
// 用户创建逻辑
$this->db->query("INSERT INTO users (name, email) VALUES ('$name', '$email')");
}
 
public function notifyUser($email) {
// 用户通知逻辑
mail($email, "Welcome", "Thanks for registering!");
}
 
public function generateReport() {
// 报告生成逻辑
echo "Generating report...";
}
}
 
class MySQLDatabase {
public function query($sql) {
// 数据库查询逻辑
echo "Executing query: $sql";
}
}
 
$userManager = new UserManager();
$userManager->createUser("Alice", "[email protected]");
$userManager->notifyUser("[email protected]");
$userManager->generateReport();

在这个例子中,UserManager 类具有多种不同的职责(用户管理、通知、报告生成),导致低内聚。此外,它直接依赖于具体的 MySQLDatabase 实现,导致高耦合。

正例

interface DatabaseInterface {
public function query($sql);
}
 
class MySQLDatabase implements DatabaseInterface {
public function query($sql) {
// 数据库查询逻辑
echo "Executing query: $sql";
}
}
 
class UserRepository {
private $db;
 
public function __construct(DatabaseInterface $db) {
$this->db = $db;
}
 
public function createUser($name, $email) {
$this->db->query("INSERT INTO users (name, email) VALUES ('$name', '$email')");
}
}
 
class EmailService {
public function sendEmail($to, $subject, $message) {
// 邮件发送逻辑
mail($to, $subject, $message);
}
}
 
class ReportGenerator {
public function generateReport() {
// 报告生成逻辑
echo "Generating report...";
}
}
 
class UserManager {
private $userRepository;
private $emailService;
private $reportGenerator;
 
public function __construct(UserRepository $userRepository, EmailService $emailService, ReportGenerator $reportGenerator) {
$this->userRepository = $userRepository;
$this->emailService = $emailService;
$this->reportGenerator = $reportGenerator;
}
 
public function createUser($name, $email) {
$this->userRepository.createUser($name, $email);
$this->emailService.sendEmail($email, "Welcome", "Thanks for registering!");
}
 
public function generateReport() {
$this->reportGenerator.generateReport();
}
}
 
$db = new MySQLDatabase();
$userRepository = new UserRepository($db);
$emailService = new EmailService();
$reportGenerator = new ReportGenerator();
$userManager = new UserManager($userRepository, $emailService, $reportGenerator);
 
$userManager->createUser("Alice", "[email protected]");
$userManager->generateReport();

在这个正例中:

  1. 高内聚
    • UserRepository 负责用户相关的数据库操作。
    • EmailService 负责邮件发送。
    • ReportGenerator 负责报告生成。
    • 每个类都只有一个明确的职责,职责明确,内聚度高。
  2. 低耦合
    • UserManager 通过接口 DatabaseInterface 依赖于数据库,而不是具体的 MySQLDatabase 实现。
    • UserManager 类依赖于高内聚的服务类(UserRepository、EmailService 和 ReportGenerator),而不是自己处理所有逻辑。
    • 这样减少了类之间的依赖,降低了耦合度。

迪米特原则(Law of Demeter, LoD)

指出一个对象应该对其他对象有最少的了解。具体来说,一个对象不应该直接调用另一个对象的方法,而是通过其自身的方法调用来完成

反例

class Engine {
public function start() {
echo "Engine started.";
}
}
 
class Car {
private $engine;
 
public function __construct() {
$this->engine = new Engine();
}
 
public function getEngine() {
return $this->engine;
}
}
 
class Driver {
public function startCar(Car $car) {
$car->getEngine()->start(); // 直接访问 Car 的内部对象 Engine
}
}
 
$car = new Car();
$driver = new Driver();
$driver->startCar($car); // 输出: Engine started.

在这个例子中,Driver 类直接调用 Car 类的 getEngine() 方法,并调用 Engine 类的 start() 方法。这违反了迪米特法则,因为 Driver 类直接与 Engine 类耦合,从而了解了 Car 类的内部实现细节。

正例

class Engine {
public function start() {
echo "Engine started.";
}
}
 
class Car {
private $engine;
 
public function __construct() {
$this->engine = new Engine();
}
 
public function startEngine() {
$this->engine->start();
}
}
 
class Driver {
public function startCar(Car $car) {
$car->startEngine(); // 通过 Car 的方法来启动引擎
}
}
 
$car = new Car();
$driver = new Driver();
$driver->startCar($car); // 输出: Engine started.

在这个例子中,Driver 类不再直接访问 Engine 对象,而是通过 Car 类的 startEngine() 方法来启动引擎。这样,Driver 类只与 Car 类交互,而不需要了解 Car 类的内部实现细节,从而遵循了迪米特法则。