1、概述
OOP(Object-oriented programming),指面向对象程序设计。也是目前主流的程序设计思想。这边讨论下由Robert C. Martin在21世纪初定义的关于OOP的五个原则SOLID。正确的应用,可以让程序更加易于维护、重用和扩展。其主要目的也是为了让软件具有低耦合,高内聚和强封装。通过将这些原则有机的结合在一起,能够让开发者编写出更高质量的强大代码。关于这些设计原则,最好是在实习开发需要中根据实际情况灵活使用,不要为了遵守而遵守。但是如果对于一个实际需求或者功能,如果没有充足的违背这些原则的理由,默认还是遵守,尤其是当代码量很庞大的时候。对于一个复杂的工程有时候就算可能会牺牲一点点运行的时间或者空间,也最好不要违背这些原则。接下来详细介绍一下。
2、单一职责原则
SRP(Single-responsibility principle):There should be never more than one reason for a class to change. 说的是一个类只应该有一个引起它改变的理由。
这个原则的思想我觉得是OOP这几个原则中最重要的一个。声明一个对象/类应该只有一个职责,并且它应该由类完全封装。这个原则将导致类中有更强的内聚力而依赖类之间更松散的耦合,从而有更好的可读性和更低复杂性的代码。当一个类有各种各样的责任时,要理解和编辑这个类会变得更加困难。因此,如果有多个职责,那最好将其拆分开来。这一点Android 中的 RecyclerView(Android RecyclerView内部机制)做的非常好。其将里面的Recycler类和LayoutManager类各自负责的功能进行隔离,为了做到职责之间的分离,甚至牺牲了一些运行时的空间和时间。
例子如下:
```
class Rectangle{
private int mVar1;
private int mVar2;
public double calculateArea(){
}
punlic void drawView(){
}
}
```
上面一个类 里面有两个方法calculateArea()和drawView()。area()负责返回矩形大小,drawView()负责绘制矩形。然后area()函数会由另外一个计算模块需要调用来使用,而draw()是由GUI的模块来使用,这时候如果GUI模块发生变化,就有可能引起draw()的改变,而如果drawView()中用到了mVar1和mVar2属性,并且calculateArea()也用到了mVar2和mVar2属性。那么修改draw()的代码就有可能会触发area()中代码的修改。改成这样就比较合适了:
```
class Rectangle{
RectangleGeometry mRectangleGeometry1;
RectangleView mRectangleView2;
...
}
class RectangleGeometry{
private int mVar1;
private int mVar2;
public double calculateArea(){
}
...
}
class RectangleView{
private int mVar1;
private int mVar2;
public double drawView(){
}
...
}
```
上面的代码将负责计算矩形的职责和绘制矩形的职责分离开来,这样当需要其中一块需要修改的时候,就不会涉及到另外一块的修改。但是这么做的问题就是有可能会产生类个数的膨胀和碎片化。这个就需要开发者自己去平衡了。
3、开闭原则
OCP(The Open-Closed Principle):Software entities (classes, modules, functions, etc…) should be open for extension, but closed for modification. 说的是软件开发应该对扩展开发,对修改关闭。当开发者想要向对象添加新功能时,应该通过通过将AbstractClass / Interface的继承来扩展它。而不应该在去修改现有的类。因为有可能另一个对象也在使用这个类,这样如果擅自修改可能会引起其他使用这个类的对象出错。而通过继承(这边指广义上的继承,包括继承类、实现接口、甚至使用组合或者其他设计模式来继承原有类的功能),来扩展原有类则不仅可以复用原有类的功能,并且新增功能来实现特定需求。
例子如下:
```
public class Rectangle {
private double mLength;
private double mHeight;
private double mArea;
// getters/setters
}
class Circle {
private double mRadius;
private double mArea;
// getters/setters ...
}
public class AreaFactory {
public double calculateArea(ArrayList... shapes) {
double area ;
for (Object shape : shapes) {
if (shape instanceof Rectangle) {
Rectangle rect = (Rectangle)shape;
area += (rect.getLength() * rect.getHeight());
} else if (shape instanceof Circle) {
Circle circle = (Circle)shape;
area +=
(circle.getRadius() * cirlce.getRadius() * Math.PI);
} else {
throw new RuntimeException("Shape not supported");
}
}
return area;
}
}
```
上面的代码就没有遵守开闭原则,如果每次多一个图形对象,AreaFactory 中的calculateArea()就要进行修改。同时每一个图形对象都要有一个area属性。改成这样比较合适:
```
public abstract class Shape {
private double mArea;
// getters/setters
double getArea();
}
public class Rectangle implements Shape{
private double length;
private double height;
// getters/setters
@Override public double getArea() {
return (length * height);
}
}
public class Circle implements Shape{
private double radius;
// getters/setters ...
@Override public double getArea() {
return (radius * radius * Math.PI);
}
}
public class AreaFactory {
public double calculateArea(ArrayList... shapes) {
double area = 0;
for (Shape shape : shapes) {
area += shape.getArea();
}
return area;
}
}
```
经过如上修改以后,需要增加一个新的形状只需要继承Shape 并实现其抽象方法就好。并不需要修改AreaFactory 对象的任何代码。
4、李氏替换原则
LSP(The Liskov Substitution Principle):subtypes must be substitutable for their base types. 这个原则讲的是子类必须完全可替代父类。正如它的名字,LSP是由Barbara Liskov定义的。对象应该可以由其子类型的实例替换,并且不会从客户端的角度影响系统的功能。开发者应该始终能够使用基类并能获得想要的结果。通常,当想要表示一个对象时,开发者会根据它的属性对其类进行设计,但正确的方式应该是根据对象的行为,也就是方法来设计类。这样就能保存抽象出来的行为就算由不同子类实现,但调用的时候不受具体是什么子类的影响。从而可以让代码易于重用和整个类的层次结构易于理解。从这可以看出LSP和前面说的OCP有很强的相互关系。Robert C. Martin甚至说“违反LSP是对OCP的潜在侵犯”。而遵守这一原则也是在鼓励开发性要面向接口编程。
例子如下:
```
public interface Car {
public void startEngine();
}
public Ferrari implements Car {
...
@Override
public void startEngine() {
//logic ...
}
}
public Tesla implements Car{
private IsCharged;
@Override
public void startEngine() {
if (!IsCharged)
return;
//logic ...
}
}
public static void letStartEngine(Car car) {
car.startEngine();
}
```
正如代码中中,有两类汽车。一类是燃料汽车和一类是电动汽车。电动汽车只有在充电时才能启动。如果汽车是电动的而不是充电的话,LetStartEngine方法将直接返回不执行下面的逻辑代码,这就打破了LSP原则。因为基类想实现启动汽车这一行为,但其子类电动车确有可能无法执行这一行为。这就有可能导致其他对象里面调用汽车启动这一行为之后的后续一系列逻辑出错。想象一下,如果Car类是很早以前其他人写的,整个软件有多处使用Car类startEngine()地方,而且执行完startEngine()会立刻向下执行与之相关的代码。作为Tesla类的开发者不应该去修改Car类(违反前面的开闭原则),更不应该去修改那些调用Car类startEngine()方法的类。所以只能修改自己的Tesla类来使其startEngine()的产生的效果不会影响其它类因为Car类的实现是Tesla就出现错误。
可以作如下修改:
```
public interface Car {
public void startEngine();
}
public Ferrari implements Car {
...
@Override
public double startEngine() {
//logic ...
}
}
public Tesla implements Car{
...
@Override
public double startEngine() {
if (!IsCharged)
TurnOnCar();
//logic ...
}
}
public void letStartEngine(Car car) {
car.startEngine();
}
```
在电动车执行启动这一行为时,当它没有充电,对其进行充电操作,确保汽车启动这一行为的有效执行。即确保了超类所拥有的性质和操作在子类中仍然成立。
5、接口隔离原则
ISP(The Interface Segregation Principle):Classes that implement interfaces, should not be forced to implement methods they do not use.这个原则在说接口实现的类不应该被强制实现它们不使用的方法。这个原则说的是关于如何编写接口的。一旦接口变得太大/太胖,就需要将其拆分为更具体更小的接口。如果一个接口添加太多不应该存在的方法,那么实现接口的类也必须实现这些方法。ISP旨在使系统保持解耦,从而更容易重构,更改和维护。
例子如下:
```
public interface OnClickListener {
void onClick(View v);
void onLongClick(View v);
void onTouch(View v, MotionEvent event);
}
```
上面是一个对于点击按钮的监听接口,如果设计成上面的形式,那么实现这个接口的开发者,比如说想对一个按钮进行监听,就需要同时实现上面三种方法,就算我其实只想监听它的点击事件,不想监听它的长按事件和触摸事件。Android开发中常常一个界面中会有好几个按钮,这时候如果每个按钮都要只要实现点击事件的监听,而如果Android官方对其监听事件是这样设计的话估计要被开发者吐槽了。正确的设计方法应该如下:
```
public interface OnClickListener {
void onClick(View v);
}
public interface OnLongClickListener {
void onLongClick(View v);
}
public interface OnTouchListener {
void onTouch(View v, MotionEvent event);
}
```
这样开发者的需求是要实现什么事件的监听就实现对应的接口就好了,不需要实现其他不必要的接口。
6、依赖倒置原则
DIP(Dependency Inversion Principle):High level modules should not depend on low level modules rather both should depend on abstraction. Abstraction should not depend on details; rather detail should depend on abstraction. 高级模块不应该依赖于低级模块,两者都应该依赖于抽象。抽象不应该依赖于细节,而细节应该依赖于抽象。这个原则是5大原则里面最不好理解的,这里的高级模块只内部包含了很多低级模块的模块。而这些高级模块里面的低级模块,其定义应该是抽象类或者接口,具体实现才是实现这些抽象类和接口的低级模块。抽象类之间的交互应该通过抽象类或者接口来进行,而不应该是具体的实现类,同时具体实现类之间的交互也应该通过抽象类或者接口。这么做的好处自然就是降低模块间的耦合。
例子如下:
```
class Program {
public void work() {
}
}
class Engineer{
Program program;
public void setProgram(Program p) {
program = p;
}
public void manage() {
program.work();
}
}
```
上述代码的问题在于它打破了依赖倒置原则中的高级模块不应该依赖于低级模块。两者都应该取决于抽象。一个高级类的Engineer类,它持有一个名为Program的低级类。如果Engineer类非常复杂,包含非常复杂的逻辑。要像Engineer引入的一个新的NewProgram 类,那就要修改Engineer中的代码,而Engineer又很复杂,会牵扯到Engineer的其他部分。同时之前的Program 类也有可能受到影响。现在对其进行重构:
```
interface IProgram {
public void work();
}
class Program implements IProgram{
public void work() {
}
}
class NewProgram implements IProgram{
public void work() {
}
}
class Engineer{
IProgram program;
public void setProgram(IProgram p) {
program = p;
}
public void manage() {
program.work();
}
}
```
在这个新设计中,通过IProgram接口添加了一个新的抽象层。这样就解决了之前代码中的问题。有一个新的NewProgram 类时,Engineer不需要改变,这样最小化的影响了Engineer类中的其它功能。
7、总结
在开发任何软件时,有两个非常重要的概念:
① 内聚:当系统的不同部分一起工作以获得比每个部分单独工作时更好的结果。
② 耦合:可以看作是一种类之间的依赖程度。
其实上面这些原则,核心目的就是为了使开发者能写出高内聚,低耦合的代码。还是最前面所说的,这些原则还是要灵活运用,而不是一味的去为了遵守而遵守它。