软件设计的5大原则
本文介绍软件设计的5大原则:开闭原则、依赖倒置原则、里氏替换原则、单一职责原则、接口隔离原则。
1. 开闭原则
开闭原则是指代码应该对扩展开放,对修改封闭。这个原则主要是用来描述,未来需求变化的时候,应该尽量不修改已有的类,而是通过增加新的类来解决。
如何来理解呢?比如我们有一个发送验证码的功能,一开始的需求是生成验证码,通过短信发送给用户,我们的VerificationCodeSender的send方法,先生成验证码,然后调用smsNotifier把验证码通知给用户。
public class VerificationCodeSender { private SmsNotifier smsNotifier; public VerificationCodeSender() { smsNotifier = new SmsNotifier(); } public String generateVerificationCode() { return "验证码"; } public void send() { String vc = generateVerificationCode(); smsNotifier.notify(vc); } }
public class SmsNotifier { public void notify(String verificationCode) { System.out.println("短信发送" + verificationCode); } }
如果后面需求变了,也要能通过邮件发送验证码给用户,这个时候,我们可能会增加一个EmailNotifier,然后VerificationCodeSender的send方法增加一个type来区分,是通过短信还是邮件发送验证码,代码会变成这样。
public class VerificationCodeSender { private SmsNotifier smsNotifier; private EmailNotifier emailNotifier; public VerificationCodeSender() { smsNotifier = new SmsNotifier(); emailNotifier = new EmailNotifier(); } public String generateVerificationCode() { return "验证码"; } public void send(int type) { String vc = generateVerificationCode(); if (type == 1) { smsNotifier.notify(vc); } else if (type == 2) { emailNotifier.notify(vc); } } }
public class EmailNotifier { public void notify(String verificationCode) { System.out.println("邮件发送" + verificationCode); } }
通过邮件发送验证码的问题暂时解决了,但是如果需求变成也可以通过电话、5G消息来通知验证码,那是不是又得改VerificationCodeSender里面的send方法了,增加新的if判断。虽然解决了问题,但是这样的代码可扩展性就差了,来一种类型,就增加一个if,涉及到已有类的修改,就不符合开闭原则了,对修改不是封闭的。
我们可以通过策略模式来重构,抽象一个Notifier接口,短信和邮件发送就是具体的策略,通过setNotifier来设置不同的策略,以后增加新的发送方式,增加一种策略,实现Notifier的接口就好了,这样就不会涉及修改VerificationCodeSender的send方法了,从而满足了开闭原则。
public interface Notifier { void notify(String verificationCode); }
public class VerificationCodeSender { private Notifier notifier; public void setNotifier(Notifier notifier) { this.notifier = notifier; } public String generateVerificationCode() { return "验证码"; } public void send() { String vc = generateVerificationCode(); if (notifier != null) { notifier.notify(vc); } } }
public class App { public static void main(String[] args) { VerificationCodeSender sender = new VerificationCodeSender(); sender.setNotifier(new SmsNotifier()); sender.send(); sender.setNotifier(new EmailNotifier()); sender.send(); } }
2. 依赖倒置原则
依赖倒置原则说的是高层不应该依赖低层,低层也不应该依赖高层,高层和低层应该都依赖高层抽象出来的一组接口,让低层来实现这一组接口。一般框架设计的核心都是利用了依赖倒置原则,因为开发框架的时候,应用代码还没有开发出来,开发框架无法调用应用代码,也不可能调用应用代码,让开发框架和应用代码都依赖一组接口规范,从而满足依赖倒置原则。
比如我们要在一个web框架里面增加对数据的加密,应用程序可以自己实现加密算法,并注册到框架里面,我们可以抽象出一个加密接口Encrypt,定义了doEncrypt方法,并提供EncryptRegister类注册加密具体实现类,ResponseProcessor是调用框架里面注册的加密算法,并对数据进行加密处理。应用代码实现了自己的Base64Encrypt加密算法,并被框架调用。框架依赖Encryp接口实现数据加密的功能,应用代码也依赖这个接口实现自己的加密算法,通过对接口共同依赖,实现了依赖倒置原则。依赖倒置原则有时候也叫好莱坞原则,don't call me ,I will call you。这里的me和I指的是框架,you指的是应用代码。
public interface Encrypt { String doEncrypt(String data); }
public class EncryptRegister { private static List<Encrypt> encryptList; public static List<Encrypt> getEncryptList() { return encryptList; } public static void register(Encrypt encrypt) { if (encryptList == null) { encryptList = new ArrayList<>(); } encryptList.add(encrypt); } }
public class ResponseProcessor { public String process(String data) { String encryptedData = data; List<Encrypt> encryptList = EncryptRegister.getEncryptList(); if (encryptList != null) { for (Encrypt encrypt : encryptList) { encryptedData = encrypt.doEncrypt(encryptedData); } } return encryptedData; } }
public class App { public static void main(String[] args) { // 注册加密算法 EncryptRegister.register(new Base64Encrypt()); System.out.println("框架开始运行"); System.out.println(new ResponseProcessor().process("123")); } public static class Base64Encrypt implements Encrypt { @Override public String doEncrypt(String data) { return "do base64 encrypt..." + data; } } }
3. 里氏替换原则
里氏替换原则是用来衡量父类和子类的继承关系是否合理的一个原则。判断一个类是否是父类的子类时,一般会用IS-A来进行判断,里氏替换原则要求,判断继承关系还应该结合具体的业务场景,如果用这个类替换所有父类出现的地方,不影响系统功能,我们就说这个继承是符合里氏替换原则,如果影响了现有功能,那么这个继承就不符合里氏替换原则。
比如下面这种继承关系,杯子是父类,玻璃杯和塑料杯都继承了杯子,人可以用玻璃杯或者塑料杯喝水,继承关系是合理的。但是如果现在有一个实验室的量杯,也继承杯子,通过IS-A来判断的话,量杯是杯子,杯子是量杯的父类,但是利用里氏替换原则判断,用量杯替代杯子,让人用量杯来喝水,这个并不合理,因为是实验室的量杯,是做实验用的,用来喝水对身体不好,所以量杯继承杯子,是不符合里氏替换原则的。

不符合IS-A继承关系的,一定是不符合里氏替换原则的。比如我们有一个类纸质书,它的原材料是纸张,我们用一个电子书来继承纸质书,这显然是不合理的,电子书的原材料不可能是纸张。所以这个继承是不符合里氏替换原则的。

里氏替换原则强调的是子类对象的行为不应该影响系统功能,用子类替换父类所有出现的地方,子类的行为仍能能够正常反映父类的行为。这就要求子类继承父类的方法时,子类的方法可以做的事情不应该比父类少。
比如我们有一个手机的父类Phone,有一个充电的方法charge,可以支持充电,我们还有一个子类BrokenPhone,坏掉的手机,覆盖了charge方法,不支持充电。子类的方法可以做的事情比父类要少,如果这个时候我们有代码App需要调用Phone的charge方法充电,用子类BrokenPhone来替代父类的话,会导致这部分代码不能正常工作,因为抛出异常了。所以BrokenPhone继承Phone是不符合里氏替换原则的。
public class Phone { public void charge() { System.out.println("充电"); } }
public class BrokenPhone extends Phone { public void charge() { throw new UnsupportedOperationException("不支持充电"); } }
public class App { public static void main(String[] args) { Phone phone = new Phone(); phone.charge(); } }
4. 单一职责原则
单一职责原则指的是当一个类A依赖于另一个类B的时候,类B中提供的方法/职责应该是单一的,不同职责的方法不应该出现一个类中。
比如系统中DataUtility定义2个方法,countVisitors用来统计访问网站的人数,cleanExpiredData用来清理过期的数据,统计模块Statistics会用到countVisitors方法,清理模块Cleaner会用到clean模块,Statistics和Cleaner都被迫依赖了DataUtility类中他们不需要的方法,DataUtility本身提供的职责也不是单一的,这样就违背了单一职责原则。
public interface DataUtility { long countVisitors(); void cleanExpiredData(); }
public class DataUtilityImpl implements DataUtility { @Override public long countVisitors() { System.out.println("统计访问者数量"); return 0; } @Override public void cleanExpiredData() { System.out.println("清理过期数据"); } }
public class Statistics { private DataUtility dataUtility; public Statistics() { dataUtility = new DataUtilityImpl(); } public void doStats() { dataUtility.countVisitors(); } }
public class Cleaner { private DataUtility dataUtility; public Cleaner() { dataUtility = new DataUtilityImpl(); } public void clean() { dataUtility.cleanExpiredData(); } }
countVisitors方法应该单独放在一个接口VisitorsUtility中,cleanExpiredData方法应该单独放在另一个接口CleanUtility中,修改后的代码如下:
public interface VisitorsUtility { long countVisitors(); }
public class VisitorsUtilityImpl implements VisitorsUtility { @Override public long countVisitors() { System.out.println("统计访问者数量"); return 0; } }
public class Statistics { private VisitorsUtility visitorsUtility; public Statistics() { visitorsUtility = new VisitorsUtilityImpl(); } public void doStats() { visitorsUtility.countVisitors(); } }
public interface CleanUtility { void cleanExpiredData(); }
public class CleanUtilityImpl implements CleanUtility { @Override public void cleanExpiredData() { System.out.println("清理过期数据"); } }
public class Cleaner { private CleanUtility cleanUtility; public Cleaner() { cleanUtility = new CleanUtilityImpl(); } public void clean() { cleanUtility.cleanExpiredData(); } }
5. 接口隔离原则
接口隔离原则是指多个类A和类B依赖某个类C的时候,类A和类B应该只依赖它们需要的方法,而不应该依赖它们不需要的方法,可以通过把类C中的方法拆分到多个接口中,从而隔离这些方法。
比如系统中有一个全局时钟SystemClock,可以返回清理数据的过期时间基准purgeBaseline,如果数据的时间大于基准,就清理掉数据,PurgeData依赖SystemClock的getPurgeBaseline方法。系统中有一个监控类,观察系统负载,根据数据量调整清理时间,Monitor依赖SystemClock的reset方法。这里PurgeData和Monitor都被迫依赖了他们不需要的方法,PurgeData只需要getPurgeBaseline方法,但是被迫依赖了reset方法,并且如果使用者不小心调用了reset方法,修改了SystemClock的状态,就可能导致系统出错。Monitor类只关心reset方法,但是也被迫依赖了getPurgeBaseline方法,即使没有用到,但是也被迫依赖了多余的方法。
public interface SystemClock { long getPurgeBaseline(); void reset(long purgeBaseline); }
public class SystemClockImpl implements SystemClock { private long purgeBaseline; public long getPurgeBaseline() { return purgeBaseline; } public void reset(long purgeBaseline) { this.purgeBaseline = purgeBaseline; } }
public class PurgeData { private SystemClock systemClock; public PurgeData(SystemClock systemClock) { this.systemClock = systemClock; } public void doPurge(long dataTime, String data) { if (systemClock.getPurgeBaseline() < dataTime) { System.out.println("purge data " + data); } } }
public class Monitor { private SystemClock systemClock; public Monitor(SystemClock systemClock) { this.systemClock = systemClock; } public void observe() { systemClock.reset(1000); try { Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); } System.out.println("数据过多,调整清理时间"); systemClock.reset(900); } }
public class App { public static void main(String[] args) { final SystemClock systemClock = new SystemClockImpl(); Thread purgeThread = new Thread(() -> { PurgeData purgeData = new PurgeData(systemClock); for (int i = 0; i < 10; ++i) { try { Thread.sleep(100); } catch (Exception e) { e.printStackTrace(); } purgeData.doPurge(999, "数据" + i); } }); Thread adjustThread = new Thread(() -> { Monitor monitor = new Monitor(systemClock); monitor.observe(); }); purgeThread.start(); adjustThread.start(); } }
可以通过拆分成2个接口Baseline和Resetable,Baseline中提供getPurgeBaseline方法,Resetable中提供reset方法,这样PurgeData和Monitor都只会依赖它们需要的方法,而不会依赖它们不需要的方法,这样就实现了接口隔离原则。
public interface Baseline { long getPurgeBaseline(); }
public interface Resetable { void reset(long purgeBaseline); }
public class SystemClockImpl implements Baseline, Resetable { private long purgeBaseline; public long getPurgeBaseline() { return purgeBaseline; } public void reset(long purgeBaseline) { this.purgeBaseline = purgeBaseline; } }
public class PurgeData { private Baseline baseline; public PurgeData(Baseline baseline) { this.baseline = baseline; } public void doPurge(long dataTime, String data) { if (baseline.getPurgeBaseline() < dataTime) { System.out.println("purge data " + data); } } }
public class Monitor { private Resetable resetable; public Monitor(Resetable systemClock) { this.resetable = systemClock; } public void observe() { resetable.reset(1000); try { Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); } System.out.println("数据过多,调整清理时间"); resetable.reset(900); } }
public class App { public static void main(String[] args) { final SystemClockImpl systemClock = new SystemClockImpl(); Thread purgeThread = new Thread(() -> { PurgeData purgeData = new PurgeData(systemClock); for (int i = 0; i < 10; ++i) { try { Thread.sleep(100); } catch (Exception e) { e.printStackTrace(); } purgeData.doPurge(999, "数据" + i); } }); Thread adjustThread = new Thread(() -> { Monitor monitor = new Monitor(systemClock); monitor.observe(); }); purgeThread.start(); adjustThread.start(); } }
浙公网安备 33010602011771号