[关闭]
@DevWiki 2015-07-08T00:43:37.000000Z 字数 3639 阅读 1213

CleanCode笔记---对象和数据结构

CleanCode


更多内容详见:CleanCode笔记

CleanCode

数据抽象

先看两段代码,都是表示笛卡尔坐标系的一个点.

代码一:

  1. public class Point{
  2. public double x;
  3. public double y;
  4. }

代码二:

  1. public interface Point{
  2. double getX();
  3. double getY();
  4. void setCartesian(double x, double y);
  5. double getR();
  6. double getTheta();
  7. void setPolar(double r, double theta);
  8. }

你觉得哪个代码更好些?是不是代码二?

首先,代码一没有封装!

代码一中的x和y是完全暴露的,任何都可以直接访问和设置新的值.
代码二中加了一层封装,你只能通过get方法获取坐标值和set方法设置原子坐标值.

其次,具象与抽象

代码一是具象的一个点,只表示在直角坐标系的一个点.
代码二是抽象的一个点,可以表示直角坐标系一个点,也可表示极坐标系中的一个点.

但是,两个都明确了一种数据结构


隐藏实现并非是在变量之上添加一个函数层那么简单,隐藏关乎抽象.

比如,我们现在需要知道手机还有多少电量.

代码三:

  1. public interface Battery{
  2. //获取以mAh为单位的总电量,
  3. int getTotalInmAh();
  4. //获取以mAh为单位的剩余电量
  5. int getRemainingInmAh();
  6. }

代码四

  1. public interface Battery{
  2. //获取剩余电量的百分比
  3. double getRemainingPrecent();
  4. }

以上两端代码,代码四最好.我们不愿意透露更多的细节,更愿意以抽象的形态表述数据.这并不只是用接口和赋值器取值器就能做好的事情.要以更好的方式呈现对象所包含的数据.

数据,对象的反对称性

从学java开始,书上就告诉我们java是面向对象语言.那么什么是面向对象什么是面向过程?

我们来看看下面的代码:
我们有正方形,长方形和圆形三种几何图形,最后要求出其面积.
代码一:

  1. public class Square{
  2. public Point topLeft;
  3. public double side;
  4. }
  5. public class Rectangle{
  6. public Point topLeft;
  7. public double width;
  8. public double height;
  9. }
  10. public class Circle{
  11. public Point center;
  12. public double radius;
  13. }
  14. public class Geometry{
  15. public final double PI = 3.14159265358;
  16. public double area(Object shape) throws NoSuchShapeException{
  17. if(shape instanceof Square){
  18. Square square = (Square)shape;
  19. return square.side * square.side;
  20. } else if (shape instanceof Rectangle){
  21. Rectangle rec = (Rectangle)shape;
  22. return rec.width * rec.hight;
  23. } else if (shape instanceof Circle){
  24. Circle cir = (Circle)shape;
  25. return PI * cir.radius * cir.radius;
  26. }
  27. throw new NoSuchShapeException();
  28. }
  29. }

代码二

  1. public interface Shape{
  2. double area();
  3. }
  4. public class Square implements Shape{
  5. private Point topLeft;
  6. private double side;
  7. public double area(){
  8. return side*side;
  9. }
  10. }
  11. public class Rectangle implements Shape{
  12. private Point topLeft;
  13. private double width;
  14. private double height;
  15. public double area(){
  16. return width * height;
  17. }
  18. }
  19. public class Circle implements Shape{
  20. private static final double PI = 3.14159265358;
  21. private Point topLeft;
  22. private double radius;
  23. public double area{
  24. return PI * radius * radius;
  25. }
  26. }

从上面两端代码中我们可以看出,代码一每一个形状只负责存储数据,不具备行为.具体的计算行为放在了Geometry类中.每一个形状都是如何,形状之间没有任何联系.

而代码二中的三个类同属于集合图形Shape类,每种图形都有各自的计算面积的方法.这才是符合人类的世界观.

但是!!!
如果在代码一中添加计算周长的函数,每个形状都不会受到任何影响.而代码二中添加计算周长的函数,每一个函数都要有所变动.
如果在代码一种添加一个新的形状,既有的形状(数据结构)不会收到影响,但是计算的函数会收到影响.而在代码二中只需要添加一个新类而已,既有的类不会有任何影响.

过程式的代码便于在不改动既有代码数据结构的前提下添加新的函数,而面向对象式的代码便于在不改动既有 函数的前提下添加新类.
反过来讲也说的通:过程式的代码难以添加新的数据结构,因为必须修改所有的函数.而面向对象代码难以添加新函数,因为必须修改所有的类.

所以:

面向对象较难的事情,对于过程式代码却叫较容易,反之亦然.

这就是数据和对象的反对称性.

得墨忒耳定律

著名的得墨忒耳定律认为:模块不应该了解它所操作对象的内部的情形.

即类C的方法f只应该调用以下对象的方法:

方法不应该调用任何函数返回的对象的方法.换言之,只跟朋友交谈,不与陌生人谈话.

以下代码明显违反了得墨忒耳定律:

  1. final String outputDir = ctxt.getOptions().getScartchDir().getAbsolutePath();

这类代码常被作为火车失事,因为它就像一列火车,一旦出了问题,不知问题出在哪.最好做类如下的切分:

  1. Options opts = ctxt.getOptions();
  2. File scratchDir = opts.getScratchDir();
  3. final String outputDir = scratchDir.getAbsolutePath();

以上代码是否违反得墨忒耳定律呢?
模块知道ctxt对象包含有多个选项,每个选项都有一个临时目录,而每个临时目录都有一个绝对路径.对于一个函数来讲,这些知识真是太丰富了.
这些代码是否违反得墨忒耳定律,就要看ctxt,Options,ScratchDir是对象还是数据结构.如果是对象,就违反了定律.如果是数据结构就没有违反.

如果是数据结构,就应该这样写代码:

  1. final String outputDir = ctxt.options.scratchDir.absolutePath;

数据传送对象

最精简的数据结构是一个只有公共变量,没有函数的类.这种数据结构有时被称为数据传送对象,或DTO(Data Transfer Objects).DTO是非常有用的结构,尤其是在与数据库通信,或解析套接字传递信息之类的场景之中.在应用程序代码里一系列将原始数据转换为数据的翻译过程中,他们往往是排头兵.

比如我们也曾写过这样的类:

  1. public class Address{
  2. private String street;
  3. private String city;
  4. private String state;
  5. private String province;
  6. public Address(String street, String city, String state, String province){
  7. this.street = street;
  8. this.city = city;
  9. this.state = state;
  10. this.province = province;
  11. }
  12. public String getStreet(){
  13. return stree;
  14. }
  15. public String getCity(){
  16. return city;
  17. }
  18. public String getState(){
  19. return state;
  20. }
  21. public String getProvince(){
  22. return province;
  23. }
  24. }

为什么要写这些get方法???为什么不直接将这些变量置为公共的呢?

小结

对象暴露行为,封装数据.便于添加新对象类型而无需修改既有的行为,同时也难以在既有的对象中添加新的行为.数据结构暴露数据,没有明显的行为,便于向既有的数据结构添加新的行为,同时也难以向既有的函数中添加新的数据结构.

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注