如何设计中国十大最强国企网站的界面风格?
摘要:网站界面风格设计描述,中国最强十大国企,漳州网站建设去博大a优,标志设计分析前言 上一篇,我们讲了开放封闭原则,想要让系统符合开放封闭原则,最重要的就
网站界面风格设计描述,中国最强十大国企,漳州网站建设去博大a优,标志设计分析前言
上一篇#xff0c;我们讲了开放封闭原则#xff0c;想要让系统符合开放封闭原则#xff0c;最重要的就是我们要构建起相应的扩展模型#xff0c;所以#xff0c;我们要面向接口编程。
而大部分的面向接口编程要依赖于继承实现#xff0c;继承的重要性不如封装和多…前言
上一篇我们讲了开放封闭原则想要让系统符合开放封闭原则最重要的就是我们要构建起相应的扩展模型所以我们要面向接口编程。
而大部分的面向接口编程要依赖于继承实现继承的重要性不如封装和多态但在大部分面向对象程序设计语言中继承却是构建一个对象体系的重要组成部分。
理论上在定义了接口之后我们就可以把继承这个接口的类完美地嵌入到我们设计好的体系之中。然而用了继承子类就一定设计对了吗事情可能并没有这么简单。新的类虽然在语法上声明了一个接口形成了一个继承关系但我们要想让这个子类真正地扮演起这个接口的角色还需要有一个好的继承指导原则。
所以这一篇我们就来看看可以把继承体系设计好的设计原则Liskov 替换法则。
Liskov 替换原则
2008 年图灵奖授予 Barbara Liskov表彰她在程序设计语言和系统设计方法方面的卓越工作。她在设计领域影响最深远的就是以她名字命名的 Liskov 替换原则Liskov substitution principle简称 LSP。
1988 年Barbara Liskov 在描述如何定义子类型时写下这样一段话 这里需要如下替换性质若每个类型 S 的对象 o1都存在一个类型 T 的对象 o2使得在所有针对 T 编程的程序 P 中用 o1 替换 o2 后程序 P 行为保持不变则 S 是 T 的子类型。 用通俗的讲法来说意思就是子类型subtype必须能够替换其父类型base type。
这句话看似简单但是违反这个原则后果是很严重的比如父类型规定接口不能抛出异常而子类型抛出了异常就会导致程序运行的失败。
虽然很好理解但你可能会有个疑问我的子类型不都是继承自父类型咋就能违反 LSP 呢这个 LSP 是不是有点多此一举呢
我们来看个例子有不少的人经常写出类似下面这样的代码
void handle(final Handler handler) {if (handler instanceof ReportHandler) {// 生成报告((ReportHandler)handler).report();return;}if (handler instanceof NotificationHandler) {// 发送通知((NotificationHandler)handler).sendNotification();}...
}根据上一篇的内容这段代码显然是违反了 OCP 的。另外在这个例子里面虽然我们定义了一个父类型 Handler但在这段代码的处理中是通过运行时类型识别Run-Time Type Identification简称 RTTI也就是这里的 instanceof知道子类型是什么的然后去做相应的业务处理。
但是ReportHandler 和 NotificationHandler 虽然都是 Handler 的子类但它们没有统一的处理接口所以它们之间并不存在一个可以替换的关系这段代码也是违反 LSP 的。这里我们就得到了一个经验法则如果你发现了任何做运行时类型识别的代码很有可能已经破坏了 LSP。 再来看一个实例,也是违法了LSP public class TestA {public void fun(int a,int b){System.out.println(ab(ab));}public static void main(String[] args) {System.out.println(父类的运行结果);TestA anew TestA();a.fun(1,2);//父类存在的地方可以用子类替代//子类B替代父类ASystem.out.println(子类替代父类后的运行结果);TestB bnew TestB();b.fun(1,2);}
}
class TestB extends TestA{Overridepublic void fun(int a, int b) {System.out.println(a-b(a-b));}
}大家肯定也都能猜出来结果是什么样子的
父类的运行结果
123
子类替代父类后的运行结果
1-2-1Process finished with exit code 0我们想要的结果是“123”。可以看到方法重写后结果就不是了我们想要的结果了也就是这个程序中子类B不能替代父类A。这违反了里氏替换原则原则从而给程序造成了错误。 子类中可以增加自己特有的方法 public class TestA {public void fun(int a,int b){System.out.println(ab(ab));}public static void main(String[] args) {System.out.println(父类的运行结果);TestA anew TestA();a.fun(1,2);//父类存在的地方可以用子类替代//子类B替代父类ASystem.out.println(子类替代父类后的运行结果);TestB bnew TestB();b.fun(1,2);b.newFun();}
}
class TestB extends TestA{public void newFun(){System.out.println(这是子类的新方法...);}
}这次运行出来的代码结果就是我们意料中的内容
父类的运行结果
123
子类替代父类后的运行结果
123
这是子类的新方法...Process finished with exit code 0基于行为的 IS-A
如果你去阅读关于 LSP 的资料很有可能会遇到一个有趣的问题也就是长方形正方形问题。在我们对于几何通常的理解中正方形是一种特殊的长方形。所以我们可能会写出这样的代码
class Rectangle {private int height;private int width;// 设置长度public void setHeight(int height) {this.height height;}// 设置宽度public void setWidth(int width) {this.width width;}//面积public int area() {return this.height * this.width;}
}class Square extends Rectangle {// 设置边长public void setSide(int side) {this.setHeight(side);this.setWidth(side);}Overridepublic void setHeight(int height) {this.setSide(height);}Overridepublic void setWidth(int width) {this.setSide(width);}
}这段代码看上去一切都很好然而它却是有问题的因为它在下面这个测试里会失败
import org.junit.Assert;import static org.hamcrest.CoreMatchers.is;public class Test {public static void main(String[] args) {Rectangle rect new Square();rect.setHeight(4); // 设置长度rect.setWidth(5); // 设置宽度Assert.assertThat(rect.area(), is(20));//对结果进行断言}
}如果想保证断言assert的正确性Rectangle 和 Square 二者在这里是不能互相替换的。使用 Rectangle 的代码必须知道自己使用的到底是 Rectangle 还是 Square。
出现这个问题的原因就在于我们构建模型时会理所当然地把我们直觉中的模型直接映射到代码模型上。在我们直觉中正方形确实是一种长方形。
在我们设计的这个对象体系中边长是可以调整的。然而在几何的体系里面长方形的边长是不能随意改变的设置好了就是设置好了。换句话说两个体系内“长方形”的行为是不一致的。所以在这个对象体系中正方形边长即使可以调整但正方形也并不是一个长方形也就是说它们之间不满足 IS-A 关系。
你可能听说过继承要符合 IS-A 的关系也就是说如果 A 是 B 的子类就需要满足 A 是一个 BA is a B。但你有没有想过凭什么 A 是一个 B 呢判断依据从何而来呢你应该知道这种判定显然不能依靠直觉。其实从前面的分析中你也能看出一些端倪来IS-A 的判定是基于行为的只有行为相同才能说是满足 IS-A 的关系。
更广泛的 LSP
如果理解了 LSP你会发现它不仅适用于类级别的设计还适用于更广泛的接口设计。比如我们在开发中经常会遇到系统集成的问题有不同的厂商都要通过 REST 接口把他们的统计信息上报到你的系统中但是有一个大厂上报的消息格式没法遵循你定义的格式因为他的系统改动起来难度比较大。你该怎么办呢
也许专门为大厂设计一个特定接口是最简单的想法但是一旦开了这个口子后面的各种集成接口都要为这个大厂开发一份特殊的而且如果未来再有其他大厂也提出要求你要不要为它们也设计特殊接口呢事实上很多项目功能不多但接口特别多就是因为在这种决策的时候开了口子。请记住公开接口是最宝贵的资源千万不能随意添加。
如果我们用 LSP 的角度看这个问题通用接口就是一个父类接口而不同厂商的内容就相当于一个个子类。让厂商面对特定接口系统将变得无法维护。后期随着人员变动接口只会更加膨胀到最后没有人说清楚每个接口到底是做什么的。
好那我们决定采用统一的接口可是不同的消息格式该怎么处理呢首先我们需要区分出不同的厂商办法有很多无论是通过 REST 的路径还是 HTTP 头的方式我们可以得到一个标识符。然后呢
很容易想到的做法就是写出一个 if 语句来像下面这样
if (identfier.equals(SUPER_VENDOR)) {...
}但是千万要遏制自己写 if 的念头一旦开了这个头后续的代码也将变得难以维护。我们可以做的是提供一个解析器的接口根据标识符找到一个对应的解析器像下面这样
RequestParser parser parsers.get(identifier);
if (parser ! null) {return parser.parse(request);
}这样一来即便有其他厂商再因为某些奇怪的原因要求有特定的格式我们要做的只是提供一个新的接口实现。这样一来所有代码的行为就保持了一致性核心的代码结构也保持了稳定。
总结
Liskov 替换原则其主要意思是说子类型必须能够替换其父类型。理解 LSP我们需要站在父类的角度去看而站在子类的角度常常是破坏 LSP 的做法一个值得警惕的现象是代码中出现 RTTI 相关的代码。继承需要满足 IS-A 的关系但 IS-A 的关键在于行为上的一致性而不能单纯凭日常的概念或直觉去理解。LSP 不仅仅可以用在类关系的设计上我们还可以把它用在更广泛的接口设计中。任何接口都是宝贵的在设计时都要精心考量。LSP 的根基在于继承但显然接口继承才是重点。
