代码整洁之道-对象和数据结构

代码整洁之道

第 6 章 对象和数据结构

6.2 数据、对象的反对称性

public class Square {
    public Point topLeft;
    public double side;
}

public class Rectangle {
    public Point topLeft;
    public double height;
    public double width;
}

public class Circle {
    public Point center;
    public double radius;
}

public class Geometry {
    public final double PI = 3.141592653589793;
    
    public double area(Object shape) throws NoSuchShapeException {
        if (shape instanceof Square) {
            Square s = (Square)shape;
            return s.side * s.side;
        }
        else if (shape instanceof Rectangle) {
            Rectangle r = (Rectangle)shape;
            return r.height * r.width;
        }
        else if (shape instanceof Circle) {
            Circle c = (Circle)shape;
            return PI * c.radius * c.radius;
        }
        throw new NoSuchShapeException();
    }
}
public class Square implements Shape {
    private Point topLeft;
    private double side;
    
    public double area() {
        return side * side;
    }
}

public class Rectangle implements Shape {
    private Point topLeft;
    private double height;
    private double width;
    
    public double area() {
        return height * width;
    }
}

public class Circle implements Shape {
    private Point center;
    private double radius;
    public final double PI = 3.141592653589793;
    
    public double area() {
        return PI * radius * radius;
    }
}

对象把数据隐藏于抽象之后,暴露操作数据的函数。
数据结构暴露其数据,没有提供有意义的函数。

添加一个perimeter()函数会怎样?

添加一个新形状呢?

对象和数据结构之间的二分原理:

过程式代码(使用数据结构的代码)便于在不改动既有数据结构的前提下添加新函数。面向对象代码便于在不改动既有函数的前提下添加新类。

反过来讲也说得通:

过程式代码难以添加新的数据结构,因为必须修改所有函数。面向对象代码难以添加新函数,因为必须修改所有类。

在任何系统中,我们有时会希望能够灵活的添加新数据类型,所以更喜欢在这部分使用对象。另外一些时候,我们希望能灵活的添加新行为,这时我们更喜欢使用数据结构和过程。

6.4 数据传送对象

最为精炼的数据结构,是一个只有公共变量、没有函数的类。这种数据结构有时被称为数据传送对象,或DTO(Data Transfer Objects)DTO是非常有用的结构,尤其是在与数据库通信、或解析套接字传递的消息之类场景中。

更常见的是“豆”(bean)结构。豆结构拥有由赋值器和取值器操作的私有变量。

对豆结构的半封装会让某些OO纯化论者感觉舒服些,不过通常没有其他好处。

public class Address {
    private String street;
    private String streetExtra;
    private String city;
    private String state;
    private String zip;
    
    public Address(String street, String streetExtra, 
                   String city, String state, String zip) {
        this.street = street;
        this.streetExtra = streetExtra;
        this.city = city;
        this.state = state;
        this.zip = zip;
    }
    
    public String getStreet() {
        return street;
    }
    
    public String getStreetExtra() {
        return streetExtra;
    }
    
    public String getCity() {
        return city;
    }
    
    public String getState() {
        return state;
    }
    
    public String getZip() {
        return zip;
    }
}

Java的访问级别修饰符

作用域 当前类 同一package 子孙类 其他package
public
protected ×
default × ×
private × × ×

C#访问级别修饰符

访问修饰符 访问级别
public 任何地方都可以访问
protected internal 同时具备protected与internal的访问权限
internal 当前程序集内部可以访问
protected 当前类内部及其子类都可以访问
private 只能在当前类内部访问

将变量设置为私有(private)
不想其他人依赖这些变量,随时修改其类型或实现。

6.1 数据抽象

以下两段代码都表示笛卡尔平面上的一个点。

public class Point {
    public double x;
    public double y;
}
public interface Point {
    double getX();
    double getY();
    void setCartesian(double x, double y);
    double getR();
    double getTheta();
    void setPolar(double r, double theta);
}

而第一段代码很明确的是在矩形坐标系中实现。我们可以单个操作那些坐标,暴露了实现;

我们不知道第二段代码的实现会是在矩形坐标系还是极坐标系中。它呈现的不止是一个数据结构,还固定了一套存取策略,我们可以单独读取某个坐标,但必须通过一次原子操作设定所有坐标。

即便通过变量取值器和赋值器使用私有变量,其实现仍然暴露了。

隐藏实现并非只是在变量之间放上一个函数层那么简单。隐藏实现关乎抽象。类并不简单的用取值器和赋值器将其变量推向外间,而是暴露抽象接口,以便用户无需了解数据的实现就能操作数据本体。

public interface Vehicle {
    double getFuelTankCapacityInGallons();
    double getGallonsOfGasoline();
}
public interface Vehicle {
    double getPercentFuelRemaining();
}

前者使用具象手段与机动车的燃料层通信,后者采用百分比抽象。

你能确定前者里面都是些变量存取器,却无法得知后者中的数据形态。

不应暴露数据细节,应以抽象形态表述数据。这并不只是用接口或者赋值器、取值器就万事大吉。要做严肃的思考,以最好的方式呈现某个对象包含的数据。

6.3 得墨忒耳律(迪米特法则)(The Law of Demeter)

模块不应了解它所操作对象的内部情形。

得墨忒耳率认为,类 C 的方法 f 只应该调用以下对象的方法:

  • C
  • 由 f 创建的对象
  • 作为参数传递给 f 的对象
  • 由 C 的实体变量持有的对象

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

final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();

这类代码常被称作火车失事,因为它看起来就像是一列火车。不要为了省一两个变量采用这种方式,起一个有意义的变量名,不仅利于阅读代码,还便于调试。最好做类似如下的切分:

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

如果ctxt是个对象,就应该要求它做点什么,而不是要求它给出内部情形。

那我们为何还要得到临时目录的绝对路径呢?

来看看同一模块(许多行之后)的这段代码:

String outFile = outputDir + "/" + className.replace('.','/') + ".class";
FileOutputStream fout = new FileOutputStream(outFile);
BufferedOutputStream bos = new BufferedOutputStream(fout);

我们发现,取得临时目录绝对路径的初衷是为了创建指定名称的临时文件。

所以,直接让ctxt对象来做这事如何?

BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName);

ctxt隐藏了其内部结构,防止当前函数因浏览它不该知道的对象而违反得墨忒耳律。

人可以命令一条狗行走,但是不应该直接指挥狗的腿行走,应该由狗去指挥控制它的腿如何行走。

一个对象应该对其他对象有最少的了解,所以又叫做最少知识原则(Least Knowledge Principle, LKP)。

降低类之间的耦合。由于每个对象尽量减少对其他对象的了解,因此,很容易使系统的功能模块独立,相互之间不存在(或很少存在)依赖关系。

  1. 只和直接的朋友交流(从依赖者的角度来说,只依赖应该依赖的对象)
  2. 减少对朋友的了解(从被依赖者的角度来说:只暴露应该暴露的方法或者属性,即在编写相关的类的时候确定方法/属性的权限)

关闭计算机

当我们按下计算机的关机按钮的时候,计算机会执行一系列的动作:保存未完成的任务、关闭服务、关闭显示器、关闭电源等等。

public class System {
    public void saveTask() {
        
    }
    
    public void closeService() {
        
    }
    
    public void closeScreen() {
        
    }
    
    public void closePower() {
        
    }
    
    public void close() {
        saveTask();
        closeService();
        closeScreen();
        closePower();
    }
}

public class Container {
    private System system;
    
    public System getSystem() {
        return system;
    }
}

public class Person {
    private Container container;
    
    public void clickCloseButton() {
        System system = container.getSystem();
        
        system.closePower();
    }
}
public class System {
    private void saveTask() {
        
    }
    
    private void closeService() {
        
    }
    
    private void closeScreen() {
        
    }
    
    private void closePower() {
        
    }
    
    public void close() {
        saveTask();
        closeService();
        closeScreen();
        closePower();
    }
}

public class Container {
    private System system;
    
    public void sendCloseCommand() {
        system.close();
    }
}

public class Person {
    private Container container;
    
    public void clickCloseButton() {        
        container.sendCloseCommand();
    }
}

迪米特法则的核心观念就是类间解耦,弱耦合。只有弱耦合了之后,类的复用才可以提高,类变更的风险才可以减低。但解耦是有限度的,不存在没有耦合的系统。所以在实际项目中,需要适度地参考这个原则,避免过犹不及。

原文地址:https://www.cnblogs.com/fanful/p/13364379.html