「物件将它们的资料隐藏在抽象层後方,然後将操纵这些资料的函式暴露在外。资料结构则将资料暴露在外,且未提供有意义的函式」
「它们不仅是对立的,且本质上也是互补的」
取自: Clean Code (p.107)
我们看一个简单的例子:
public interface Vehicle
{
public double GetGallonsOfGasoline();
}
// vs.
public interface Vehicle
{
public double GetPercentFuelRemaining();
}
上述的例子中,後者隐藏了程序的实作细节,利用更为抽象化的词汇 "PercentFuel (燃料百分比)" 取代 "GallonsOfGasoline (加仑汽油)"
如果从上述例子还无法深刻体会到封装所带来的好处,不彷再思考一个问题: 你会喜欢看到手机剩下的电量百分比(%),或是看到毫安培值(mAh)?
再看一个经典例子:
public class 正方形 {
...
}
public class 长方形 {
...
}
public class 圆形 {
...
}
// Procedural Programming
public class 几何图形 {
public const double PI = 3.14;
// 求面积
public double getArea(Object shapre) {
if (shape instanceof 正方形) {
var s = (正方形) shape;
return s.side * s.side;
}
else if (shape instanceof 长方形) {
var r = (长方形) shape;
return r.height * r.width;
}
else if (shape instanceof 圆形) {
var c = (圆形) shape;
return PI * c.radius * c.radius;
}
}
}
上述写法为 结构化(Procedural) 导向的程序设计方式
思考1: 当我们想新增 getPerimeter(Object shape)
函式来求周长时,会影响什麽?
思考2: 当我们新增了一个图形类别(e.g., 三角形),会影响什麽?
现在我们采用 物件导向(OOP) 的程序设计方式改写上述例子
public class 正方形 implements 几何图形 {
public double getArea();
}
public class 长方形 implements 几何图形 {
public double getArea();
}
public class 圆形 implements 几何图形 {
public const double PI = 3.14;
public double getArea();
}
// Object-Oriented Programming
public abstract class 几何图形 {
public double getArea();
}
这是物件导向的写法,这里的 getArea() 方法是多型(Polymorphism)的
思考1: 当我们想新增 getPerimeter(Object shape)
函式来求周长时,会影响什麽?
思考2: 当我们新增了一个图形类别(e.g., 三角形),会影响什麽?
谨记开头的定义,资料和物件不仅是对立、更是互补的
「因此,使用物件导向而感到困难的事物,在结构化里却比较容易;反之亦然」
「模组不该知道『关於它所操纵物件的内部运作』」
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
很明显地,outputDir 知道 ctxt 物件含有 options, 而 options 又包含 absolutePath。这里有太多资讯被函式预先知道了Options opts = ctxt.getOptions();
File scratchDir = opts.getScratchDir();
final String outputDir = scratchDir.getAbsolutePath();
「也代表作者根本不确定,它们是否需要函式或型态的保护」
「最佳的资料结构形式,是一个类别里只有公用变数,没有任何函式」
此类资料结构通常称为 资料传输物件 (DTO)
补充: [2] 除了 DTO 之外,还有 BO、DAO、VO...等不同的资料传输物件,是很好的解耦方式。但使用上可能要注意过度设计(Over-Design)的议题 (e.g., 前端想多一个 Input 栏位存进 DB,这中间的过程至少需要经过 3 层的资料传输物件...)
「优秀的软件开发者能理解其个中原因,在不带有偏颇的情况下,选择最适合的方法来完成手中的工作」
取自: Clean Code (p.114)
「有时候我们买下第三方软件套件,或使用开放原始码套件。不管为了哪种原因,我们都必须将这些外来的程序码整洁地整合到我们的程序码中」
取自: Clean Code (p.127)
接下来以 java.util.Map 作为例子,这是一个提供 Hash 资料结构相关功能的介面
假设我们需要将 Sensor (感测器) 存放进 HashMap 的资料结构中,最简单的写法如下
// 宣告
Map sensors = new HashMap()
// 其他程序需要存取时...
Sensor s = (Sensor) sensors.get(sensorId);
上述写法的问题是,客户端 (Client) 程序必须负责把来自 Map 介面里的物件,手动转型成正确的资料型态。这并不是整洁好读的程序码
接着我们透过泛型(Generics),改写成
// 宣告
Map<Sensor> sensors = new HashMap<Sensor>()
// 其他程序需要存取时...
Sensor s = sensors.get(sensorId);
上述写法的可读性有显着改善,但仍然有个问题:Map 介面改变时,系统里会有很多地方也需要连带修正
使用 Map 更整洁的作法如下
public class Sensors
{
private Map _sensors = new HashMap()
public Sensor getById(string id)
{
return (Sensor) sensors.get(id);
}
// ...
}
使用者无需关心实作细节是否使用泛型,因为边界上的介面 (Map) 被封装了。转型和型态管理都在 Sensors 类别内部处理了
「避免在公用 API 里回传介面,或将介面当作参数传递给 API」
「在边界的程序码必须能清楚的分割,并定义预期的测试。避免让我们的程序过度使用第三方软件的特殊之处。最好是依靠 (Depend) 在你可以控制的程序上,免得最後反倒受它控制」
「只在最少处引用第三方软件」
取自: Clean Code (p.135)
本章主要在说明该如何与第三方套件解耦,避免第三方软件的改变影响到本身的系统。相关概念也可以衍生到团队协作时,透过边界来切分已知和未知 (Adapter Pattern)。之後在 Clean Architecture 篇会从软件架构层面探讨「边界」
前言: 今天我们要来介绍React里很强大的一个工具!没错就是Style Components!废...
ShellExView 今天来认识的小工具是看 Shell 的(猜测 ShellExView She...
这系列的文章不会讲完全部 KSP 的实作,毕竟我也还正在实作中,不过实作的方向应该是跟前几篇讲的差不...
但市面上的裸机Hyperviser还有其他选择(ESXI, Proxmox VE…),为何独锺unR...
讲到硬体就会用到权限控制,然後一定会用onActivityResult和startActivityF...