六大设计原则
单一职责原则SRP
定义是:应该有且仅有一个原因引起类的变更。”
比如我们可以把用户相关的类抽取成一个用户信息BO(Business Object,业务对象),把行为抽取成一个用户信息修改Biz(Business Logic,业务逻辑)
我们也可以把电话的接口抽象为连接管理接口和数据传输接口
但是职责”或变化原因”都是不可度量的。所以实际上受很多因素制约。
接口一定要做到单一职责,类的设计尽量做到只有一个原因引起变化。”
里式替换
- 子类必须完全实现父类的方法,即父类出现的地方子类就可以出现。但是如果子类不能完整实现父类的方法,或者父类的某些方法在子类中发生畸变,则建议断开父子继承关系,采用依赖、聚集等关系代替继承。
- 子类可以有自己的个性
- 覆盖或实现父类的方法时,输入参数可以被放大。子类中方法的前置条件必须与超类中被覆写的方法的前置条件相同或更宽松
- 覆写或实现父类的方法时输出结果可以被缩小。增强程序健壮性,版本升级时,可以保持兼容性。即使增加子类,原有的子类还可以继续运行。在实际项目中,每个子类对应不同的业务含义,使用父类作为参数,传递不同的子类完成不同的业务逻辑。
依赖倒置原则
● 高层模块不应该依赖低层模块,两者都应该依赖其抽象;
● 抽象不应该依赖细节;
● 细节应该依赖抽象。
在Java中,只要定义变量就必然要有类型,一个变量可以有两种类型:表面类型和实际类型,表面类型是在定义的时候赋予的类型,实际类型是对象的类型,如zhangSan的表面类型是IDriver,实际类型是Driver。
public class Client {
public static void main(String[] args) {
IDriver zhangSan = new Driver();
ICar bmw = new BMW();
//张三开宝马车
zhangSan.drive(bmw);
//张三开奔驰车
ICar benz = new Benz();
zhangSan.drive(benz);
}
}
在新增加低层模块时,只修改了业务场景类,也就是高层模块,对其他低层模块如Driver类不需要做任何修改,业务就可以运行,把变更”引起的风险扩散降到最低。
注意 在Java中,只要定义变量就必然要有类型,一个变量可以有两种类型:表面类型和实际类型,表面类型是在定义的时候赋予的类型,实际类型是对象的类型,如zhangSan的表面类型是IDriver,实际类型是Driver。
依赖倒置对并行开发的影响。两个类之间有依赖关系,只要制定出两者之间的接口(或抽象类)就可以独立开发了,而且项目之间的单元测试也可以独立地运行,而TDD(Test-Driven Development,测试驱动开发)开发模式就是依赖倒置原则的最高级应用。”
司机驾驶汽车的例子,甲程序员负责IDriver的开发,乙程序员负责ICar的开发,两个开发人员只要制定好了接口就可以独立地开发了,甲开发进度比较快,完成了IDriver以及相关的实现类Driver的开发工作,而乙程序员滞后开发,那甲是否可以进行单元测试呢?答案是可以,我们引入一个JMock工具,其最基本的功能是根据抽象虚拟一个对象进行测试,测试类如下所示。
public class DriverTest extends TestCase{
Mockery context = new JUnit4Mockery();
@Test
public void testDriver() {
//根据接口虚拟一个对象
final ICar car = context.mock(ICar.class);
IDriver driver = new Driver();
//内部类”
context.checking(new Expectations(){{
oneOf (car).run();
}});
driver.drive(car);
}
}
我们只需要一个ICar的接口,就可以对Driver类进行单元测试。从这一点来看,两个相互依赖的对象可以分别进行开发,孤立地进行单元测试,进而保证并行开发的效率和质量”
抽象是对实现的约束,对依赖者而言,也是一种契约,不仅仅约束自己,还同时约束自己与外部的关系,其目的是保证所有的细节不脱离契约的范畴,确保约束双方按照既定的契约(抽象)共同发展,只要抽象这根基线在,细节就脱离不了这个圈圈,始终让你的对象做到言必信,行必果”。”
依赖的传递方法
- 构造函数传递依赖对象
- Setter方法传递依赖对象
- 接口声明传递依赖对象
// 1
public interface IDriver {
//是司机就应该会驾驶汽车
public void drive();
}
public class Driver implements IDriver{
private ICar car;
//构造函数注入
public Driver(ICar _car){
this.car = _car;
}
//司机的主要职责就是驾驶汽车
public void drive(){
this.car.run();
}
}
// 2
public interface IDriver {
//车辆型号
public void setCar(ICar car);
//是司机就应该会驾驶汽车
public void drive();”
//车辆型号
public void setCar(ICar car);
//是司机就应该会驾驶汽车
public void drive();
}
public class Driver implements IDriver{
private ICar car;
public void setCar(ICar car){
this.car = car;
}
//司机的主要职责就是驾驶汽车
public void drive(){
this.car.run();
}
}
// 3
public class Driver implements IDriver{
//司机的主要职责就是驾驶汽车
public void drive(ICar car){
car.run();
}
}
最佳实践
依赖倒置的本质就是通过抽象(接口或抽象类)使各个类或模块的实现彼此独立,互不影响,实现模块之间的松耦合。
需要遵循以下几个规则:
- 每个类尽量都有接口或抽象类,或者抽象类和接口两者都具备。因为有了抽象才能依赖倒置
- 变量的表面类型尽量是接口或抽象类。
- 任何类都不应该从具体类派生。维护项目的工作可以不考虑这个规则,扩展开发修复行为可以通过一个继承关系,覆写一个方法就可以修正bug,不必继承最高的基类。
- 尽量不要覆写基类的方法。类间依赖的是抽象,覆写了抽象方法,对依赖的稳定性会产生一定的影响
- 接口负责定义public属性和方法,且声明与其他对象的依赖关系,抽象类负责公共构造部分的实现,实现类准确实现业务逻辑,在适当的时候对父类进行细化。
- 面向接口编程就是依赖倒置的核心
接口隔离原则
接口分为两种:
实例接口:java类也是一种接口,如Person xiaoLi = new Person(); 这个实例遵守的标准就是Person类
类接口:Java中使用interface关键字定义接口
什么是隔离?
- 客户端不应该依赖它不需要的接口
- 类间的依赖关系应该建立在最小的接口上
即:建立尽可能细化、接口中的方法尽量少的接口
迪米特法则
核心观念就是类间解耦,弱耦合,只有弱耦合以后,类的复用率才能提高。其要求的结果就是产生了大量的中转或跳转类,导致系统的复杂性提高,为维护带来了难度。使用时需要反复权衡,既做到结构清晰,又要做到高内聚低耦合。
开闭原则
对扩展开放,对修改关闭
注意 我们把价格定义为int类型并不是错误,在非金融类项目中对货币处理时,一般取2位精度,通常的设计方法是在运算过程中扩大100倍,在需要展示时再缩小100倍,减少精度带来的误差。
- 开闭原则有利于单元测试
所有已经投产的代码都有意义,并且受到系统规则的约束,这样的代码不仅逻辑正确,且能保证苛刻条件下不产生有毒代码。因此在变化提出时,需要尽可能不修改原有的代码,仅仅通过扩展实现变化。否则,就需要把原有的测试过程回笼一遍。
所以我们通过扩展实现业务逻辑的变化,而不是修改。单元测试只需要保证新增的方法正确就行。
- 提高复用性
缩小逻辑粒度,直到一个逻辑不可再拆分为止。
- 提高可维护性
只要扩展一个类,而非修改一个类,是最容易的。如果要读懂原有的复杂代码,再修改容易出问题。
- 面向对象开发的要求
万物皆对象,万物皆运动,有运动就有变化,需要我们在设计之初就考虑到所有可能的变化因素,留下接口,等待“可能”变成“现实”。
如何使用开闭原则?
抽象约束
通过接口或抽象类可以约束一组可能变化的行为,并且能够实现对扩展开放,其包含三层含义:
- 通过接口或抽象类约束扩展,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的public方法;
- 参数类型、引用对象尽量使用接口或抽象类,而不是实现类;
- 抽象层尽量保持稳定,一旦确定不允许修改。
增加一个接口IComputerBook和实现类ComputerBook,而BookStore不用任何修改就可以完成书店销售计算机书籍的业务。
首先,ComputerBook类必须实现IBook的3个方法,是通过IComputerBook接口传递进来的约束,即IBook接口对扩展类产生了约束力,正是由于该约束力,BookStore业务类才不需要大量修改
public class BookStore {
private final static ArrayList<IBook> bookList = new ArrayList<IBook>();
//static静态模块初始化数据,实际项目中一般是由持久层完成
static{
bookList.add(new NovelBook("天龙八部",3200,"金庸"));
bookList.add(new NovelBook("巴黎圣母院",5600,"雨果"));
bookList.add(new NovelBook("悲惨世界",3500,"雨果"));
bookList.add(new NovelBook("金瓶梅",4300,"兰陵笑笑生"));
//增加计算机书籍
bookList.add(new ComputerBook("Think in Java",4300,"Bruce Eckel","编程语言"));
}
//模拟书店卖书
public static void main(String[] args) {
NumberFormat formatter = NumberFormat.getCurrencyInstance();
formatter.setMaximumFractionDigits(2);
System.out.println("-----------书店卖出去的书籍记录如下:-----------");
for(IBook book:bookList){
System.out.println("书籍名称:" + book.getName()+"\t书籍作者:" + book.getAuthor()+ "\t书籍价格:" + formatter.format (book.getPrice()/100.0)+"元");
}
}
}
其次,如果原有的程序设计采用的不是接口,而是实现类,如下所示:
private final static ArrayList<NovelBook> bookList = new ArrayList<NovelBook>();
一旦这样设计就无法扩展,需要修改原有的业务逻辑(即main方法),这样的扩展就是形同虚设。
最后,如果我们在IBook类增加一个方法getScope,是否可以?不可以!因为原有的实现类NovelBook已经投产运行,它不需要该方法,而且接口是与其他模块交流的契约,修改契约就等于让其他模块修改。因此,接口或抽象类一旦定义,就应该立即执行,不能有改接口的思想。
所以,要实现对扩展开放,首要的前提是抽象约束
元数据控制模块行为
什么是元数据?用来描述环境和数据的数据,通俗地说就是配置参数,参数可以从文件中获得,也可以从数据库中获得。如login方法检查IP地址是否在可以直接访问系统的列表,然后再决定是否需要到数据库中验证密码;又如Spring容器使用的控制反转,在SpringContext配置文件中,通过扩展一个子类,修改配置文件,完成业务变化。
制定项目章程
章程中指定了所有人员都必须遵守的约定,对项目来说,约定优于配置。
封装变化
将相同的变化封装到一个接口或抽象类中
将不同的变化封装到不同的接口或抽象类中,不应该让两个不同的变化出现在同一个接口或抽象类中。
封装变化也就是受保护的变化,封装可能发生的变化。23个设计模式就是从不同的角度对变化进行封装。
设计模式
单例模式(Singleton Pattern)
定义
确保某一个类只有一个实例,且自行实例化并向整个系统提供这个实例
/Users/cooperniu/Downloads/NoteBook/2022年/设计模式.assets/截屏2022-03-14\ 下午4.03.48.png

单例类的通用代码:
public class Singleton {
private static final Singleton singleton = new Singleton();
//限制产生多个对象
private Singleton(){
}
//通过该方法获得实例对象
public static Singleton getSingleton(){
return singleton;
}
//类中其他方法,尽量是static
public static void doSomething(){
}
}
优点
- 在内存中只有一个实例,减少内存开销,特别是当一个对象需要频繁创建、销毁时,而且创建或销毁时性能又无法优化,效果明显。
- 当一个对象的产生需要较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在启动应用时直接产生一个单例对象,然后用永久驻留内存的方式来解决。(Java EE中注意垃圾回收机制)
缺点
- 没有接口,扩展困难,若扩展,只能改代码。那为什么不能增加接口?因为接口对单例模式没有任何意义,它要求“自行实例化”,且提供单一实例、接口或抽象类是不可能被实例化的。特殊情况下,单例模式可以实现接口、被继承。
- 不利于测试。在并行开发环境中,如果单例模式没有完成,无法测试,没有接口也不能使用mock方法虚拟一个对象
使用场景
- 生成唯一序列号的环境
- 在整个项目中需要一个共享访问点或共享数据
- 创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源
- 需要大量的静态常量和方法(工具类)的环境,可以单例也可以static
注意事项
- 高并发时,注意线程同步问题,上面的代码线程安全,下面的代码线程不安全。如一个线程A执行到singleton = new Singleton(),但还没有获得对象(对象初始化是需要时间的),第二个线程B也在执行,执行到(singleton == null)判断,那么线程B获得判断条件也是为真,于是继续运行下去,线程A获得了一个对象,线程B也获得了一个对象,在内存中就出现两个对象!
public class Singleton {
private static Singleton singleton = null;
//限制产生多个对象
private Singleton(){
}
//通过该方法获得实例对象
public static Singleton getSingleton(){
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
工厂模式
定义一个用于创建对象的接口,让子类决定实例化哪个类。工厂方法使一个类的实例化延迟到其子类。
在工厂方法模式中,抽象产品类Product负责定义产品的共性,实现对事物最抽象的定义;Creator为抽象创建类,即抽象工厂,具体如何创建产品类是由具体的实现工厂ConcreteCreator完成。
// 抽象产品类
public abstract class Product {
//产品类的公共方法
public void method1(){
//业务逻辑处理
}
//抽象方法
public abstract void method2();
}
// 具体产品类
public class ConcreteProduct1 extends Product {
public void method2() {
//业务逻辑处理
}
}
public class ConcreteProduct2 extends Product {
public void method2() {
//业务逻辑处理
}
}
// 抽象工厂类
public abstract class Creator {
/*
* 创建一个产品对象,其输入参数类型可以自行设置
* 通常为String、Enum、Class等,当然也可以为空
*/
public abstract <T extends Product> T createProduct(Class<T> c);
}
// 具体工厂类
public class ConcreteCreator extends Creator {
public <T extends Product> T createProduct(Class<T> c){
Product product=null;
try {
product = (Product)Class.forName(c.getName()).newInstance();
} catch (Exception e) {
//异常处理
}
return (T)product;
}
}
// 场景类
public class Client {
public static void main(String[] args) {
Creator creator = new ConcreteCreator();
Product product = creator.createProduct(ConcreteProduct1.class);
/*
* 继续业务处理
*/
}
}
优点
- 良好的封装性,代码结构清晰。一个对象创建有条件约束,如果一个调用者需要一个具体的产品对象,只要知道这个产品的类名就可以了,不用知道创建对象的艰辛过程,降低模块间的耦合
- 扩展性优秀。在增加产品类的情况下,只要适当地修改具体的工厂类或者扩展工厂类就可以拥抱变化。
- 屏蔽产品类。产品类如何变化,调用者不需要关心,只需要关心产品的接口,只要接口保持不变,系统中的上层模块就不要发生变化。
- 典型的解耦框架。高层模块需要知道产品的抽象类,其他实现类不需要关心,符合迪米特法则,不需要的就不要交流;符合依赖倒置原则,只依赖产品类的抽象;符合里式替换原则,使用产品子类替换产品父类,没问题。
使用场景
首先,工厂方法模式是new一个对象的替代品,所以在所有需要生成对象的地方都可以使用,但是需要慎重地考虑是否要增加一个工厂类进行管理,增加代码的复杂度。
其次,需要灵活的、可扩展的框架时,可以考虑采用工厂方法模式。万物皆对象,那万物也就皆产品类,例如需要设计一个连接邮件服务器的框架,有三种网络协议可供选择:POP3、IMAP、HTTP,我们就可以把这三种连接方法作为产品类,定义一个接口如IConnectMail,然后定义对邮件的操作方法,用不同的方法实现三个具体的产品类(也就是连接方式)再定义一个工厂方法,按照不同的传入条件,选择不同的连接方式。如此设计,可以做到完美的扩展,如某些邮件服务器提供了WebService接口,很好,我们只要增加一个产品类就可以了。
再次,工厂方法模式可以用在异构项目中,例如通过WebService与一个非Java的项目交互,“虽然WebService号称是可以做到异构系统的同构化,但是在实际的开发中,还是会碰到很多问题,如类型问题、WSDL文件的支持问题,等等。从WSDL中产生的对象都认为是一个产品,然后由一个具体的工厂类进行管理,减少与外围系统的耦合。
最后,可以使用在测试驱动开发的框架下。例如,测试一个类A,就需要把与类A有关联关系的类B也同时产生出来,我们可以使用工厂方法模式把类B虚拟出来,避免类A与类B的耦合。目前由于JMock和EasyMock的诞生,该使用场景已经弱化了,读者可以在遇到此种情况时直接考虑使用JMock或EasyMock。
扩展
1.缩小为简单工厂模式
一个模块只需要一个工厂类,没有必要把它生产出来,使用静态方法就可以了。

// 简单工厂模式中的工厂类
public class HumanFactory {
public static <T extends Human> T createHuman(Class<T> c){
//定义一个生产出的人种
Human human=null;
try {
//产生一个人种
human = (Human)Class.forName(c.getName()).newInstance();
} catch (Exception e) {
System.out.println("人种生成错误!");
}
return (T)human;
}
}
// 简单工厂模式中的场景类.HumanFactory类仅有两个地方发生变化:去掉继承抽象类,并在createHuman前增加static关键字;工厂类发生变化,也同时引起了调用者NvWa的变化,
public class NvWa {
public static void main(String[] args) {
//女娲第一次造人,火候不足,于是白色人种产生了
System.out.println("--造出的第一批人是白色人种--");
Human whiteHuman = HumanFactory.createHuman(WhiteHuman.class);
whiteHuman.getColor();
whiteHuman.talk();
//女娲第二次造人,火候过足,于是黑色人种产生了
System.out.println("\n--造出的第二批人是黑色人种--");
Human blackHuman = HumanFactory.createHuman(BlackHuman.class);
blackHuman.getColor();
blackHuman.talk();
//第三次造人,火候刚刚好,于是黄色人种产生了
System.out.println("\n--造出的第三批人是黄色人种--");
Human yellowHuman = HumanFactory.createHuman(YellowHuman.class;
yellowHuman.getColor();
yellowHuman.talk();
}
}
缺点:不容易扩展
2.升级为多个工厂类
当我们在做一个比较复杂的项目时,经常会遇到初始化一个对象很费精力,所有的产品类都放在一个工厂方法进行初始化会使代码不清晰。我们可以为每个产品定义一个创造者,然后由调用者自己去选择与哪个工厂方法关联。
在复杂的应用中一般采用多工厂的方法,然后再增加一个协调类,避免调用者与各个子工厂交流,协调类封装子工厂类,对高层模块提供统一的访问接口。
3.替代单例模式
Singleton定义了一个private的无参构造函数,不允许通过new的方式创建一个对象。那如何建立一个单例对象?通过反射!!
// 负责生成单例的工厂类
public class SingletonFactory {
private static Singleton singleton;
static{
try {
Class cl= Class.forName(Singleton.class.getName());
//获得无参构造
Constructor constructor=cl.getDeclaredConstructor();
//设置无参构造是可访问的
constructor.setAccessible(true);
//产生一个实例对象
singleton = (Singleton)constructor.newInstance();
} catch (Exception e) {
//异常处理
}
}
public static Singleton getSingleton(){
return singleton;
}
}
通过获得类构造器,然后设置访问权限,生成一个对象,然后提供外部访问,保证内存中的对象唯一。当然,其他类也可以通过反射的方式建立一个单例对象,确实如此,但是一个项目或团队是有章程和规范的,何况已经提供了一个获得单例对象的方法,为什么还要重新创建一个新对象呢?除非是有人作恶。
以上通过工厂方法模式创建了一个单例对象,该框架可以继续扩展,在一个项目中可以产生一个单例构造器,所有需要产生单例的类都遵循一定的规则(构造方法是private),然后通过扩展该框架,只要输入一个类型就可以获得唯一的一个实例。
4.延迟初始化
一个对象被消费完毕后,不立即释放,工厂类保持其初始状态,等待其再次被使用。

ProductFactory负责产品类对象的创建,通过prMap变量产生一个缓存,对需要再次被重用的对象保留。如果在Map中已经有的对象,直接取出返回;如果没有,则根据需要的类型产生一个对象并放入Map中,以方便下一次调用。
// 延迟初始化的工厂类
public class ProductFactory {
private static final Map<String,Product> prMap = new HashMap();
public static synchronized Product createProduct(String type) throws Exception{
Product product =null;
//如果Map中已经有这个对象
if(prMap.containsKey(type)){
product = prMap.get(type);
}else{
if(type.equals("Product1")){
product = new ConcreteProduct1();
}else{
product = new ConcreteProduct2();
}
//同时把对象放到缓存容器中
prMap.put(type,product);
}
return product;
}
}
延迟加载框架是可以扩展的。如限制某一个产品类的最大实例化数量,可以通过判断Map中的对象数量来实现。如JDBC连接数据库,都要求设置一个MaxConnections最大连接数,即内存中最大实例化的数量。
还可以用在对象初始化比较复杂的情况下,如硬件访问,设计多方面的交互,则可以通过延迟加载降低对象的产生和销毁带来的复杂性。
抽象工厂模式
定义:为创建一组相关或相互依赖的对象提供一个接口,而且无须指定它们的具体类
// 场景类
// 在场景类中,没有任何一个方法与实现类有关系,对于一个产品来说,我们只要知道它的工厂方法就可以直接产生一个产品对象,无须关心它的实现类
public class Client{
public static void main(String[] args){
AbstractCreator creator1 = new Creator1();
AbstractCreator creator2 = new Creator2();
AbstractProductA a1 = creator1.createProductA();
AbstractProductA a2 = creator2.createProductA();
AbstractProductB b1 = creator1.createProductB();
AbstractProductB b2 = creator2.createProductB();
}
}
优点
封装性
产品族内的约束为非公开状态。例如男女比例问题,抽象工厂里可以有一个约束,每生产一个女性,就同时生产1.2个男性,这样的生产过程对于调用工厂类的高层模块透明,它不需要知道这个约束。具体约束在工厂内实现
缺点
产品族扩展非常困难
使用场景
一个对象族都有相同的约束,则可以使用抽象工厂模式
抽象工厂模式是一个简单的模式,使用的场景非常多,大家在软件产品开发过程中,涉及不同操作系统的时候,都可以考虑使用抽象工厂模式,例如一个应用,需要在三个不同平台(Windows、Linux、Android)上运行,你会怎么设计?分别设计三套不同的应用?非也,通过抽象工厂模式屏蔽掉操作系统对应用的影响。三个不同操作系统上的软件功能、应用逻辑、UI都应该是非常类似的,唯一不同的是调用不同的工厂方法,由不同的产品类去处理与操作系统交互的信息。
有N个产品族,在抽象工厂类中就应该有N个创建方法。有M个产品等级就应该有M个实现工厂类,在每个实现工厂中,实现不同产品族的生产任务。
模板方法模式
定义
定义一个操作中的算法的框架,而将一些步骤延迟到子类中实现。使得子类可以不改变一个算法的结构即可重定义该算法的特定步骤。
AbstractClass为抽象模板,方法有两类:
- 基本方法:由子类实现,在模板方法被调用,尽量设计为protected类型,符合迪米特法则。
- 模板方法:一般是一个具体方法,即框架,实现对基本方法的调度,完成固定的逻辑。为防止恶意操作,一般模板方法都加上final关键字,不允许被覆写
具体模板实现父类所定义的一个或多个抽象方法。
优点
- 封装不变部分,扩展可变部分;
- 提取公共部分代码,便于维护;
- 行为由父类控制,子类实现;子类可以通过扩展的方式增加相应的功能,符合开闭原则
缺点
- 抽象类定义了部分抽象方法,由子类实现,子类执行结果影响了父类的结果,即子类对父类产生了影响。
使用场景
- 多个子类有共有的方法,且逻辑基本相同
- 重要、复杂的算法,可以把核心算法设计为模板方法,周报的相关细节功能则由各个子类实现
- 重构时,模板方法模式是一个常用的模式,把相同的代码抽取到父类中,然后通过钩子函数约束其行为
本文由博客一文多发平台 OpenWrite 发布!
本文详细介绍了面向对象设计的六大原则:单一职责原则、里式替换原则、依赖倒置原则、接口隔离原则、迪米特法则和开闭原则。这些原则有助于实现软件的高内聚、低耦合,提高代码的可维护性和复用性。通过实例解释了如何在实际项目中应用这些原则,包括接口设计、类的继承与扩展、依赖管理等方面,强调了通过抽象和解耦来适应变化的重要性。
409

被折叠的 条评论
为什么被折叠?



