您当前的位置: 首页 >  Java

梁云亮

暂无认证

  • 2浏览

    0关注

    1211博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

【精品】 Java中类设计原则

梁云亮 发布时间:2022-03-03 13:31:41 ,浏览量:2

1.单一职责原则(Single Responsibility Principle,简称SRP)

单一职责原则定义是:不要存在多于一个导致类变更的原因。通俗地说,即一个类只负责一项职责单一职责原则。

类T负责两个不同的职责:职责P1,职责P2。当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障或者难以维护,这就违背了单一职责

单一职责原则让每个类都只做一件事,减低了类的复杂性。但是如果严格遵守单一职责原则,又会导致类的数目大增,反而增加了整体的复杂性。这就是单一职责原则的争议性。所以在实践中,很少会看到严格遵守单一职责原则的代码。

一个类只负责一件事儿,一个方法只负责一件事儿,写了太多的分支判断,去执行各自的业务逻辑时候,就容易出现职责不单一,所以只能拆分解决职责不单一的问题。

优点:

  • 降低类的复杂度
  • 提高类的可读性,因为类的职能单一,看起来比较有目的性,显得简单
  • 提高系统的可维护性,降低变更程序引起的风险
2.接口隔离原则(Interface Segregation Principle, ISP)

客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上。

接口隔离原则的要点,就是要细化接口。那么这样做主要有四个好处,分别是:

  1. 避免接口污染;
  2. 提高灵活性;
  3. 提供定制服务;
  4. 实现高内聚。

下面就来详细说一下接口隔离原则的好处。

2.1 避免接口污染

一个类如果要实现一个接口,那么就要实现这个接口要求的所有方法,如果这个接口里面包含这个类不需要的方法,那么就会造成接口污染,这是不好的设计,会对系统留下隐患。 比如说我们有一个枪的接口,枪有两个属性:扳机和子弹。枪有一个功能:射杀。其接口如下:

public interface Gun {
	String trigger = "扳机";
	String bullet = "子弹";

	public void shot();
}

然后我们现在需要一个玩具枪的类。玩具枪有扳机也有子弹,只是不能射杀。为了图方便,我们直接用IGun这个接口来实现我们的玩具枪:

public class ToyGun implements Gun{
	@Override
	public void shot() {
		// TODO Auto-generated method stub
	}
}

玩具枪是不能射杀的,但是由于它实现了Gun接口,所以只能空实现它并不需要的shot方法,于是玩具枪这个类就被污染了。这好像也没有什么不妥,但是这是有隐患的,因为玩具枪一旦实现了Gun接口,那么在程序里它就代表一把能射杀的枪。假设在后面突然遇到了一个老虎,唯一保命的方法就是拿枪射杀这个老虎,结果你拿到的是你之前为了图方便做的ToyGun,那么你面临的就是灭顶之灾。

2.2 提高灵活性

一个类是可以同时实现多个接口的,所以将一个臃肿的接口分割为若干个小接口,通过小接口的不同组合可以满足更多的需求。

举个例子。我们现在需要一个代表美女的接口,美女的标准也很明确:面貌、身材和气质,那么我们的美女接口就出来了:

public interface BeautifulGirl{
	/**
	 * 好面貌
	 */
	private String face = "good";
	/**
	 * 好身材
	 */
	private String figure = "nice";
	/**
	 * 好气质
	 */
	private String temperament = "great";
}

这并没有什么问题。但是在现实中,一定是要美貌和气质兼备的才算美女吗?非也,其实也有长得不好看,但是气质很好的气质美女的,当然也有没有气质但是长得好看的美女。这样上面的接口就不适用了,因为按照上面的的接口,只有长得好看而且气质好的才算美女。 可以通过细化这个接口解决这个问题。上述的接口可以一分为三:

public interface FaceGirl{
	/**
	 * 好面貌
	 */
	private String face = "good";
}
public interface FigureGirl{
	/**
	 * 好身材
	 */
	private String figure = "nice";
}
public interface temperamentGirl{
	/**
	 * 好气质
	 */
	private String temperament = "great";
}

然后通过这三个接口的不同组合,就能满足外貌美女、气质美女和外貌气质俱佳的美女的不同需求了。所以,细化接口可以让我们的接口更加灵活,满足更多需求。

//外貌+身材养女
public class AA implements FaceGirl,FigureGirl{
	
}
//气质美女
public class BB implements TemperamentGirl{
	
}
2.3 提供定制服务

比如我们开发了一个图书管理系统,其中有一个查询图书的接口:

public interface BookDao {
	/**
	 * 根据作者搜索
	 * 
	 * @return
	 */
	public List searchByAuthor();

	/**
	 * 根据书名搜索
	 * 
	 * @return
	 */
	public List searchByTitle();

	/**
	 * 根据分类搜索
	 * 
	 * @return
	 */
	public List searchByCatagory();

	/**
	 * 复杂的搜索
	 * 
	 * @return
	 */
	public List complexSearch();
}

我们的图书馆管理系统的访问者有管理人员和公网,其中complexSearch方法非常损耗服务器的性能,它只提供给管理人员使用。其他方法管理人员和公网都可以使用。公网这部分是另一个项目组在开发的,所以当时我们口头上跟公网项目组说明不能在公网上调用complexSearch这个方法。图书馆管理系统上线后,有一天发现系统速度非常慢,在熬了一个通宵排查后,发现是由于公网项目组某个程序员的疏忽,把complexSearch方法公布到了公网中… 显然通过口头的方式说哪一个方法不能调用是不管用的,要想彻底解决这个问题,还是得通过细化接口,为访问者定制专有的接口才行。那么上述的接口可以一分为二:

  • 简单的搜索:
public interface BookDaoSimple {
	/**
	 * 根据作者搜索
	 * 
	 * @return
	 */
	public List searchByAuthor();

	/**
	 * 根据书名搜索
	 * 
	 * @return
	 */
	public List searchByTitle();

	/**
	 * 根据分类搜索
	 * 
	 * @return
	 */
	public List searchByCatagory();
}
  • 复杂的搜索:
public interface BookDaoComplex {
	/**
	 * 复杂的搜索
	 * 
	 * @return
	 */
	public List complexSearch();
}

这样我们就可以分别给管理人员和公网定制接口了:

给管理人员提供BookDaoSimple 和BookDaoComplex 两个接口; 给外网提供BookDaoSimple 这个接口。

2.4 实现高内聚

高内聚就是提高接口、类、模块的处理能力,减少对外的交互。比如说,你告诉你的下属“一个小时之内去月球搬一块石头回来”,然后你就躺在海滩上晒着太阳喝着果汁,一个小时之后你的下属就搬着一块月亮上的石头回来给你了。这种不讲任何条件,不需要你关心任何细节,立即完成任务的行为就是高内聚的表现。

具体到接口中,还是尽量细化你的接口。接口是对外界的承诺,承诺越少对系统的开发越有利,变更的风险也就越少,同时也有利于降低成本。

3.依赖倒置原则

依赖倒置原则也可以理解为“依赖抽象原则”。之所以将其称之为依赖倒置是因为在日常生活中,人们习惯于依赖于具体事务(细节),而不是抽象。比如说我们说开车就是具体的车,用电脑就是用具体的电脑。那么如果要倒过来去依赖抽象,就是依赖倒置。 很简单,我们的任务是声明一个司机类和一个奔驰车类,然后让司机开车。我们按照我们现实生活的直觉来,两个类都是具体类,司机就是司机,奔驰车就是奔驰车,没有抽象类的存在。 具体实现参考博客:工厂方法设计模式

通过上面的例子,相信大家已经领略到在代码中使用依赖倒置原则的重要性了。总结一下依赖倒置原则的优点:

  • 减少类之间的耦合;
  • 降低并行开发引起的风险;
  • 提高代码的可读性和可维护性。
  • 面向接口编程是依赖倒置原则的最佳实践

增强了耦合性。当父类的常量,变量和方法被修改时,需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果------大段代码需要重构。

4. 里氏替代原则

任何使用基类的地方,都可以透明的使用其子类 继承:子类拥有基类的一切属性和行为,任何基类出现的地方都可以用子类代替。 在创建对象的时候尽量使用父类做变量的声明;为了能够更加灵活。

假设我们有A和B两个条件:

  • A条件:在用类T的对象o1定义的程序P中,将o1全部替换为类S的对象o2,而程序P的行为没有发生变化;
  • B条件:类S是类T的子类。

根据里氏替换原则的第一个定义,A条件可以推导出B条件;根据里氏替换原则的第二个定义,B条件可以推导出A条件。所以,里氏替换原则其实就是:A条件和B条件互为充要条件。

在使用继承时,遵循里氏替换原则,在子类中尽量不要重写父类的方法。继承实际上让两个类耦合性增强了,如果必须重写,可以通过聚合,组合**,依赖**,重新写一个父类让两个成兄弟来解决问题。.

意义:继承,是面向对象语言非常优秀的语言机制。里氏替换原则的意义,就是规范继承的用法,让我们最大限度地发挥继承的优点。

5. 开闭原则

对扩展开放(提供方),对修改关闭(使用方)。其含义是说,一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化。

开闭原则是面向对象设计的终极目标,它使软件实体拥有一定的适应性和灵活性的同时具备稳定性和延续性。具体来说,其作用如下: 1.对软件测试的影响 软件遵守开闭原则的话,软件测试时只需要对扩展的代码进行测试就可以了,因为原有的测试代码仍能够正常运行 2.可以提高代码的可复用性 粒度越小,被复用的可能性就越大;在面向对象的程序设计中,根据原子和抽象编程可以提高代码的可复用性。 3.提高软件的可维护性 遵守开闭原则的软件,其稳定性高和延续性强,从而易于维护。

开闭原则的使用:

  1. 抽象约束 1.通过接口或抽象类约束扩展,对扩展进行边界限定, 不允许出现在接口或抽象类中不存在的public方法 2.参数类型,引用对象(调用对象具体方法)尽量使用接口或者抽象类,而不是实现类 3.抽象层尽量保持稳定,一旦确定不允许修改接口或抽象类一旦定义,应立即执行,不能有修改接口的想法,除非是彻底的大返工
  2. 元数据控制模块行为 元数据:用来描述环境和数据的数据,通俗的说就是配置参数 通过扩展一个子类,修改配置文件,完成了业务的变化,也是框架的好处
  3. 制定项目章程
  4. 封装变化

对变化的封装包含两层含义:

  1. 将相同的变化封装到一个接口或抽象类中
  2. 将不同的变化封装到不同的接口或抽象类中
6. 迪米特法则(Law of Demeter, LoD)

迪米特法则要求一个对象应该对其他对象有最少的了解,所以迪米特法则又叫做最少知识原则(Least Knowledge Principle, LKP)。

迪米特法则还有个更简单的定义:只与直接的朋友通信。直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖,关联,组合,聚合等。其中,我们称出现成员变量,方法参数,方法返回值中的类为直接的朋友,而出现在局部变量中的类不是直接的朋友。也就是说,陌生的类最好不要以局部变量的形式出现在类的内部。

例如:手机,app和书籍,手机里面可以有app,打开app才有书籍,所以书籍不能出现在手机中,只能在app中,要看书籍必须要有app对象然后调用相应方法读书。

7.合成复用原则

合成复用原则是尽量使用合成/聚合复用,而不是使用继承复用。

继承的优点:

  • 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性
  • 提高代码的重用性
  • 子类可以形似父类,但又异于父类
  • 提高代码的可扩展性,实现父类的方法就可以”为所欲为”,很多开源框架的扩展接口都是通过继承父类完成。

继承的缺点:

  • 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
  • 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
  • 继承限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。
  • 继承是侵入性的。只要继承,就必须拥有父类的属性和方法,如果出现子类不应该有的东西,那就需要断掉继承;(可以再来一个父类,在这个父类里面只包含应该有的东西)
  • 降低代码的灵活性。子类必须拥有父类的属性和方法,让子类自由的世界中多了些约束
  • 子类出现的地方,父类不一定能够代替。

组合或聚合复用优点:

  • 它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
  • 新旧类之间的耦合度低。这种复用所需的依赖较少,新对象存取成分对象的唯一方法是通过成分对象的接口。
  • 复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。
关注
打赏
1665409997
查看更多评论
立即登录/注册

微信扫码登录

0.0412s