Android 开发之SOLID原则
SOLID是用来帮助定义面向对象设计五个基本原则的助记符:
- 单一功能原则
- 开闭原则
- 里氏替换原则
- 接口分离原则
- 依赖反转原则
SOLID#1:单一功能原则(SRP)
一个类在修改时应该有、也只能有一个理由
我们来更详细的说明这一原则,假设我们需要为Recycleview准备一个adapter,你很可能已经知道adapter用于从数据集合中获取数据并将它们适配到view。我曾见到过这样的实现:
// 违反单一功能原则
public class MovieRecyclerAdapter extends RecyclerView.Adapter<MovieRecyclerAdapter.ViewHolder> {
private List<Movie> movies;
private int itemLayout;
public MovieRecyclerAdapter(List<Movies> movies, int itemLayout)
{
this.movies = movies;
this.itemLayout = itemLayout;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
{
View v = LayoutInflater.from(parent.getContext())
.inflate(itemLayout, parent, false);
return new ViewHolder(v);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
final Movie movie = movies.get(position);
holder.itemView.setTag(movie);
holder.title.setText(movie.getTitle());
holder.rating.setText(movie.getRating());
String genreStr = "";
for (String str: movie.getGenre()) {
genreStr += str + ", ";
}
genreStr = genreStr.length() > 0 ?
genreStr.substring(0, genreStr.length() - 2) : genreStr;
holder.genre.setText(genreStr);
holder.releaseYear.setText(movie.getYear());
Glide.with(holder.thumbNail.getContext())
.load(movies.get(position)
.getThumbnailUrl())
.into(holder.thumbNail);
}
@Override
public int getItemCount() {
return movies.size();
}
public static class ViewHolder extends RecyclerView.ViewHolder {
@Bind(R.id.title) TextView title;
@Bind(R.id.rating) TextView rating;
@Bind(R.id.genre) TextView genre;
@Bind(R.id.releaseYear) TextView releaseYear;
@Bind(R.id.thumbnail) ImageView thumbNail;
public ViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
}
}
}
上面的这段代码违反了单一功能原则,因为adapter的onBindViewHolder函数不仅仅将Movie对象映射到view中,它同时还对数据进行了格式化。这就违反了单一功能原则。adapter应该仅仅负责将按顺序将数据适配到view。这里onBindViewHolder还从事了其他不应该由它负责的事情。更新后的onBindViewHolder应该是这样的:
// 单一功能原则 - 修改示例
public class MovieRecyclerAdapter extends RecyclerView.Adapter<MovieRecyclerAdapter.ViewHolder> {
private List<Movie> movies;
private int itemLayout;
public MovieRecyclerAdapter(List<Movie> movies, int itemLayout)
{
this.movies = movies;
this.itemLayout = itemLayout;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
{
View v = LayoutInflater.from(parent.getContext())
.inflate(itemLayout, parent, false);
return new ViewHolder(v);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
final Movie movie = movies.get(position);
holder.itemView.setTag(movie);
holder.title.setText(movie.getTitle());
holder.rating.setText(movie.getRating());
holder.genre.setText(
ArraysUtil.convertArrayListToString(movie.getGenre()));
holder.releaseYear.setText(movie.getYear());
Glide.with(holder.thumbNail.getContext())
.load(movies.get(position)
.getThumbnailUrl())
.into(holder.thumbNail);
}
@Override
public int getItemCount() {
return movies.size();
}
public static class ViewHolder extends RecyclerView.ViewHolder {
@Bind(R.id.title) TextView title;
@Bind(R.id.rating) TextView rating;
@Bind(R.id.genre) TextView genre;
@Bind(R.id.releaseYear) TextView releaseYear;
@Bind(R.id.thumbnail) ImageView thumbNail;
public ViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
}
}
}
正如Bob大叔所言:
在单一功能原则(SRP)的上下文中,我们将功能定义为“改变的理由”。如果你在修改一个类的时候有多个动机,那么这个类就有多个功能。
SOLID#2:开闭原则(OCP)
软件实体(类,模块,函数等等)应该对扩展开放,但是对修改封闭。
这里我们总的讨论一下当需要一个新的功能时,我们应当怎样设计我们的模块、类和函数,我们不能修改已有的代码,而是新添加代码供现有的代码使用。我们来看一个例子:
// 违反开闭原则
// Rectangle.java
public class Rectangle {
private double length;
private double height;
// getters/setters ...
}
// Circle.java
public class Circle {
private double radius;
// getters/setters ...
}
// AreaFactory.java
public class AreaFactory {
public double calculateArea(ArrayList<Object>... shapes) {
double area = 0;
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类。所以这里违反了开闭原则。它既没有对修改封闭也没有对扩展开放。这个实现真的很糟糕,我们来重构一下:
// 开闭原则: 好的示例
// Shape.java
public interface Shape {
double getArea();
}
// Rectangle.java
public class Rectangle implements Shape{
private double length;
private double height;
// getters/setters ...
@Override
public double getArea() {
return (length * height);
}
}
// Circle.java
public class Circle implements Shape{
private double radius;
// getters/setters ...
@Override
public double getArea() {
return (radius * radius * Math.PI);
}
}
// AreaFactory.java
public class AreaFactory {
public double calculateArea(ArrayList<Shape>... shapes) {
double area = 0;
for (Shape shape : shapes) {
area += shape.getArea();
}
return area;
}
}
现在,如果我们需要添加一个新的形状,ArearFactory就不需要做任何修改,因为它通过Shape接口开放扩展。
SOLID#3:里氏替换原则(LSP)
子类决不允许破坏父类的类型定义。
很简单,子类在重载父类的方法时,不能破坏父类已经定义好的功能。这里有一个简单的例子可以说明这个概念:
// 违反里氏替换原则
// Car.java
public interface Car {
public void startEngine();
}
// Ferrari.java
public Ferrari implements Car {
...
@Override
public double startEngine() {
//logic ...
}
}
// Tesla.java
public Tesla implements Car{
...
@Override
public double startEngine() {
if (!IsCharged)
return;
//logic ...
}
}
// Make the call
public void letStartEngine(Car car) {
car.startEngine();
}
如你所见,在上面这段代码中,有两种类型的汽车。一种是燃油汽车,另一种是电动汽车。电动汽车只有在带电是才能启动引擎。如果一辆电动不带电那么LetStartEngine函数就不能正常运行。这违反了LSP原则,因为它需要带电才能启动,但是IsCharged(这同样是合同的一部分)不能在基类中设置。
为了解决这个问题,你可以这样做:
// Make the call
public void LetStartEngine(Car car) {
if (car instanceof Tesla)
((Tesla)car).TurnOnCar();
car.startEngine();
}
但是这样违反了开闭原则,所以合适的方式是在StartEngine函数中自动开启汽车,像这样:
// Fix of Liskov's Substitution principle
public interface Car {
public void startEngine();
}
// Ferrari.java
public Ferrari implements Car {
...
@Override
public double startEngine() {
//logic ...
}
}
// Tesla.java
public Tesla implements Car{
...
@Override
public double startEngine() {
if (!IsCharged)
TurnOnCar();
//logic ...
}
}
// Make the call
public void letStartEngine(Car car) {
car.startEngine();
}
SOLID#4:接口分离原则(ISP)
接口分离原则的观点就是客户端不应该依赖于一些它所不需要的方法。
该原则指出一旦一个接口变得很臃肿,那么就有必要将它拆分成更小的接口,这样客户端只需要了解和它相关的方法。你知道的,Android中View类是所有Android view的根基类(root superclass)。如Button,它的根基类是View。我们一起来看看这段代码:
public interface OnClickListener {
void onClick(View v);
void onLongClick(View v);
void onTouch(View v, MotionEvent event);
}
如你所见,这个接口包含了3个不同的方法,假设我们想捕获一个button上的点击(click):
// Violation of Interface segregation principle
Button valid = (Button)findViewById(R.id.valid);
valid.setOnClickListener(new View.OnClickListener {
public void onClick(View v) {
// TODO: do some stuff...
}
public void onLongClick(View v) {
// we don't need to it
}
public void onTouch(View v, MotionEvent event) {
// we don't need to it
}
});
这个接口过于臃肿,因为即使你不需要它们你也必须实现所有的方法。我们尝试使ISP来修复这个问题:
// Fix of Interface Segregation principle
public interface OnClickListener {
void onClick(View v);
}
public interface OnLongClickListener {
void onLongClick(View v);
}
public interface OnTouchListener {
void onTouch(View v, MotionEvent event);
}
现在我们就可以使用单个接口而不必实现一些乱七八糟的方法。
SOLID#5:依赖反转规则(DIP)
1、上层的模块不应该依赖于底层的模块。他们都应该依赖于抽象。
2、抽象不取决于具体的实现,具体的实现取决于抽象。
我们一起来看一段代码。我们的许多代码都是这样写的:
// violation of Dependency's inversion principle
// Program.java
class Program {
public void work() {
// ....code
}
}
// Engineer.java
class Engineer{
Program program;
public void setProgram(Program p) {
program = p;
}
public void manage() {
program.work();
}
}
上面这段代码的问题在于违反了依赖反转规则;即上面的第一条:上层模块不应该依赖于底层模块,两者都应该依赖于抽象。这里我们的类Engineer就是上层类,底层的类就是Program。
我们假设Engineer类非常复杂,包含了非常复杂的逻辑。现在我们想引入新的类SuperProgram。我们一起来看一下有什么缺点:
-
我们必须修改Engineer类(记住这个类非常复杂,并且这些修改需要付出时间和精力)。
-
现有Engineer类的某些功能可能会收到影响。
-
重新进行单元测试。
// Dependency Inversion Principle - Good example interface IProgram { public void work(); } class Program implements IProgram{ public void work() { // ....code } } class SuperProgram implements IProgram{ public void work() { //....code } } class Engineer{ IProgram program; public void setProgram(IProgram p) { program = p; } public void manage() { program.work(); } }
在新的设计中,我们通过IProgram接口添加了抽象层。现在上面提到的所有问题都得到解决(注意在上层的逻辑中没有做任何修改):
- 添加SUperProgram时Engineer类不需要做修改。
- 将对Engineer功能影响的风险降到最低,因为我们没有修改它。
- 不需要对Engineer类重新做单元测试。
作为结论,我们可以说单一功能原则是关于角色和高层设计。 开放/封闭原则是关于类的设计和功能扩展。 里氏替换原则是关于子类型和继承。 接口分离原则(ISP)是关于业务逻辑和客户端之间的通信。 而依赖反转原则(DIP)则是关于领导或帮助我们尊重所有其他原则。