GPT省流
单一职责原则(Single Responsibility Principle)
👉 该原则规定一个类应该只有一个发生变化的原因,也就是说一个类应该只负责一项职责。这样做的目的是高内聚🔑,低耦合⛓️。
💡 当一个类承担太多职责时,其就很容易被修改,从而降低了代码的可维护性和可扩展性。
✅ 使用场景:假设我们有一个处理文件的程序,根据该原则,我们应该将读取文件、解析文件内容以及发送错误报告等功能分离到不同的类中。这样可以提高代码的可维护性和可扩展性。
🔍 总结:单一职责原则强调一个类应该只负责一个职责,这样可以提高内聚性,降低耦合度,从而提高代码的可维护性和可扩展性。
开闭原则(Open-Closed Principle)
👉 该原则规定软件实体应该对扩展开放,对修改关闭。也就是说,当需要扩展系统功能时,应该新增代码,而不是修改原有代码。这样可以提高代码的可维护性和可复用性。
💡 通过这种设计,我们可以在不修改原有代码的情况下,添加新的功能,从而避免了潜在的代码破坏风险。
✅ 使用场景:假设我们有一个计算几何图形面积的程序,根据该原则,我们应该设计一种可扩展的方式,以便在不修改原有代码的情况下,添加新的几何图形类型。
🔍 总结:开闭原则强调软件实体应该对扩展开放,对修改关闭。这样可以提高代码的可维护性和可复用性,避免了潜在的代码破坏风险。
里氏代换原则(Liskov Substitution Principle)
👉 该原则规定,如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,替换o1能使得程序的行为没有变化,那么类型T2就被称为是类型T1的子类型。
💡 这个原则的核心思想是:子类应该可以替换基类的使用,而不会破坏程序的正确性。
✅ 使用场景:假设我们有一个表示几何图形的基类Shape,以及两个子类Rectangle和Square。根据这个原则,我们应该能够使用Square对象替换Rectangle对象,而不会破坏程序的正确性。
🔍 总结:里氏代换原则强调子类应该可以替换基类的使用,而不会破坏程序的正确性。这样可以提高代码的可扩展性和可维护性。
接口隔离原则(Interface Segregation Principle)
👉 该原则规定,客户端不应该被强制依赖它不需要使用的接口。换句话说,一个接口应该只定义一组相关的方法,而不应该将不相关的方法捆绑在一起。
💡 这样做可以避免客户端被强制依赖不需要的方法,提高了代码的灵活性和可维护性。
✅ 使用场景:假设我们有一个表示工作人员的接口Employee。不同类型的工作人员可能需要实现不同的方法。根据该原则,我们应该将这些方法划分为多个专门的接口,而不是将它们捆绑在一个大的接口中。
🔍 总结:接口隔离原则规定一个接口应该只定义一组相关的方法,客户端不应该被强制依赖它不需要使用的接口。这样可以提高代码的灵活性和可维护性。
合成复用原则(Composite Reuse Principle)
👉 该原则规定,应该优先使用对象组合而不是类继承来实现代码复用。通过组合可以获得比继承更好的可扩展性。
💡 继承可能会引入一些不需要的方法和属性,增加了代码的复杂性和不必要的耦合。
✅ 使用场景:假设我们正在开发一个汽车模拟系统,我们需要表示不同类型的汽车,每种汽车都有不同的引擎和变速箱。根据该原则,我们应该使用组合而不是继承来实现这个系统。
🔍 总结:合成复用原则强调优先使用对象组合而不是类继承来实现代码复用。这样可以获得更好的可扩展性,避免继承可能引入的不必要的复杂性和耦合。
迪米特法则(Law of Demeter)
👉 迪米特法则,也被称为最少知识原则,规定一个对象应该对其他对象保持最少的了解。换句话说,一个类应该只与它的直接朋友交谈,而不与陌生人交谈。
💡 这样做可以减少类之间的耦合,提高系统的模块化程度和可维护性。
✅ 使用场景:假设我们有一个订单处理系统,包含客户、订单和支付处理器。按照该原则,订单处理应该只与直接相关的对象交互,而不需要知道支付处理的具体细节。
🔍 总结:迪米特法则强调一个对象应该对其他对象保持最少的了解,只与直接相关的对象交互。这样可以减少类之间的耦合,提高系统的模块化程度和可维护性。
总的来说,这些设计原则都是为了提高代码的可维护性、可扩展性和可复用性,从而提高软件系统的整体质量。通过遵循这些原则,我们可以编写出更加健壮、灵活和易于维护的代码。同时,在实际开发中,我们还需要结合具体的场景和需求,权衡利弊,选择最佳的设计方式。
单一职责原则 (Single Responsibility Principle)
单一职责原则规定,一个类应该只有一个发生变化的原因,也就是说一个类应该只负责一项职责。这样做的目的是高内聚,低耦合。当一个类承担太多职责时,其就很容易被修改。
使用场景
假设我们有一个处理文件的程序,它包含了读取文件、解析文件内容以及发送错误报告的功能。根据单一职责原则,我们应该将这些功能分离到不同的类中。
// FileReader 负责读取文件
type FileReader struct {
// ...
}
func (f *FileReader) Read(path string) ([]byte, error) {
// 读取文件并返回内容
}
// FileParser 负责解析文件内容
type FileParser struct {
// ...
}
func (p *FileParser) Parse(data []byte) (result interface{}, err error) {
// 解析文件内容
}
// ErrorReporter 负责发送错误报告
type ErrorReporter struct {
// ...
}
func (r *ErrorReporter) Report(err error) {
// 发送错误报告
}
通过这样的设计,当需要修改某一个职责时,只需要修改相应的类,而不会影响到其他类。这样可以提高代码的可维护性和可扩展性。
开闭原则 (Open-Closed Principle)
开闭原则规定,软件实体应该对扩展开放,对修改关闭。也就是说,当需要扩展系统功能时,应该新增代码,而不是修改原有代码。这样可以提高代码的可维护性和可复用性。
使用场景
假设我们有一个计算几何图形面积的程序,它目前只支持计算矩形和圆形的面积。根据开闭原则,我们应该设计一种可扩展的方式,以便在不修改原有代码的情况下,添加新的几何图形类型。
// Shape 接口定义了计算面积的方法
type Shape interface {
Area() float64
}
// Rectangle 实现了 Shape 接口
type Rectangle struct {
width, height float64
}
func (r Rectangle) Area() float64 {
return r.width * r.height
}
// Circle 实现了 Shape 接口
type Circle struct {
radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.radius * c.radius
}
// AreaCalculator 负责计算不同图形的面积
type AreaCalculator struct {
shapes []Shape
}
func (c *AreaCalculator) Sum() float64 {
var sum float64
for _, s := range c.shapes {
sum += s.Area()
}
return sum
}
在这个设计中,我们定义了一个 Shape
接口,并让矩形和圆形分别实现了这个接口。当需要添加新的几何图形类型时,只需要实现 Shape
接口,而不需要修改原有代码。这样就满足了开闭原则。
里氏代换原则 (Liskov Substitution Principle)
里氏代换原则规定,如果对每一个类型为 T1 的对象 o1,都有类型为 T2 的对象 o2,替换 o1 能使得程序的行为没有变化,那么类型 T2 就被称为是类型 T1 的子类型。这个原则的核心思想是:子类应该可以替换基类的使用,而不会破坏程序的正确性。
使用场景
假设我们有一个表示几何图形的基类 Shape
,以及两个子类 Rectangle
和 Square
。根据里氏代换原则,我们应该能够使用 Square
对象替换 Rectangle
对象,而不会破坏程序的正确性。
// Shape 定义了几何图形的基本属性和方法
type Shape interface {
Area() float64
Resize(ratio float64)
}
// Rectangle 实现了 Shape 接口
type Rectangle struct {
width, height float64
}
func (r *Rectangle) Area() float64 {
return r.width * r.height
}
func (r *Rectangle) Resize(ratio float64) {
r.width *= ratio
r.height *= ratio
}
// Square 实现了 Shape 接口
type Square struct {
side float64
}
func (s *Square) Area() float64 {
return s.side * s.side
}
func (s *Square) Resize(ratio float64) {
s.side *= ratio
}
// ResizeShapes 函数接受一个 Shape 切片作为参数,并调整它们的大小
func ResizeShapes(shapes []Shape, ratio float64) {
for _, s := range shapes {
s.Resize(ratio)
}
}
在上面的例子中,我们定义了一个 ResizeShapes
函数,它接受一个 Shape
切片作为参数,并调用每个形状的 Resize
方法。由于 Rectangle
和 Square
都实现了 Shape
接口,我们可以将它们作为参数传递给 ResizeShapes
函数,而不会破坏程序的正确性。这就满足了里氏代换原则。
接口隔离原则 (Interface Segregation Principle)
接口隔离原则规定,客户端不应该被强制依赖它不需要使用的接口。换句话说,一个接口应该只定义一组相关的方法,而不应该将不相关的方法捆绑在一起。
使用场景
假设我们有一个表示工作人员的接口 Employee
。不同类型的工作人员可能需要实现不同的方法。根据接口隔离原则,我们应该将这些方法划分为多个专门的接口,而不是将它们捆绑在一个大的接口中。
// Employee 接口定义了工作人员的基本方法
type Employee interface {
GetName() string
GetAge() int
}
// FullTimeEmployee 接口定义了全职员工的附加方法
type FullTimeEmployee interface {
Employee
GetSalary() float64
GetBenefits() []string
}
// Contractor 接口定义了合同工的附加方法
type Contractor interface {
Employee
GetHourlyRate() float64
GetContractDuration() int
}
// FullTimeWorker 实现了 FullTimeEmployee 接口
type FullTimeWorker struct {
name string
age int
salary float64
benefits []string
}
func (f *FullTimeWorker) GetName() string {
return f.name
}
func (f *FullTimeWorker) GetAge() int {
return f.age
}
func (f *FullTimeWorker) GetSalary() float64 {
return f.salary
}
func (f *FullTimeWorker) GetBenefits() []string {
return f.benefits
}
// Freelancer 实现了 Contractor 接口
type Freelancer struct {
name string
age int
hourlyRate float64
contractDuration int
}
// 实现 Contractor 接口的其他方法...
在这个例子中,我们定义了一个基本的 Employee
接口,以及两个特定的接口 FullTimeEmployee
和 Contractor
。FullTimeEmployee
接口继承了 Employee
接口,并添加了一些全职员工特有的方法,而 Contractor
接口则添加了一些合同工特有的方法。通过这种设计,不同类型的工作人员只需要实现与自己相关的接口,而不会被强制实现不需要的方法,从而满足了接口隔离原则。
合成复用原则 (Composite Reuse Principle)
合成复用原则规定,应该优先使用对象组合而不是类继承来实现代码复用。通过组合可以获得比继承更好的可扩展性。
使用场景
假设我们正在开发一个汽车模拟系统,我们需要表示不同类型的汽车,每种汽车都有不同的引擎和变速箱。根据合成复用原则,我们应该使用组合而不是继承来实现这个系统。
// Engine 接口定义了引擎的基本方法
type Engine interface {
Start()
Stop()
}
// Transmission 接口定义了变速箱的基本方法
type Transmission interface {
ChangeGear(gear int)
}
// Car 结构体使用组合来包含引擎和变速箱
type Car struct {
engine Engine
transmission Transmission
}
func (c *Car) Start() {
c.engine.Start()
}
func (c *Car) Stop() {
c.engine.Stop()
}
func (c *Car) ChangeGear(gear int) {
c.transmission.ChangeGear(gear)
}
// GasolineEngine 实现了 Engine 接口
type GasolineEngine struct{}
func (e *GasolineEngine) Start() {
fmt.Println("Gasoline engine started")
}
func (e *GasolineEngine) Stop() {
fmt.Println("Gasoline engine stopped")
}
// ElectricEngine 实现了 Engine 接口
type ElectricEngine struct{}
func (e *ElectricEngine) Start() {
fmt.Println("Electric engine started")
}
func (e *ElectricEngine) Stop() {
fmt.Println("Electric engine stopped")
}
// AutomaticTransmission 实现了 Transmission 接口
type AutomaticTransmission struct{}
func (t *AutomaticTransmission) ChangeGear(gear int) {
fmt.Printf("Automatic transmission changed to gear %d\n", gear)
}
// 使用示例
func main() {
gasolineCar := &Car{
engine: &GasolineEngine{},
transmission: &AutomaticTransmission{},
}
electricCar := &Car{
engine: &ElectricEngine{},
transmission: &AutomaticTransmission{},
}
gasolineCar.Start()
gasolineCar.ChangeGear(3)
gasolineCar.Stop()
electricCar.Start()
electricCar.ChangeGear(2)
electricCar.Stop()
}
在这个例子中,我们使用组合而不是继承来构建汽车系统。Car
结构体包含了 Engine
和 Transmission
接口,而不是继承它们。这样,我们可以轻松地创建不同类型的汽车,只需要组合不同的引擎和变速箱实现即可。这种设计更加灵活,易于扩展,符合合成复用原则。
迪米特法则 (Law of Demeter)
迪米特法则,也被称为最少知识原则,规定一个对象应该对其他对象保持最少的了解。换句话说,一个类应该只与它的直接朋友交谈,而不与陌生人交谈。
使用场景
假设我们有一个订单处理系统,包含客户、订单和支付处理器。按照迪米特法则,订单处理应该只与直接相关的对象交互。
// Customer 表示客户
type Customer struct {
Name string
}
// Order 表示订单
type Order struct {
Items []string
TotalCost float64
}
// PaymentProcessor 处理支付
type PaymentProcessor struct{}
func (pp *PaymentProcessor) ProcessPayment(amount float64) error {
// 处理支付逻辑
fmt.Printf("Processing payment of $%.2f\n", amount)
return nil
}
// OrderProcessor 处理订单
type OrderProcessor struct {
paymentProcessor *PaymentProcessor
}
func (op *OrderProcessor) ProcessOrder(customer *Customer, order *Order) error {
fmt.Printf("Processing order for %s\n", customer.Name)
// OrderProcessor 只与 PaymentProcessor 直接交互
return op.paymentProcessor.ProcessPayment(order.TotalCost)
}
// 使用示例
func main() {
customer := &Customer{Name: "John Doe"}
order := &Order{
Items: []string{"Book", "Pen"},
TotalCost: 25.99,
}
paymentProcessor := &PaymentProcessor{}
orderProcessor := &OrderProcessor{paymentProcessor: paymentProcessor}
err := orderProcessor.ProcessOrder(customer, order)
if err != nil {
fmt.Println("Error processing order:", err)
} else {
fmt.Println("Order processed successfully")
}
}
在这个例子中,OrderProcessor
只与 Customer
、Order
和 PaymentProcessor
直接交互。它不需要知道支付处理的具体细节,而是将这个责任委托给了 PaymentProcessor
。这种设计减少了类之间的耦合,提高了系统的模块化程度,符合迪米特法则的要求。
总结
本篇文章主要介绍了几种面向对象设计的核心思想。再次回顾一下上面提到的设计模式:
-
🔍 单一职责原则指出,一个类应该只有一个发生变化的原因,即只负责一个职责,这有助于提高代码的可维护性和可扩展性。
-
🔓 开闭原则要求软件实体对扩展开放,对修改关闭,新增功能时应该添加新代码而不是修改原有代码。这可以提高代码的复用性。
-
👩🎓 里氏代换原则要求子类可以替换基类,不会破坏程序的正确性。这有助于建立健壮的继承体系。
-
🧱 接口隔离原则指出,一个接口应该只定义一组相关的方法,而不应该将不相关的方法捆绑在一起。这有助于降低类之间的耦合度。
-
🔨 合成复用原则优先使用对象组合而不是类继承来实现代码复用,这可以获得更好的可扩展性。
-
🤝 迪米特法则要求一个对象应该对其他对象保持最少的了解,只与直接相关的对象交互,这有助于提高模块化程度。
后面的设计模式主要都是这几种设计原则的体现,但是,不存在满足所有原则的设计模式,更多是多种原则的组合与取舍。