概述
单一职责原则(Single Responsibility Principle)
所谓职责是指类变化的原因。如果一个类有多于一个的动机被改变,那么这个类就具有多于一个的职责。而单一职责原则就是指一个类或者模块应该有且只有一个改变的原因。
如果一个类承担的职责过多,就等于把这些职责耦合在一起了。一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计,当发生变化时,设计会遭受到意想不到的破坏。而如果想要避免这种现象的发生,就要尽可能的遵守单一职责原则。此原则的核心就是解耦和增强内聚性。
产生原因与职责扩散
没有任何的程序设计人员不清楚应该写出高内聚低耦合的程序,但是很多耦合常常发生在不经意之间,其原因就是:
职责扩散:因为某种原因,某一职责被分化为颗粒度更细的多个职责了。
解决办法
遵守单一职责原则,将不同的职责进行封装到不同的类和模块种,来减少职责之间的耦合。
里氏替换原则(Liskov Substitution Principle)
If S is a declared subtype of T, objects of type S should behave as objects of type T are expected to behave, if they are treated as objects of type T
从字面上翻译:如果S是T的子类型,对于S类型的任意对象,如果将他们看作是T类型的对象,则对象的行为也理应与期望的行为一致。
而另一种关于里氏替换原则的描述为Robert Martin在《敏捷软件开发:原则、模式与实践》一书中对原论文的解读:子类型(subtype)必须能够替换掉他们的基类型(base type)。这个是更简明的一种表述。
结合上述的两种描述,我们需要理解替换的实际含义,将后述的句子结合到到上文看,S需要完成T的期望行为,从抽象的角度看,S实际完成T不同形态下的行为。
什么是替换?
替换的前提是面向对象语言所支持的多态特性,同一个行为具有多个不同表现形式或形态的能力。
public static void temp(List<String> list) ;
List<String> list = new ArratList<String>();
temp(list);
List<String> list = new LinkedList<String>();
temp(list);
上述的代码,就是完成了替换行为,ArrayList与Linkedlist都实现了List接口,那当某个方法参数或变量是List
接口类型时,可以通过创建ArrayList去实现,也可以通过LinkedList去实现,这就是上述的替换。
实现里氏替换原则的要求
在开发中,为了满足客户不同的场景中的使用需求,都需要为用户提供基类以及接口去使用衍生类来完成。里氏替换原则
实际上就是将基类、接口从不同的用户场景下分离出来,达到普适性。
从小的时候,老师都给我们灌输一个理念,正方形是一个特殊的长方形,那么我们把他替换到我们的代码场景中呢。
我们定义一个长方形:
public class Rectangle {
private double height;
private double width;
public double getHeight() {
return height;
}
public void setHeight(double height) {
this.height = height;
}
public double getWidth() {
return width;
}
public void setWidth(double width) {
this.width = width;
}
}
按上述的逻辑,正方形其实就是height与width属性相等的特殊长方形,或者说正方形是长方形的衍生类。那么我们可以由此设计,在每使用一个set方法传入height与width时,可以直接给另一个属性赋值,这样就可以达到,长与宽始终相等。
public class Square extends Rectangle{
private double height;
private double width;
@Override
public void setHeight(double height) {
this.height = height;
this.width = height;
}
@Override
public void setWidth(double width) {
this.height = width;
this.width = width;
}
@Override
public double getHeight() {
return height;
}
@Override
public double getWidth() {
return width;
}
}
以上是设计者的逻辑,换到我们应用的场景中,使用者并不会根据衍生类实际的行为的不同而去创建不同的实例对象;就好比一个正方形摆在面前,人们直觉性的认为这是一个正方形,而不是认为这是一个“特殊的长方形”,而去使用长方形的类;但是在设计者眼中拥有不一样的特性,长宽相等,就应该在构造方法中一并传值;这两种不同的视角的差异,造成了设计时类的行为不统一,所以达不到想要的结果。就比如下面代码:main方法中创建了一个正方形
public static void main(String[] args) {
Rectangle rectangle = new Square();
rectangle.setHeight(5.0);
rectangle.setWidth(4.0);
System.out.println(area(rectangle));
}
public static double area(Rectangle rectangle){
return (rectangle.getHeight()*rectangle.getWidth());
}
我们可以看到Rectangle使用衍生类Square,可是两次的传值却不相同,导致
height = 5.0
weith = 5.0
被后面的setWidth(4.0)覆盖掉了
hright = 4.0
weith = 4.0
通过area方法计算出来的结果:
这并不是用户期望得出来的结果,或者说这并不是基类期望的结果。
上述的代码实现了属性的继承、逻辑关系的继承,但是并没有实现基类行为的继承。基类中可以赋值两次,为什么衍生类中复制两次却得不到期望的结果呢?按上述代码为例,按照里氏替换准则,我们真的有必要重写Set方法吗?正是因为按子类中的特殊性来构造方法,造成了普适性的缺失。
依照里氏替换原则,基类中衍生出来的类,必须完整支持基类中的行为,不能因为其特殊性而失去了普适性。也就是说,其前置条件一定要弱于基类的前置条件
,其后置条件一定要强于基类的后置条件
。
前置条件
- 前置条件是在方法运行之前必须为真的条件(condition)或者说断言(predicate)。换句话说,该方法告诉使用者:“
这是我对你的期望
”。即正在调用的方法期望在调用该方法之前或调用该方法时满足一定的条件。除非满足前置条件,否则不能保证操作会按其应有的方式执行。
后置条件
- 后置条件是在方法运行之后能够被保证为真的条件或者说断言。换句话说,该方法告诉使用者:“
这是我承诺为你做的
”。如果操作正确且满足前置条件(可能有多个),则可以保证后置条件为真。
只有当衍生类比基类更加具有普适性,才能完整的支持基类的行为。而需要衍生类的后置条件更强,是因为衍生类处理的场景更加具体,只有后置条件比基类后置条件强,才能将衍生类功能“独立”出来
。
违反里氏替换原则的危害
当我们违反了这一原则会带来有一些危害:
- 反直觉。期望所有子类行为是一致的,但如果不一致可能需要文档记录,或者在代码跑失败后涨此知识;
- 不可读。如果子类行为不一致,可能需要不同的逻辑分支来适配不同的行为,徒增代码复杂度;
- 不可用。可能出错的地方终将会出错。
最后
以上就是慈祥书包为你收集整理的里氏替换原则与单一职责原则概述与代码示例的全部内容,希望文章能够帮你解决里氏替换原则与单一职责原则概述与代码示例所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复