代码有高低质量之分,而设计模式则是提高代码质量如提高其可维护性、扩展性、复用性等等,学习设计模式也可以提高个人对系统设计的理解等等,反正好处多多,正好最近也在学习,便记录一下。
要理解设计模式,我觉得除了要懂得这些设计模式的使用场景外,还要知道他们是围绕什么样的思想或者遵守什么样规则,那不得不提设计原则了。
1.开闭原则
对扩展开放,对修改关闭,即对于某个模块功能应该尽量使用继承或者接口来实现扩展,而不应该在原来代码上修改
就比如一个动物接口有play的方法,一个dog实现了play方法,如下面的代码
public class OpenAndClosePrinciple {
public static void main(String[] args) {
assert new Dog().play().equals("i am happy");
System.out.println(new Dog().play());
System.out.println(new ExtraDog().play());
}
interface Animal{
public String play();
}
static class Dog implements Animal{
@Override
public String play() {
return "i am happy";
}
}
static class ExtraDog extends Dog{
@Override
public String play() {
return super.play()+"too";
}
}
}
但是某一天这个dog需要在play的基础上加其他东西,此时如果把Dog的play方法返回的字符串加上"too",则会导致主函数的业务逻辑不通过,即修改原有对象的方法会影响到引用该方法的业务逻辑,而修改Animal接口增加一个play2方法也不合适,这时候可以使用继承狗的方式去重写这个方法而不改写原来的代码,开闭原则要求尽可能通过扩展实现变化,尽量少的改变已有模块,尤其是一些比较底层,通用的代码
2.里氏替换原则
所有引用基类的地方必须能透明地使用其子类对象。比如一个可以接受Iterable参数的方法,他可以接受他全部的子类作为参数,但是一个接受ArrayList的方法不一定能接口同样实现了Iterable的接口的HashSet方法。
1.里氏替换原则是开闭原则的实现基础,设计程序时应尽可能基类进行对象的定义及引用,具体运行时再决定具体的子类类型。
2.提高复用性,子类继承父类时会继承其属性和方法,但同时也侵入子类代码,也增加了耦合度,父类变更要考虑对子类造成的影响
3.提高扩展性,可以通过重写父类方法进行功能扩展
依赖倒置原则:
程序依赖于抽象接口,不能依赖具体的实现,即对抽象、通用进行编程,尽量不要对实现进行编程
static class Person{
public void feed(Dog dog){
System.out.println("i feed a "+dog.getClass().getSimpleName());
}
public void feed(Cat cat){
System.out.println("i feed a "+cat.getClass().getSimpleName());
}
public void feed(BaseAminal aminal){
System.out.println("i feed a "+aminal.getClass().getSimpleName());
}
}
从代码可以看出,如果没有面向基类编程,则每天添加一个动物,就需要写一个feed方法,而第三个面向抽象对象的方法则不用写过多的方法,减少代码量,增强可读性
1)高层模块不应该依赖低层模块,应该依赖抽象(接口或者抽象类)
2)接口或抽象类不应该依赖于实现类,实现类应该依赖于接口或者抽象
3.接口隔离原则
客户端不应该依赖它不需要的接口,类间的依赖关系应该建立在最小的接口上。也就是说建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。
比如下面的代码,因为movable接口功能过多,导致main函数的输出不符合现实逻辑
public class InterfaceClosePrinciple {
public static void main(String[] args) {
System.out.println(new Dog().FlyOrRun()+"天空");
}
}
interface movable{
String FlyOrRun();
}
abstract class BaseAminal implements movable{
public String move(){
return this.FlyOrRun();
}
}
class Dog extends BaseAminal {
@Override
public String FlyOrRun() {
return "running to";
}
}
但是把这两个功能拆分以后,得到的结果确实也符合逻辑
public class InterfaceClosePrinciple {
public static void main(String[] args) {
System.out.println(new Dog().fly()+"天空");
}
}
abstract class BaseAminal implements movable {
public String moveWithFly(){
return this.fly();
}
public String moveWithRun(){
return this.run();
}
}
interface movable {
String fly();
String run();
}
class Dog extends BaseAminal {
@Override
public String fly() {
return "i cant't fly to";
}
@Override
public String run() {
return "running to";
}
}
4.单一职责原则
单一职责可以降低类的复杂性,提高代码可读性、可维护性,并且使得修改引起的风险降低,保证设计类、接口、方法时做到功能单一,职责明确.
如果模块承担的职责过多,除了不易维护之外还会剥夺其他模块的能力,比如一个用户他的邮箱、性别等等信息都统一由一个updte语句修改,那么可能会造成某些不需要修改的字段在某次方法调用中被修改了,在造成数据错误的同时也增加了排查难度,应该每个方法都尽量实现的功能的尽量单一,但同时也要注意划分是否恰当,随着需求、系统变更,如果划分不当,会导致原来符合单一职责的模块代码的代码量增多,资源浪费,对于接口隔离模式也是——比如上面的接口多增加了Dog类做不了几个移动的方法,比如开车、坐飞机、坐火箭等等移动方式,这时候狗也要实现这些方法就冗余了,对于狗而言这是不用考虑的方法,所以在使用时也要注意划分问题
接口隔离原则虽然和单一原则一样有细化的思想,但是他们针对的不一样,接口隔离针对的是抽象(比如接口、抽闲类),而单一职责则是相对具体的(比如具体类、具体方法)
5.迪米特法则(最少知识原则)
一个软件实体应尽可能少的和其他实体发生相互作用,用于降低类和类的耦合度,他也叫只和朋友类交流,朋友类的定义:
出现在方法中的出参入参、成员变量等等,比如下面的driverByMySelf方法,他没有和直接的朋友进行交流,即自己操作其他无关的变量,增加了耦合度,而driver方法就和Car进行交互,省心省力,但在使用该原则时也要注意与其他实体发生交互时不需要操作其他实体过多的方法,只需要调用自己需要的即可
class Man {
public void driver(Car car) {
car.move();
}
public void driverByMySelf(Car car){
new Engine().start();
new Tire().start();
}
}
class Car {
private String engine;
public void move() {
new Engine().start();
new Tire().start();
}
}
class Engine {
public void start() {
System.out.println("发动机启动");
}
}
class Tire {
public void start() {
System.out.println("轮胎转动");
}
}
6.合成复用原则
合成复用原则是通过将已有的对象纳入新对象中,作为新对象的成员对象来实现的,新对象可以调用已有对象的功能,从而达到复用。
原则是优先使用组合/聚合的方式,而不是优先继承,但是组合和聚合也有不同点,组合则是面向整体的,各部分不能脱离而独立存在,比如身体-心脏就是组合的关系,属于强关联,而聚合就是弱关联,比如学生-教师的关系,学生脱离教师也没有影响,这个原则主要是建议利用好已有的对象达到功能复用。
总结:
1.里氏替换和依赖倒置原则:要求面向抽象、基类编程
2.接口隔离原则和单一职能原则:细化接口、职责
3.开闭原则:对修改关闭,对扩展开放
4.合成复用原则:注意复用
5.迪米特法则:通过直接对象做事,降低与其他无关类的耦合