# Clean Code 6 - 物件及資料結構 ## 資料抽象化 具體的座標點 ```ruby class Point attr_accessor :x, :y end ``` 抽象的座標點 ```ruby class Point attr_reader :x, :y, :r, :theta def set_cartesian(x, y) # ... end def setPolar(r, theta) # ... end end ``` 差異 | 具體的座標點 | 抽象的座標點 | | ------------------------------------------------------------ | ------------------------------------------------- | | 從介面就可以看出資料結構 | 從介面看不出資料結構 | | 可單獨設定 x 或 y (這邊的 point 可以看做是一個值,x 跟 y 應該要一起設定較好) | 設定座標點時,限制一定要同時設定 x, y 或 r, theta | | 暴露了程式的實現過程 | 隱藏實作 | ## 資料/物件的反對稱性 | 物件 | 資料 | | ------------------------------------------------ | ------------------------------------------------------------ | | 資料被隱藏起來,需要透過物件提供的介面去操作資料 | 將資料暴露在外,可以直接讀取或寫入資料。譬如 Hash 或是上面的「具體的座標點」 | | 包含行為 | 不包含行為 | **Procedural Shape** ```java 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(); } } ``` **Polymorphic Shape** ```java 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; } } ``` | | Procedural Shape | Polymorphic Shape | | --------------------------------- | ------------------------------------------ | ------------------------------- | | 新增 function,譬如計算 perimeter | 只需在 Geometry 裡面新增一個 function 即可 | 需要在所有類別新增這個 function | | 新增類別,譬如 Triangle | Geometry 裡面所有的 function 都需要修改 | 新增一個類別即可 | | | 方便新增函式 | 方便新增類別 | ## The Law of Demeter the Law of Demeter says that a method `f` of a class `C` should only call the methods of these: - `C` - An object created by `f` - An object passed as an argument to `f` - An object held in an instance variable of `C` ```ruby class User def function_a(company) # ok function_b # ok service = MailService.new service.deliver # ok company.do_somthing # ok @instance_variable.do_something # ok if get_options returns data # not ok if get_options returns object company.get_options.get_scratch_dir end def function_b # ... end end ``` ## 隱藏結構 火車事故章節的程式碼 ```java Options opts = ctxt.getOptions(); File scratchDir = opts.getScratchDir(); final String outputDir = scratchDir.getAbsolutePath(); ``` 當有程式違反 LOD 時,應該要停下來思考,為什麼這邊需要知道這麼多資訊? 當我們再往下看程式,發現拿絕對路徑是為了「要在此目錄下產生一個給定名稱的檔案」 ``` String outFile = outputDir + "/" + className.replace('.', '/') + ".class"; FileOutputStream fout = new FileOutputStream(outFile); BufferedOutputStream bos = new BufferedOutputStream(fout); ``` 因此,我們可以把「產生檔案」這件事情丟給 ctxt 做(因為 ctxt 知道絕對路徑是什麼) 就可以遵循 LOD,ctxt 也可以隱藏內部結構 BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName) ## 資料傳輸物件 (Data Transfer Object, DTO) 一個類別裡面只有公用變數,沒有任何函式。 ruby 的 Struct? ## 活動記錄 (Active Records) 通常擁有 save 與 find 等方法。 通常是由資料庫或其他資料來源轉換而來。 不幸的是,我們常發現,開發者會試著將這類的資料結構看做是物件,然後加入處理商業法則的方法(咦!!? ## Value Object (補充) [Rails value object design pattern (longliveruby.com)](https://longliveruby.com/articles/rails-value-object-design-pattern)