有几天没发文章了,一直有人在公众号问我关于观察者模式的问题,所以我决定抽时间写一写关于设计模式的内容。今天先介绍一些基础的东西。
六大原则
我以前在面试别的人的时候,总是喜欢聊聊设计模式,因为总感觉功能部分都能写出来,但是代码质量和代码设计的东西熟练,才能更好地跟团队配合,方便产品的迭代。
六大原则是:
- 单一职责原则
- 里氏替换原则
- 依赖倒置原则
- 接口隔离原则
- 迪米特原则
- 开闭原则
也有人说是五大原则的,少了迪米特原则。乍一看,其中开发者们最熟悉的或者说听得最多的也就是开闭原则了,其它听起来都会有一些陌生。下面就一个个介绍一下。
单一职责原则
这是一个最简单,却最难做到的原则。为什么这么说呢?
它的定义只有一句话:不要存在多于一个导致类变更的原因。通俗的说,即一个类只负责一项职责。但为什么说很难做到呢,我刚才想去我的代码中找到一个比较合适的例子来说明这个问题,却没有一个具有代表性的,因为职责这个概念有些主观,接口根据职责划分。下面想个简单的例子吧。
例如一个用户系统,有姓名和年龄两个属性:
public class Person {
private String name;
private String age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
public void changeAge(String age) {
}
public void changeName(String age) {
}
}
这个类建立有什么问题呢?name和age的set,get方法是数据类型,也就是业务对象,但是changeAge和changeName需要跟服务器进行交互,属于业务逻辑,甚至于在这两个方法中还需要调用set,get方法。
根据单一职责原则,我们需要将业务和数据分开:
public class PersonObj {
private String name;
private String age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
}
public class PersonLogic {
public void changeAge(String age) {
}
public void changeName(String age) {
}
}
单一原则的好处:
- 可以降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多;
- 提高类的可读性,提高系统的可维护性;
- 变更引起的风险降低,变更是必然的,如果单一职责原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。
里氏替换原则
定义1:如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。
定义2:所有引用基类的地方必须能透明地使用其子类的对象。
根据定义我们可以这样理解:
- 子类必须完全实现父类的方法
- 子类可以有自己的个性
- 覆写或实现父类的方法时输入参数可以宽于或等于父类参数
- 覆写或实现父类的方法时输出结果可以窄于或等于父类参数
第一点好理解,后面是什么意思呢?看个例子:
public class Father {
public void printf(HashMap map){
System.out.printf("父类方法");
}
}
public class Son {
public void printf(Map map){
System.out.printf("父类方法");
}
}
这样只要传入的参数是HashMap都是执行父类的方法,子类由于比父类参数要宽,相当于重载了printf方法。这样做的目的不容易引起逻辑的混乱
依赖导致原则
定义:高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。
依赖倒置原则的核心思想是面向接口编程,我们依旧用一个例子来说明面向接口编程比相对于面向实现编程好在什么地方。场景是这样的,父亲给孩子讲故事,需要给他一本书,或一份报纸:
class Book{
public String getContent(){
return "读书";
}
}
class NewsPaper{
public String getContent(){
return "报纸";
}
}
class Father{
public void read(Book book){
System.out.println("爸爸"+book.getContent());
}
public void read(NewsPaper news){
System.out.println("爸爸"+news.getContent());
}
}
public class Client{
public static void main(String[] args){
Father f = new Father();
f.read(new Book());
f.read(new NewsPaper());
}
}
上述代码没有什么问题,但是有一天我们再新增一个可读的东西,如pad,那我们要重新写一个pad类,同时Father类还要新增一个read方法。如果哪天再增加一个类,还有做如上处理,太麻烦了。
我们可以这样优化一下:
interface IReader{
public String getContent();
}
然后让Book Newspaper Pad类都实现这个接口,这样父类只需要写一个方法即可:
class Father{
public void read(IReader reader){
System.out.println("爸爸"+reader.getContent());
}
}
接口隔离原则
定义:客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。
简单翻译一下这句话,就是一个类去实现接口的时候,不应该去实现他不需要的方法。
interface I {
public void method1();
public void method2();
public void method3();
public void method4();
public void method5();
}
class A{
public void depend1(I i){
i.method1();
}
public void depend2(I i){
i.method2();
}
public void depend3(I i){
i.method3();
}
}
class B implements I{
public void method1() {
System.out.println("类B实现接口I的方法1");
}
public void method2() {
System.out.println("类B实现接口I的方法2");
}
public void method3() {
System.out.println("类B实现接口I的方法3");
}
public void method4() {}
public void method5() {}
}
class C{
public void depend1(I i){
i.method1();
}
public void depend2(I i){
i.method4();
}
public void depend3(I i){
i.method5();
}
}
class D implements I{
public void method1() {
System.out.println("类D实现接口I的方法1");
}
//对于类D来说,method2和method3不是必需的,但是由于接口A中有这两个方法,
//所以在实现过程中即使这两个方法的方法体为空,也要将这两个没有作用的方法进行实现。
public void method2() {}
public void method3() {}
public void method4() {
System.out.println("类D实现接口I的方法4");
}
public void method5() {
System.out.println("类D实现接口I的方法5");
}
}
public class Client{
public static void main(String[] args){
A a = new A();
a.depend1(new B());
a.depend2(new B());
a.depend3(new B());
C c = new C();
c.depend1(new D());
c.depend2(new D());
c.depend3(new D());
}
}
可以看到,如果接口过于臃肿,只要接口中出现的方法,不管对依赖于它的类有没有用处,实现类中都必须去实现这些方法,这显然不是好的设计。如果将这个设计修改为符合接口隔离原则,就必须对接口I进行拆分。在这里我们将原有的接口I拆分为三个接口。
nterface I1 {
public void method1();
}
interface I2 {
public void method2();
public void method3();
}
interface I3 {
public void method4();
public void method5();
}
class A{
public void depend1(I1 i){
i.method1();
}
public void depend2(I2 i){
i.method2();
}
public void depend3(I2 i){
i.method3();
}
}
class B implements I1, I2{
public void method1() {
System.out.println("类B实现接口I1的方法1");
}
public void method2() {
System.out.println("类B实现接口I2的方法2");
}
public void method3() {
System.out.println("类B实现接口I2的方法3");
}
}
class C{
public void depend1(I1 i){
i.method1();
}
public void depend2(I3 i){
i.method4();
}
public void depend3(I3 i){
i.method5();
}
}
class D implements I1, I3{
public void method1() {
System.out.println("类D实现接口I1的方法1");
}
public void method4() {
System.out.println("类D实现接口I3的方法4");
}
public void method5() {
System.out.println("类D实现接口I3的方法5");
}
}
接口隔离原则的含义是:建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。本
迪米特法则
定义:一个对象应该对其他对象保持最少的了解。
说白点就是尽量降低耦合。
我在网上找到了这样一个简单例子,可以看一下,做个对比:
有一个集团公司,下属单位有分公司和直属部门,现在要求打印出所有下属单位的员工ID。先来看一下违反迪米特法则的设计。
//总公司员工
class Employee{
private String id;
public void setId(String id){
this.id = id;
}
public String getId(){
return id;
}
}
//分公司员工
class SubEmployee{
private String id;
public void setId(String id){
this.id = id;
}
public String getId(){
return id;
}
}
class SubCompanyManager{
public List getAllEmployee(){
List list = new ArrayList();
for(int i=0; i<100; i++){
SubEmployee emp = new SubEmployee();
//为分公司人员按顺序分配一个ID
emp.setId("分公司"+i);
list.add(emp);
}
return list;
}
}
class CompanyManager{
public List getAllEmployee(){
List list = new ArrayList();
for(int i=0; i<30; i++){
Employee emp = new Employee();
//为总公司人员按顺序分配一个ID
emp.setId("总公司"+i);
list.add(emp);
}
return list;
}
public void printAllEmployee(SubCompanyManager sub){
List list1 = sub.getAllEmployee();
for(SubEmployee e:list1){
System.out.println(e.getId());
}
List list2 = this.getAllEmployee();
for(Employee e:list2){
System.out.println(e.getId());
}
}
}
public class Client{
public static void main(String[] args){
CompanyManager e = new CompanyManager();
e.printAllEmployee(new SubCompanyManager());
}
}
现在这个设计的主要问题出在CompanyManager中,根据迪米特法则,只与直接的朋友发生通信,而SubEmployee类并不是CompanyManager类的直接朋友(以局部变量出现的耦合不属于直接朋友),从逻辑上讲总公司只与他的分公司耦合就行了,与分公司的员工并没有任何联系,这样设计显然是增加了不必要的耦合。按照迪米特法则,应该避免类中出现这样非直接朋友关系的耦合。修改后的代码如下:
class SubCompanyManager{
public List getAllEmployee(){
List list = new ArrayList();
for(int i=0; i<100; i++){
SubEmployee emp = new SubEmployee();
//为分公司人员按顺序分配一个ID
emp.setId("分公司"+i);
list.add(emp);
}
return list;
}
public void printEmployee(){
List list = this.getAllEmployee();
for(SubEmployee e:list){
System.out.println(e.getId());
}
}
}
class CompanyManager{
public List getAllEmployee(){
List list = new ArrayList();
for(int i=0; i<30; i++){
Employee emp = new Employee();
//为总公司人员按顺序分配一个ID
emp.setId("总公司"+i);
list.add(emp);
}
return list;
}
public void printAllEmployee(SubCompanyManager sub){
sub.printEmployee();
List list2 = this.getAllEmployee();
for(Employee e:list2){
System.out.println(e.getId());
}
}
}
修改后,为分公司增加了打印人员ID的方法,总公司直接调用来打印,从而避免了与分公司的员工发生耦合。
开闭原则
这应该是我们听得最多的一个原则了,在平时的项目开发中,用的也最多,因为谁也不想,一次产品的迭代,还需要修改核心代码,然后全部重新测试一次。
定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
这个定义没有那么复杂难理解,我不再做过多的解释,我这里还是通过一个小例子来说明:
public interface ICar {
public String getName();
public float getPrice();
}
public class Car implements ICar{
private String name;
private float price;
public Car(String name,float price){
this.name = name;
this.price = price;
}
@Override
public String getName() {
return name;
}
@Override
public float getPrice() {
return price;
}
}
当有一天我们获取车的价格需要打折时,可以重新写一个类SaleCar:
public class SaleCar extends Car{
public SaleCar(String name, float price) {
super(name, price);
}
@Override
public float getPrice() {
return super.getPrice()*8/10;
}
}
我们这样做的目的是当有新功能出现的时候,尽量不要去修改原有的逻辑,可以实现一个新的类,然后覆写父类的方法,这样,原有的逻辑没有变,新的需求也实现了。当有一天出现bug了,可以直接修改这一个类就可以。