总原则——开闭原则 (Open Closed Principle, OCP)
一个软件实体,如类、模块和函数应该对扩展开放,对修改关闭。
在程序需要进行拓展的时候,不能去修改原有的代码,而是要扩展原有代码,实现一个热插拔的效果。所以一句话概括就是:为了使程序的扩展性好,易于维护和升级。
// Bad example
class Rectangle {
public int width;
public int height;
}
class AreaCalculator {
public int calculateArea(Rectangle[] rectangles) {
int area = 0;
for (Rectangle rectangle : rectangles) {
area += rectangle.width * rectangle.height;
}
return area;
}
}
// Good example
abstract class Shape {
public abstract int calculateArea();
}
class Rectangle extends Shape {
public int width;
public int height;
@Override
public int calculateArea() {
return width * height;
}
}
class Circular extends Shape {
public int radius;
@Override
public int calculateArea() {
return (int) (Math.PI * radius * radius);
}
}
class AreaCalculator {
public int calculateArea(Shape[] shapes) {
int area = 0;
for (Shape shape : shapes) {
area += shape.calculateArea();
}
return area;
}
}
在错误的例子中,如果我们要计算其他形状的面积,就需要修改AreaCalculator类。而在正确的例子中,我们通过引入抽象类Shape来实现开放封闭原则。现在,我们可以添加新的形状类,如Triangle,而无需修改AreaCalculator类。
1、单一职责原则 (Single Responsibility Principle, SRP)
一个类应该只有一个发生变化的原因。
不要存在多于一个导致类变更的原因,也就是说每个类应该实现单一的职责,否则就应该把类拆分。
// Bad example
class Employee {
public void calculateSalary() {
// Calculate salary
}
public void generateReport() {
// Generate report
}
}
// Good example
class Employee {
public void calculateSalary() {
// Calculate salary
}
}
class ReportGenerator {
public void generateReport(Employee employee) {
// Generate report
}
}
在错误的例子中,Employee类负责计算薪水和生成报告,这违反了单一职责原则。正确的例子将这两个功能分离到不同的类中,使得每个类都只负责一项任务
2、里氏替换原则 (Liskov Substitution Principle, LSP)
所有引用基类的地方必须能透明地使用其子类的对象。
任何基类可以出现的地方,子类一定可以出现。里氏替换原则是继承复用的基石,只有当衍生类可以替换基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。
里氏代换原则是对“开-闭”原则的补充。实现“开闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏替换原则是对实现抽象化的具体步骤的规范。里氏替换原则中,子类对父类的方法尽量不要重写和重载因为父类代表了定义好的结构,通过这个规范的接口与外界交互,子类不应该随便破坏它。
class Bird {
public void fly() {
// Fly
}
}
class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("Penguins can't fly!");
}
}
// Good example
class Bird {
}
class FlightlessBird extends Bird {
public void fly() {
// Fly
}
}
class FlightlessBirds extends FlightlessBird {
@Override
public void fly() {
// Fly
}
}
class Penguin extends Bird {
}
在错误的例子中,Penguin类违反了里氏替换原则,因为它不能像其他鸟类那样飞行。正确的例子引入了一个新的中间类FlightlessBird,使得Penguin类不再违反LSP
3、依赖倒置原则 (Dependency Inversion Principle, DIP)
1、上层模块不应该依赖底层模块,它们都应该依赖于抽象。
2、抽象不应该依赖于细节,细节应该依赖于抽象。
面向接口编程,依赖于抽象而不依赖于具体。写代码时用到具体类时,不与具体类交互,而与具体类的上层接口交互。
// Bad example
class MySQLDatabase {
public void saveData(String data) {
// Save data
// Save data to MySQL database
}
}
class DataManager {
private MySQLDatabase database;
public DataManager(MySQLDatabase database) {
this.database = database;
}
public void saveData(String data) {
database.saveData(data);
}
}
// Good example
interface Database {
void saveData(String data);
}
class MySQLDatabase implements Database {
@Override
public void saveData(String data) {
// Save data to MySQL database
}
}
class MongoDBDatabase implements Database {
@Override
public void saveData(String data) {
// Save data to MongoDB database
}
}
class DataManager {
private Database database;
public DataManager(Database database) {
this.database = database;
}
public void saveData(String data) {
database.saveData(data);
}
}
在错误的例子中,DataManager类依赖于具体的MySQLDatabase实现,这使得代码难以修改和扩展。正确的例子引入了一个Database接口,使得DataManager依赖于抽象而不是具体实现。这样,我们可以轻松地切换到其他数据库,例如MongoDBDatabase。
4、接口隔离原则 (Interface Segregation Principle, ISP)
1、客户端不应该依赖它不需要的接口。
2、类间的依赖关系应该建立在最小的接口上。
每个接口中不存在子类用不到却必须实现的方法,如果不然,就要将接口拆分。使用多个隔离的接口,比使用单个接口(多个接口方法集合到一个的接口)要好。
// Bad example
interface Animal {
void eat();
void swim();
void fly();
}
class Shark implements Animal {
@Override
public void eat() {
// Eat
}
@Override
public void swim() {
// Swim
}
@Override
public void fly() {
throw new UnsupportedOperationException("Sharks can't fly!");
}
}
// Good example
interface Swimmer {
void swim();
}
interface Eater {
void eat();
}
interface Flyer {
void fly();
}
class Shark implements Swimmer, Eater {
@Override
public void eat() {
// Eat
}
@Override
public void swim() {
// Swim
}
}
在错误的例子中,Animal接口包含了不相关的方法,迫使Shark实现不需要的fly方法。正确的例子将接口划分为更小的接口,遵循了接口隔离原则。
5、迪米特法则(最少知道原则)(Law of Demeter)
只与你的直接朋友交谈,不跟“陌生人”说话。
一个类对自己依赖的类知道的越少越好。无论被依赖的类多么复杂,都应该将逻辑封装在方法的内部,通过public方法提供给外部。这样当被依赖的类变化时,才能最小的影响该类。
最少知道原则的另一个表达方式是:只与直接的朋友通信。类之间只要有耦合关系,就叫朋友关系。耦合分为依赖、关联、聚合、组合等。我们称出现为成员变量、方法参数、方法返回值中的类为直接朋友。局部变量、临时变量则不是直接的朋友。我们要求陌生的类不要作为局部变量出现在类中。
// Bad example
class Employee {
private String id;
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
class CollegeEmployee {
private String id;
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
class CollegeManager {
public List<CollegeEmployee> getAllEmployee() {
List<CollegeEmployee> list = new ArrayList<CollegeEmployee>();
for (int i = 0; i < 10; i++) {
CollegeEmployee emp = new CollegeEmployee();
emp.setId("学院员工id= " + i);
list.add(emp);
}
return list;
}
}
class SchoolManager {
public List<Employee> getAllEmployee() {
List<Employee> list = new ArrayList<Employee>();
for (int i = 0; i < 5; i++) {
Employee emp = new Employee();
emp.setId("学校总部员工id= " + i);
list.add(emp);
}
return list;
}
void printAllEmployee(CollegeManager sub) {
List<CollegeEmployee> list1 = sub.getAllEmployee();
System.out.println("------------学院员工------------");
for (CollegeEmployee e : list1) {
System.out.println(e.getId());
}
List<Employee> list2 = this.getAllEmployee();
System.out.println("------------学校总部员工------------");
for (Employee e : list2) {
System.out.println(e.getId());
}
}
}
// Good example
class Employee {
private String id;
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
class CollegeEmployee {
private String id;
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
class CollegeManager {
public List<CollegeEmployee> getAllEmployee() {
List<CollegeEmployee> list = new ArrayList<CollegeEmployee>();
for (int i = 0; i < 10; i++) {
CollegeEmployee emp = new CollegeEmployee();
emp.setId("学院员工id= " + i);
list.add(emp);
}
return list;
}
public void printEmployee() {
List<CollegeEmployee> list1 = getAllEmployee();
System.out.println("------------学院员工------------");
for (CollegeEmployee e : list1) {
System.out.println(e.getId());
}
}
}
class SchoolManager {
public List<Employee> getAllEmployee() {
List<Employee> list = new ArrayList<Employee>();
for (int i = 0; i < 5; i++) {
Employee emp = new Employee();
emp.setId("学校总部员工id= " + i);
list.add(emp);
}
return list;
}
void printAllEmployee(CollegeManager sub) {
sub.printEmployee();
List<Employee> list2 = this.getAllEmployee();
System.out.println("------------学校总部员工------------");
for (Employee e : list2) {
System.out.println(e.getId());
}
}
}
CollegeEmployee 不是 直接朋友 而是一个陌生类,这样违背了 迪米特法则,成员变量、方法参数、方法返回值中的类为直接朋友
6、合成/聚合复用原则 (Composite/Aggregate Reuse Principle, CARP)
尽量使用对象组合/聚合,而不是继承关系达到软件复用的目的。
合成或聚合可以将已有对象纳入到新对象中,使之成为新对象的一部分,因此新对象可以调用已有对象的功能。
// Bad example
class Engine {
public void start() {
// Start engine
}
}
class Car extends Engine {
public void drive() {
start();
// Drive
}
}
// Good example
class Engine {
public void start() {
// Start engine
}
}
class Car {
private Engine engine;
public Car(Engine engine) {
this.engine = engine;
}
public void drive() {
engine.start();
// Drive
}
}
在错误的例子中,Car类通过继承Engine来实现复用。然而,这使得代码难以修改和扩展。正确的例子通过组合的方式,将Engine作为一个成员变量,从而遵循了合成复用原则。 通过遵循这些设计模式哲学中的六大原则,我们可以编写出更优雅、灵活和可维护的代码。在实际项目中,我们可能会根据具体情况灵活运用这些原则,以实现最佳的设计。希望这篇文章能帮助你更好地理解设计模式,并在实际编程过程中运用得心应手。
记忆口诀:SOLID CD(稳固的CD)。