hi 大家好,我是 DHL。就职于美团、快手、小米。公众号:ByteCode,分享有用的原创文章,涉及鸿蒙、Android、Java、Kotlin、性能优化、大厂面经。
来自微信小程序「猿面试」当中的一道面试题,更多大厂面试题 Java
、Android、鸿蒙和ArkTS
、设计模式
、算法和数据结构
欢迎前往查看「猿面试」。
理解设计模式的七大原则,能够帮助开发者写出更好的代码,因此在面试中也是问的比较多的问题,这篇文章将会配合示例,简要说明设计模式的六大原则,帮助你在面试中更好的回答这些问题。
1. 单一职责原则 (Single Responsibility Principle, SRP)
每个类应该只负责一项职责,不应该成为一个多功能的“大杂烩”。例如一个 Logger
类应该只负责日志记录的职责,而不应该同时处理文件操作等其他工作。
// 符合单一职责原则的Logger类
public class Logger {
public void log(String message) {
// 只负责日志逻辑
System.out.println(message);
}
}
2. 开放封闭原则 (Open/Closed Principle, OCP)
软件实体(如类、模块、函数等)应该对扩展开放,对修改封闭,这意味着在不修改现有代码的基础上,可以增加新的功能。
// 绘图类接口
public interface Shape {
void draw();
}
// 圆形类
public class Circle implements Shape {
public void draw() {
// 绘制圆形
}
}
// 画布类
public class Canvas {
public void drawShape(Shape shape) {
shape.draw(); // 使用Shape接口绘制各种形状,支持多种扩展形状,无需修改此类
}
}
使用抽象类或接口允许类进行扩展,而无需修改类本身的代码。
3. 里氏替换原则 (Liskov Substitution Principle, LSP)
子类应该能够替换它们的父类,并且替换之后不改变程序的正确性。也就是说一个函数如果使用的是一个基类对象,那么它应该能够使用这个基类的任何一个子类对象,而程序依然能正确运行
// 鸟类
public class Bird {
public void fly() {
// 实现飞行
}
}
// 燕子类
public class Swallow extends Bird {
// 可以直接使用 Bird 类的飞行功能
}
// 使用鸟类对象的客户端代码
public class BirdUser {
public void makeBirdFly(Bird bird) {
bird.fly();
}
}
4. 依赖倒置原则 (Dependency Inversion Principle, DIP)
这个原则告诉我们,我们的代码应该依赖于接口和抽象类,而不是具体的类。这可以使我们更容易地替换组件,而不用去修改依赖于这些组件的其他代码。
// 抽象层高级的存储接口
public interface Storage {
void save(Object data);
}
// 实现了 Storage 的文件存储类
public class FileStorage implements Storage {
public void save(Object data) {
// 将数据保存到文件
}
}
// 高层模块
public class DataProcessor {
private Storage storage;
public DataProcessor(Storage storage) {
this.storage = storage;
}
public void process(Object data) {
// 处理数据...
storage.save(data);
}
}
5. 接口隔离原则 (Interface Segregation Principle, ISP)
这个原则告诉我们,我们应该设计细小的、专门的接口,而不是设计大而全的接口。这个原则鼓励我们把大的接口拆分成一组更小的和更具体的接口,只需要知道和使用它们真正需要的接口。
// 细分接口
public interface Printer {
void print(Document d);
}
public interface Scanner {
Document scan();
}
// 组合设备实现两个接口
public class ComboDevice implements Printer, Scanner {
public void print(Document d) {
// 实现打印功能
}
public Document scan() {
// 实现扫描功能
return new Document();
}
}
6. 迪米特法则 (Law of Demeter, LoD) 或最小知识原则
对象应该尽可能少地了解其他对象。换言之,一个对象应该对其他对象有最小的了解,并且只与其直接的朋友通信。在这里,“朋友”指的是那些直接的成员变量、方法参数或者是方法内部创建的对象。
// 一个具体的例子:顾客、服务员和厨师
class Customer {
void dine() {
Waiter waiter = new Waiter();
waiter.takeOrder();
}
}
class Waiter {
void takeOrder() {
Chef chef = new Chef();
chef.cook();
}
}
class Chef {
void cook() {
// cooking the dish
}
}
// 在这里,Customer 类并不直接和 Chef 类交流,
// 它通过 Waiter 类来让厨师开始烹饪。这样,Customer 类就遵循了迪米特法则。
在这个例子中,Customer
不需要知道 Chef
接口的细节。它只需要与 Waiter
交流,这样就减少了类之间的直接依赖关系。
依此,Customer
关心的是结果,而过程由 Waiter
和 Chef
负责,这样做有助于减少系统中各部分的耦合,使得代码更容易理解和维护。
合成复用原则
合成复用原则强调优先使用对象组合(通过组合已有的类来构成新的类)而不是继承来达到复用的目的,这样可以使系统更加灵活和可维护。
如果修改父类的方法会影响子类的方法,那么这个父类和子类就不应该采用继承,而应该使用组合。
合成复用原则的错误示例:通过继承来复用代码
class Vehicle {
void startEngine() {
// ...启动发动机...
}
}
class Car extends Vehicle {
// Car 继承了 Vehicle,自动获得了 startEngine 方法
}
合成复用原则的正确示例:通过组合来复用代码
class Engine {
void start() {
// ...启动发动机...
}
}
class Car {
private Engine engine; // Car 类包含一个 Engine 类型的对象
Car(Engine engine) {
this.engine = engine; // 通过构造器传入 Engine 实例
}
void startCar() {
engine.start(); // 调用 Engine 类的 start 方法来启动车辆
}
}
在后者的例子中,Car 类并不是继承了 Engine 类,而是有了一个 Engine 类型的对象,它可以调用 Engine 对象的 start 方法来启动发动机。这样做的好处是可以灵活地更换 Engine,而不会影响到 Car 类本身,同时也避免了通过继承而造成的紧耦合问题。
全文到这里就结束了,感谢你的阅读,如果文章对你有帮助,欢迎在看、点赞、分享给身边的小伙伴,你的点赞是我持续更新的动力。
推荐阅读:
Hi 大家好,我是 DHL,大厂程序员,公众号:ByteCode ,在美团、快手、小米工作过。分享有用的原创文章,涉及鸿蒙、Android、Java、Kotlin、性能优化、大厂面经。