单一职责原则(SRP)
原则定义:一个类只有一个发生变化的原因,解释来说,就是一个类,只负责一个功能领域中的相应职责。
单一职责原则最鲜明的特点是高内聚,低耦合,如果将许多功能放在一个类中,使用if
来判断这个功能应当由哪个角色去操作,这样的代码在维护的时候过于麻烦,因此,在软件的设计中,要将不同职责的代码分离,降低软件的复杂度,是模块化操作的最基本原则。
Java Demo
public interface IUserService {
void advertisement();
void blueLight();
}
public class OdinaryUserServiceImpl implements IUserService {
@Override
public void advertisement() {
System.out.println("ordinary,short advertisement");
}
@Override
public void blueLight() {
System.out.println("ordinary,1080p");
}
}
public class GuestUserServiceImpl implements IUserService {
@Override
public void advertisement() {
System.out.println("guest,long advertisement");
}
@Override
public void blueLight() {
System.out.println("guest,only 720P");
}
}
public class VipUserServiceImpl implements IUserService {
@Override
public void advertisement() {
System.out.println("Vip,No advertisement");
}
@Override
public void blueLight() {
System.out.println("Vip,high quality");
}
}
public class ApiTest {
public static void main(String[] args) {
IUserService guest = new GuestUserServiceImpl();
guest.advertisement();
guest.blueLight();
}
}
开闭原则(OCP)
原则定义:对于扩展是开放的,对于修改是封闭的。即软件实体应该尽量在不修改源代码的基础上进行功能的扩展。根据这个原则的定义,我们来回想一下Java语言中,那种特性可以让开发者做到仅扩展,不修改,我想答案已经呼之欲出了,那就是继承。
定义了一个抽象基类
Shape
,它有一个抽象方法draw()
。然后我们创建了两个具体的子类Circle
和Rectangle
,它们实现了draw()
方法。在客户端代码中,我们可以通过向Drawing
类的drawAllShapes()
方法传递不同的Shape
对象来绘制不同的形状,而不必修改drawAllShapes()
方法的源代码。
Java Demo
import java.util.ArrayList;
import java.util.List;
// 抽象基类
interface Shape {
void draw();
}
// 具体子类 - Circle
class Circle implements Shape {
@Override
public void draw() {
System.out.println("Drawing Circle");
}
}
// 具体子类 - Rectangle
class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("Drawing Rectangle");
}
}
// 客户端代码
class Drawing {
public void drawAllShapes(List<Shape> shapes) {
for (Shape shape : shapes) {
shape.draw();
}
}
}
public class Main {
public static void main(String[] args) {
List<Shape> shapes = new ArrayList<>();
shapes.add(new Circle());
shapes.add(new Rectangle());
Drawing drawing = new Drawing();
drawing.drawAllShapes(shapes);
}
}
C++ Demo
#include <iostream>
#include <vector>
// 抽象基类
class Shape {
public:
virtual void draw() const = 0;
};
// 具体子类 - Circle
class Circle : public Shape {
public:
void draw() const override {
std::cout << "Drawing Circle" << std::endl;
}
};
// 具体子类 - Rectangle
class Rectangle : public Shape {
public:
void draw() const override {
std::cout << "Drawing Rectangle" << std::endl;
}
};
// 客户端代码
void drawAllShapes(const std::vector<Shape*>& shapes) {
for (const auto& shape : shapes) {
shape->draw();
}
}
int main() {
std::vector<Shape*> shapes;
shapes.push_back(new Circle());
shapes.push_back(new Rectangle());
drawAllShapes(shapes);
// 清理内存
for (const auto& shape : shapes) {
delete shape;
}
return 0;
}
Golang Demo
package main
import "fmt"
// 接口
type Shape interface {
Draw()
}
// 具体类型 - Circle
type Circle struct{}
func (c *Circle) Draw() {
fmt.Println("Drawing Circle")
}
// 具体类型 - Rectangle
type Rectangle struct{}
func (r *Rectangle) Draw() {
fmt.Println("Drawing Rectangle")
}
// 客户端代码
func DrawAllShapes(shapes []Shape) {
for _, shape := range shapes {
shape.Draw()
}
}
func main() {
shapes := []Shape{&Circle{}, &Rectangle{}}
DrawAllShapes(shapes)
}
里氏替换原则(LSP)
原则定义:任何基类可以出现的地方子类一定可以出现。LSP
是继承复用的基石,当子类可以替换到基类,且软件功能不受影响的时候,基类才是真正的被复用,里氏替换原则是对开闭原则的补充,不仅仅只是单纯的扩展基类的功能,更是对于基类与子类继承关系的一种具体体现。
直白来讲,对于一个软件来说,它察觉不到父类与子类的区别,即使将父类完全替换为其中一个子类,软件的功能也完全不受影响,所有使用父类的地方,完全可以透明的使用子类。
引申意义:子类可以扩展父类的功能,但是不能修改父类的功能。
-
含义1:子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法。
-
含义2:子类中可以增加自己特有的方法。
-
含义3:当子类方法重载父类方法时,方法的前置条件(即方法的入参、输入)要比父类方法的输入参数更宽松。
-
含义4:当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的输出/返回值)要比父类更严格或相等。
迪米特法则(LoD)
原则定义:迪米特法则的意义在于低耦合,一个对象要更加关注自身负责的功能,不要过多的参与其他对象,又叫做最少知道原则。
迪米特原则要求我们在开发过程中,应减少两个对象之间的交互,如果两个对象之间不必彼此通信,那么这两个对象就不必要发生任何直接的作用,如果其中的一个对象需要调用另一个对象的方法时,可以通过第三者转发来进行调用。
迪米特法则强调不要和“陌生人”说话、只与你的直接朋友通信,在迪米特法则中,对于一个对象,其朋友包括以下几类:
-
当前对象本身(this);
-
以参数形式传入到当前对象方法中的对象;
-
当前对象的成员对象;
-
如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友;
-
当前对象所创建的对象。
迪米特法则的使用除了“只和朋友交流以外”,还可以分为以下几点:
避免链式调用,不要在一个对象中深入调用另一个对象的方法,而应该通过尽可能少的层次进行交流.
尽量减少对外部对象的访问,尽量通过自身方法来完成需要的操作,而不是通过获取外部对象的状态来间接实现.
将需要进行交流的对象尽可能地封装起来,使得其他对象不需要了解其内部实现细节,只需要通过接口进行交流。
举个例子:*User
类仅与其直接的朋友 Friend
类交流,而不直接调用 Friend
类的方法。通过引入 Message
类来表示消息,并在 Friend
类中处理消息的发送,从而遵循了迪米特法则。*
Java Demo
import java.util.List;
import java.util.ArrayList;
// User 类
class User {
private String name;
private List<Friend> friends;
public User(String name) {
this.name = name;
this.friends = new ArrayList<>();
}
// 添加好友
public void addFriend(Friend friend) {
this.friends.add(friend);
}
// 向所有好友发送消息
public void sendMessageToFriends(String messageContent) {
for (Friend friend : friends) {
friend.sendMessage(messageContent);
}
}
}
// Friend 类
class Friend {
private String name;
public Friend(String name) {
this.name = name;
}
// 接收消息
public void sendMessage(String messageContent) {
System.out.println(name + " received message: " + messageContent);
}
}
public class Main {
public static void main(String[] args) {
User user = new User("Alice");
user.addFriend(new Friend("Bob"));
user.addFriend(new Friend("Charlie"));
user.sendMessageToFriends("Hello, friends!");
}
}
C++ Demo
class User {
private String name;
private List<Friend> friends;
public User(String name) {
this.name = name;
this.friends = new ArrayList<>();
}
public void addFriend(Friend friend) {
this.friends.add(friend);
}
public void sendMessageToFriends(String messageContent) {
for (Friend friend : friends) {
friend.sendMessage(messageContent);
}
}
}
class Friend {
private String name;
private List<Message> messages;
public Friend(String name) {
this.name = name;
this.messages = new ArrayList<>();
}
public void sendMessage(String messageContent) {
this.messages.add(new Message(messageContent));
}
}
class Message {
private String content;
public Message(String content) {
this.content = content;
}
}
Golang Demo
package main
import "fmt"
// User 结构体
type User struct {
name string
friends []*Friend
}
// NewUser 函数创建一个新的用户实例
func NewUser(name string) *User {
return &User{
name: name,
friends: make([]*Friend, 0),
}
}
// AddFriend 方法向用户的好友列表中添加一个好友
func (u *User) AddFriend(friend *Friend) {
u.friends = append(u.friends, friend)
}
// SendMessageToFriends 方法向用户的所有好友发送消息
func (u *User) SendMessageToFriends(messageContent string) {
for _, friend := range u.friends {
friend.SendMessage(messageContent)
}
}
// Friend 结构体
type Friend struct {
name string
}
// NewFriend 函数创建一个新的好友实例
func NewFriend(name string) *Friend {
return &Friend{
name: name,
}
}
// SendMessage 方法向好友发送消息
func (f *Friend) SendMessage(messageContent string) {
fmt.Printf("%s received message: %s\n", f.name, messageContent)
}
func main() {
user := NewUser("Alice")
user.AddFriend(NewFriend("Bob"))
user.AddFriend(NewFriend("Charlie"))
user.SendMessageToFriends("Hello, friends!")
}
依赖倒置原则(DCP)
原则定义:高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。
在传统的开发模式中,依赖遵循自定向下的设计,逐层依赖,中层和高层的耦合度很高,牵一发而动全身,不利于日常维护。
所以,我们要将具体的功能从高层方法中抽象出来,使高层对象通过愁里出来的中间层完善功能,底层对象通过接入中间层接口完成对功能的开发。
依赖倒置原则可以概括为一条思想:针对接口编程,不针对实现编程。顾名思义,这个原则旨在强调编写代码时要依赖于抽象接口而不是具体的实现类,举个例子,假设我们有一个电子邮件系统,需要将对接各种客户端比如网易、QQ等的邮件发送给客户,高层实现就是发送邮件这个广泛的行为,因为对接客户端的不同,我们可以将这个行为抽象出不同的接口,每个接口对接一种客户端,这样,用户发送邮件不会在意具体那种客户端以及代码时如何处理发送邮件的过程,只需要专注于发送邮件的行为就好了。
这样也可以降低代码之间的耦合度,使编程更加灵活。
Java Demo
// 抽象接口
interface Database {
void query();
}
// 具体实现
class MySQLDatabase implements Database {
@Override
public void query() {
System.out.println("Querying MySQL database");
}
}
// 高层模块
class App {
private final Database database;
public App(Database database) {
this.database = database;
}
public void fetchData() {
database.query();
}
}
public class Main {
public static void main(String[] args) {
Database mySQLDatabase = new MySQLDatabase();
App app = new App(mySQLDatabase);
app.fetchData();
}
}
C++ Demo
#include <iostream>
// 抽象接口
class IDatabase {
public:
virtual void query() = 0;
};
// 具体实现
class MySQLDatabase : public IDatabase {
public:
void query() override {
std::cout << "Querying MySQL database" << std::endl;
}
};
// 高层模块
class App {
private:
IDatabase* database;
public:
App(IDatabase* db) : database(db) {}
void fetchData() {
database->query();
}
};
int main() {
MySQLDatabase mySQLDatabase;
App app(&mySQLDatabase);
app.fetchData();
return 0;
}
接口隔离原则(ISP)
原则定义:接口中的方法不是每一种都会被用户使用,所以要求开发者将庞大臃肿的接口细分为小接口,让每个接口只包含当前用户感兴趣的方法,即只让用户依赖其必须依赖的接口,而不需要依赖其他接口。
-
一个类对一个类的依赖应该建立在最小的接口上
-
建立单一接口不要建立庞大臃肿的接口
-
尽量细化接口,接口中的方法应该尽量少
这样就很符合高内聚,低耦合的思想。
Golang Demo
package main
import "fmt"
// 图形接口
type Shape interface {
Draw()
}
// 移动接口
type Movable interface {
Move(x, y float64)
}
// 缩放接口
type Scalable interface {
Scale(factor float64)
}
// 旋转接口
type Rotatable interface {
Rotate(angle float64)
}
// 矩形
type Rectangle struct{}
func (r Rectangle) Draw() {
fmt.Println("Drawing Rectangle")
}
func (r Rectangle) Move(x, y float64) {
fmt.Printf("Moving Rectangle to (%.2f, %.2f)\n", x, y)
}
func (r Rectangle) Scale(factor float64) {
fmt.Printf("Scaling Rectangle by %.2f\n", factor)
}
func (r Rectangle) Rotate(angle float64) {
fmt.Printf("Rotating Rectangle by %.2f degrees\n", angle)
}
// 圆形
type Circle struct{}
func (c Circle) Draw() {
fmt.Println("Drawing Circle")
}
func (c Circle) Move(x, y float64) {
fmt.Printf("Moving Circle to (%.2f, %.2f)\n", x, y)
}
func (c Circle) Scale(factor float64) {
fmt.Printf("Scaling Circle by %.2f\n", factor)
}
// 客户端代码
func main() {
// 创建矩形并进行操作
rectangle := Rectangle{}
rectangle.Draw()
rectangle.Move(10, 10)
rectangle.Scale(2)
rectangle.Rotate(45)
// 创建圆形并进行操作
circle := Circle{}
circle.Draw()
circle.Move(5, 5)
circle.Scale(1.5)
}
总结
不同的原则体现了不同的语言的基础体系,如继承、抽象、多态、封装等,总的来说,设计模式最先被称为“可复用面向对象软件的基础”,而面向对象的思想已经在个大编程语言中扎下了深深的根,我的老师曾经说过:”将设计模式全部写一遍,那么你就完全学会了这门语言。“今日看来,所言非虚。