在软件设计中,只有尽量降低各个模块之间的耦合度,才能提高系统可维护性、可扩展性和代码的可复用率。
在软件设计中,前人根据经验,总结出23种经典的设计模式,比如:单例模式、代理模式,建造器模式……
但这23种设计模式又是怎么来的呢?它们又要遵循什么原则呢?这就是下文要说的七种设计原则。先简单介绍一下,七大设计原则:
单一职责原则:专注降低类的复杂度,实现类要职责单一
依赖倒置原则:系统抽象化的具体实现,要求面向接口编程,是面向对象设计的主要实现机制之一
接口隔离原则:要求接口的方法尽量少,接口尽量细化
开放关闭原则:所有面向对象原则的核心,设计要对扩展开发,对修改关闭
里式替换原则:实现开放关闭原则的重要方式之一,设计不要破坏继承关系
迪米特法则:降低系统的耦合度,使一个模块的修改尽量少的影响其他模块,扩展会相对容易
组合复用原则:在软件设计中,尽量使用组合/聚合而不是继承达到代码复用的目的
二 设计模式七大原则 1. 单一职责原则 SRP单一职责原则 SRP(Single Responsibility Principle)
(1)是什么?就一个类而言,应该仅有一个引起它变化的原因,通俗的说,就是一个类只负责一项职责(可以到方法层,即一个方法只做一个功能)。此原则的核心就是解耦和增强内聚性。
如类 A 负责两个不同职责:职责 1,职责 2。当职责 1 需求变更而改变 A 时,可能造成职责 2 执行错误,所以需要将类 A 的粒度分解为 A1,A2。
注意:一项职责 != 只有一个方法
(2)为什么要使用?如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力,这样就会增加类的耦合性,导致这个类会非常臃肿。
(3)优点- 降低类的复杂度
- 提高类的可读性,提高系统的可维护性
- 降低变更引起的风险(降低对其他功能的影响)
接口隔离原则 ISP (Interface Segregation Principle)
(1)是什么?接口隔离原则讲的是:使用多个专门的接口比使用单一的接口要好。换句话来说,就是从一个客户类的角度来说,一个类对另外一个类的依赖性应当是建立在最小的接口上的。
(2)示例这个接口隔离原则,举个例子,就会一目了然
// 接口中有5个方法,如果某个类要实现此接口,就得一口气全部实现
public interface TestInterface {
public void method1();
public void method2();
public void method3();
public void method4();
public void method5();
}
// 测试类
class Test1 {
public void mm1(TestInterface i) {
i.method1();
}
public void mm2(TestInterface i) {
i.method2();
}
public void mm3(TestInterface i) {
i.method3();
}
}
//实现上述接口,但该类只使用到其中的三个方法
class Test2 implements TestInterface{
@Override
public void method1() {
System.out.println("类Test2实现接口TestInterface的方法1");
}
@Override
public void method2() {
System.out.println("类Test2实现接口TestInterface的方法2");
}
@Override
public void method3() {
System.out.println("类Test2实现接口TestInterface的方法3");
}
@Override
public void method4() {}
@Override
public void method5() {}
}
class Test3{
public void mm1(TestInterface i) {
i.method1();
}
public void mm2(TestInterface i) {
i.method4();
}
public void mm3(TestInterface i) {
i.method5();
}
}
//该类只需要其中的三个方法
class Test4 implements TestInterface{
@Override
public void method1() {
System.out.println("类Test4实现接口TestInterface的方法1");
}
@Override
public void method2() { }
@Override
public void method3() { }
@Override
public void method4() {
System.out.println("类Test4实现接口TestInterface的方法4");
}
@Override
public void method5() {
System.out.println("类Test4实现接口TestInterface的方法5");
}
}
//测试类,我们看一下调用方式
public class Client {
public static void main(String[] args) {
Test1 test1 = new Test1();
test1.mm1(new Test2());
test1.mm2(new Test2());
test1.mm3(new Test2());
Test3 test3 = new Test3();
test3.mm1(new Test4());
test3.mm2(new Test4());
test3.mm3(new Test4());
}
}
// 输出结果
类Test2实现接口TestInterface的方法1
类Test2实现接口TestInterface的方法2
类Test2实现接口TestInterface的方法3
类Test4实现接口TestInterface的方法1
类Test4实现接口TestInterface的方法4
类Test4实现接口TestInterface的方法5
很显然,这不遵循接口设计原则,因为用不到的接口,也要去实现。那么我们怎么去设计遵循接口设计原则的代码呢?
public interface TestInterface1 {
public void method1();
}
interface TestInterface2{
public void method2();
public void method3();
}
interface TestInterface3 {
public void method4();
public void method5();
}
class Test1{
public void mm1(TestInterface1 i){
i.method1();
}
public void mm2(TestInterface2 i){
i.method2();
}
public void mm3(TestInterface2 i){
i.method3();
}
}
class Test2 implements TestInterface1,TestInterface2{
@Override
public void method1() {
System.out.println("类Test2实现接口TestInterface1的方法1");
}
@Override
public void method2() {
System.out.println("类Test2实现接口TestInterface2的方法2");
}
@Override
public void method3() {
System.out.println("类Test2实现接口TestInterface2的方法3");
}
}
class Test3{
public void mm1(TestInterface1 i){
i.method1();
}
public void mm2(TestInterface3 i){
i.method4();
}
public void mm3(TestInterface3 i){
i.method5();
}
}
class Test4 implements TestInterface1,TestInterface3{
@Override
public void method1() {
System.out.println("类Test4实现接口TestInterface1的方法1");
}
@Override
public void method4() {
System.out.println("类Test4实现接口TestInterface3的方法4");
}
@Override
public void method5() {
System.out.println("类Test4实现接口TestInterface3的方法5");
}
}
再次理解一下接口隔离原则:
建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。
也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。但其实接口隔离原则也算是“看人下菜碟”,要根据客户不同的需求,去指定不同的服务,这就是接口隔离原则中推荐的方式。
注:在Java中,是可以实现多个接口的,但类的继承只能有一个,所有要慎用类的继承
3. 依赖倒转原则 DIP依赖倒转原则 DIP (Dependence Inversion Principle)
- 高层模块不应该依赖低层模块,二者都应该依赖其抽象 (抽象类/接口),不要去依赖一个具体的子类
- 抽象不应该依赖细节,细节应该依赖抽象,这个是开闭原则的基础
- 具体内容:针对接口编程,依赖于抽象而不依赖于具体
- 面向接口的编程,多用抽象的接口来描述相同的动作
- 依赖倒转原则的设计理念:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多,在 Java 中,抽象指的是接口或抽象类,细节就是具体的实现类
里氏代换原则 LSP (Liskov Substitution Principle),里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。即在一个程序中,在使用基类的地方,用其子类替换,程序应该继续能运行。
里氏代换原则 LSP 是继承复用的基石,只有当衍生类(子类)可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类(子类)也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
- 子类中可以增加自己特有的方法
- 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松
- 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格
一句话总结:尽量不要重写父类的已经实现了的方法,可以用接口等其他方法绕过
5. 开闭原则 OCP开闭原则 OCP (Open Close Principle),开闭原则就是说对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。
当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。(尽量增加一种功能/扩展,而不是修改,因为被修改的这部分可能正在被使用)
所以一句话概括就是:为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类,举个例子
//书店卖书
interface Books{
//书籍名称
public Sting getName();
//书籍价格
public int getPrice();
//书籍作者
public String getAuthor();
}
public class NovelBook implements Books {
private String name;
private int price;
private String author;
public NovelBook(String name, int price, String author) {
this.name = name;
this.price = price;
this.author = author;
}
@Override
public String getName() {
return name;
}
@Override
public int getPrice() {
return price;
}
@Override
public String getAuthor() {
return author;
}
}
以上的代码是数据的实现类和书籍的类别,下面我们将要开始对书籍进行一个售卖活动:
public class BookStore {
private final static ArrayList sBookList = new ArrayList();
static {
sBookList.add(new NovelBook("天龙八部", 4400, "金庸"));
sBookList.add(new NovelBook("射雕英雄传", 7600, "金庸"));
sBookList.add(new NovelBook("钢铁是怎么炼成的", 7500, "保尔·柯查金"));
sBookList.add(new NovelBook("红楼梦", 3300, "曹雪芹"));
}
public static void main(String[] args) throws IOException {
NumberFormat format = NumberFormat.getCurrencyInstance();
format.setMaximumFractionDigits(2);
System.out.println("----书店卖出去的书籍记录如下---");
for (Books book : sBookList) {
System.out.println("书籍名称:" + book.getName()
+ "\t书籍作者:" + book.getAuthor()
+ "\t书籍价格:" + format.format(book.getPrice() / 100.00) + "元");
}
}
}
运行结果如下:
----书店卖出去的书籍记录如下---
书籍名称:天龙八部 书籍作者:金庸 书籍价格:¥44.00元
书籍名称:射雕英雄传 书籍作者:金庸 书籍价格:¥76.00元
书籍名称:钢铁是怎么炼成的 书籍作者:保尔·柯查金 书籍价格:¥75.00元
书籍名称:红楼梦 书籍作者:曹雪芹 书籍价格:¥33.00元
但是,如果说现在书店卖书的时候要求打折出售,40以上的书要7折售卖,40以下的打8折,实现方法有三种:
-
第一种:修改接口。 在 Books 上新增加一个方法 getOnSalePrice(),专门进行打折,所有实现类实现这个方法。但是这样修改的后果就是实现类 NovelBook 要修改,BookStore 中的 main 方法也修改,同时 Books 作为接口应该是稳定且可靠的,不应该经常发生变化,否则接口做为契约的作用就失去了效能,其他不想打折的书籍也会因为实现了书籍的接口必须打折,因此该方案被否定。
-
第二种:修改实现类。 修改 NovelBook 类中的方法,直接在 getPrice() 中实现打折处理,这个应该是大家在项目中经常使用的就是这样办法,通过 class 文件替换的方式可以完成部分业务(或是缺陷修复)变化,但是该方法还是有缺陷的,例如采购书籍人员也是要看价格的,由于该方法已经实现了打折处理价格,因此采购人员看到的也是打折后的价格,这就产生了信息的蒙蔽效果,导致信息不对称而出现决策失误的情况。该方案也不是一个最优的方案。
-
第三种:增加子类。通过扩展实现变化增加一个子类 OffNovelBook,覆写 getPrice 方法,高层次的模块(也就是 static 静态模块区)通过 OffNovelBook 类产生新的对象,完成对业务变化开发任务。好办法,风险也小。
public class OnSaleBook extends NovelBook {
public OnSaleBook(String name, int price, String author) {
super(name, price, author);
}
@Override
public String getName() {
return super.getName();
}
@Override
public int getPrice() {
int OnsalePrice = super.getPrice();
int salePrce = 0;
if (OnsalePrice >4000){
salePrce = OnsalePrice * 70/100;
}else{
salePrce = OnsalePrice * 80/100;
}
return salePrce;
}
@Override
public String getAuthor() {
return super.getAuthor();
}
}
上面的代码是扩展出来的一个类,而不是在原来的类中进行的修改。
public class BookStore {
private final static ArrayList sBookList = new ArrayList();
static {
sBookList.add(new OnSaleBook("天龙八部", 4400, "金庸"));
sBookList.add(new OnSaleBook("射雕英雄传", 7600, "金庸"));
sBookList.add(new OnSaleBook("钢铁是怎么炼成的", 7500, "保尔·柯查金"));
sBookList.add(new OnSaleBook("红楼梦", 3300, "曹雪芹"));
}
public static void main(String[] args) throws IOException {
NumberFormat format = NumberFormat.getCurrencyInstance();
format.setMaximumFractionDigits(2);
System.out.println("----书店卖出去的书籍记录如下---");
for (Books book : sBookList) {
System.out.println("书籍名称:" + book.getName()
+ "\t书籍作者:" + book.getAuthor()
+ "\t书籍价格:" + format.format(book.getPrice() / 100.00) + "元");
}
}
}
结果展示:
----书店卖出去的书籍记录如下---
书籍名称:天龙八部 书籍作者:金庸 书籍价格:¥30.80元
书籍名称:射雕英雄传 书籍作者:金庸 书籍价格:¥53.20元
书籍名称:钢铁是怎么炼成的 书籍作者:保尔·柯查金 书籍价格:¥52.50元
书籍名称:红楼梦 书籍作者:曹雪芹 书籍价格:¥26.40元
在开闭原则中,抽象化是一个关键,解决问题的关键在于抽象化。在 Java 语言中,可以给出一个或者多个Java 抽象类或者是Java接口,规定所有的具体类必须提供方法特征作为系统设计的抽象层,这个抽象层会遇见所有的可能出现的扩展,因此,在任何扩展情况下都不回去改变,这就让系统的抽象层不需要修改,从而满足开闭原则的第二条,对修改进行闭合。
6. 迪米特法则迪米特法则 DP (Demeter Principle),迪米特法则又叫作最少知道原则(Least Knowledge Principle 简写LKP)。
为什么叫最少知道原则?
就是说:一个实体应当尽量少的与其他实体之间发生相互作用,一个对象应该对其他对象保持最少的了解,使得系统功能模块相对独立。 其实它主要是为了解决一个我们最常见的问题,就是类之间的关系,所以类与类之间的关系越密切,耦合度就越大,当一个类放生改变的时间,对另一个类的影响也会越大。
迪米特法则又叫最少知道原则,即一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类不管多么复杂,都尽量将逻辑封装在类的内部,对外除了提供的 public 方法,不对外泄露任何信息
迪米特法则还有个更简单的定义:只与直接的朋友通信
直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,如依赖,关联,组合,聚合等。其中,我们称出现在成员变量,方法参数,方法返回值中的类为直接的朋友,而出现在局部变量中的类不是直接的朋友(陌生的类)。注意,陌生的类最好不要以局部变量的形式出现在类的内部。
简单描述一下,三个类:Someone、Friend、Stranger(该类未给出)。Someone和Friend是朋友,和Stranger不是朋友,代码和依赖图如下。
public class Someone{
public void operation1(Friend friend){
Stranger stranger = friend.provide();
stranger.operation3();
}
}
public class Friend{
private Stranger stranger = new Stranger();
public void operation2(){
}
public Stranger provide(){
return stranger;
}
}
可以看出,Someone具有一个方法operation1(),这个方法接收Friend为参数,显然,根据朋友的定义,Friend和Stranger是朋友关系,其中的Friend的provide()方法会提供自己所创建的Stranger实例,这其实就很显然了,Someone的方法operation1()并不满足迪米特法则,为什么会这么说呢?因为这个方法引用Stranger对象,而Stranger对象不是Someone的朋友。
使用迪米特法则进行改造
public class Someone{
public void operation1(Friend friend){
friend.forward();
}
}
public class Frend{
private Stranger stranger = new Stranger();
public void operation2(){
System.out.printIn("In Friend.operation2()");
}
public void forward(){
stranger.operation3();
}
}
修改后的代码,由于使用了调用转发,使得调用的具体的细节被隐藏在Friend内部,从而使Someone与Stranger之间的直接联系被省略掉了,这样一来,系统内部的耦合度降低了。
7. 合成复用原则该原则就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分:新的对象通过向这些对象的委派达到复用已有功能的目的。而在我们的代码中尽可能使用组合而不是用继承是什么原因呢?
原因如下:
-
继承复用破坏包装,它把父类的实现细节直接暴露给了子类,这违背了信息隐藏的原则;
-
如果父类发生了改变,那么子类也要发生相应的改变,这就直接导致了类与类之间的高耦合,不利于类的扩展、复用、维护等,也带来了系统僵硬和脆弱的设计。而用合成和聚合的时候新对象和已有对象的交互往往是通过接口或者抽象类进行的,就可以很好的避免上面的不足,而且这也可以让每一个新的类专注于实现自己的任务,符合单一职责原则。
合成复用原则使用建议:
合成和聚合均是关联的特殊情况。聚合用来表示“拥有”关系或者整体与部分的关系;而合成则用来表示一种强得多的“拥有”关系。在一个合成关系里面,部分和整体的生命周期是一样的。一个合成的新的对象完全拥有对其组成部分的支配权,包括它们的创建和销毁等。 使用程序语言的术语来说,组合而成的新对象对组成部分的内存分配、内存释放有绝对的责任。要正确的选择合成/复用和继承,必须透彻地理解里氏替换原则。