在開發網絡應用時,創建模型是相當重要的一步。隨着建模工具軟件的發展,目前愈來愈多的軟件採用雙向相關模型建模,本文主要討論的是雙向相關模型與傳統單向模型相比的優點。javascript
雙向相關模型和單向模型相比主要的區別在於其動態的描述了模型之間的交互關係:傳統單項模型僅僅關注當前模型的狀態變化,並不馬上考慮自身狀態變化後對環境的影響;而雙向相關模型則在當前模型狀態的每一次變化以後,都馬上通知被此變化影響到的其餘模型也要改變,即每一次操做中咱們改變的並不是是單個模型而是整個環境,由於這些模型之間都是由一對多或多對一關係關聯起來的。java
最方便生成雙向相關模型的方法是藉助於 PowerDesigner 工具。例如,若是咱們須要對這樣一個場景進行建模:人(person)和角色(role),一我的能夠擁有多個角色,同時多我的也能夠擁有相同的角色。這是一個典型多對多關係,那麼使用 PowerDesigner 建模的結果可以下所示:git
(Person 模型擁有主鍵 id 和名稱 name,Role 模型擁有主鍵 id 和描述 description,PersonRole 模型擁有主鍵 id 和兩個外鍵 personId 及 roleId,其中 personId 和 Person 的主鍵 id 關聯,roleId 和 Role 的主鍵 id 關聯。)算法
將以上模型生成 Java 代碼後,咱們將獲得 Person、Role、PersonRole 三個類,其中 PersonRole 用來描述 Person 和 Role 的對應關係,咱們能夠稱 Person 和 Role 是多對多關係。由於在現實中咱們的對象都是從數據庫中裝載獲得,爲了讓這些在內存中地址不一樣的對象能夠得到業務邏輯上的「相等」或「不等」,咱們經過使用重定義模型對象的 hashCode()、equals() 以及使用 java.util.LinkedHashSet 做爲集合實現形式的方法來實現(若是業務不關心集合中對象的順序,可使用 java.util.HashSet 方式替代 java.util.LinkedHashSet ,這樣能夠進一步提高性能),咱們把以上特性包裝到一個泛型類 PojoSupport 中並做爲全部模型對象的基類,最終獲得的代碼爲(如下代碼是在 PowerDesigner 自動生成代碼上作簡單修改所得):數據庫
package myPackage; public abstract class PojoSupport<T extends PojoSupport<T>> { abstract public Object getId(); @Override /* 從新定義hash方法 */ public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((getId() == null) ? 0 : getId().hashCode()); return result; } @Override /* 從新定義equals方法 */ public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; PojoSupport<?> other = (PojoSupport<?>) obj; if (getId() == null) { return false; } else if (!getId().equals(other.getId())) return false; return true; } }
package myPackage; public class Person extends PojoSupport<Person> { public Person(Integer id, String name) { this.id = id; this.name = name; } private Integer id; private java.lang.String name; private java.util.Collection<PersonRole> personRole; public java.util.Collection<PersonRole> getPersonRole() { if (personRole == null) { personRole = new java.util.LinkedHashSet<PersonRole>(); } return personRole; } public java.util.Iterator<PersonRole> getIteratorPersonRole() { if (personRole == null) { personRole = new java.util.LinkedHashSet<PersonRole>(); } return personRole.iterator(); } public void setPersonRole(java.util.Collection<PersonRole> newPersonRole) { removeAllPersonRole(); for (java.util.Iterator<PersonRole> iter = newPersonRole.iterator(); iter.hasNext();) { addPersonRole((PersonRole) iter.next()); } } public void addPersonRole(PersonRole newPersonRole) { if (newPersonRole == null) { return; } if (this.personRole == null) { this.personRole = new java.util.LinkedHashSet<PersonRole>(); } if (!this.personRole.contains(newPersonRole)) { this.personRole.add(newPersonRole); newPersonRole.setPerson(this); } else { for (PersonRole temp : this.personRole) { if (newPersonRole.equals(temp)) { if (temp != newPersonRole) { removePersonRole(temp); this.personRole.add(newPersonRole); newPersonRole.setPerson(this); } break; } } } } public void removePersonRole(PersonRole oldPersonRole) { if (oldPersonRole == null) { return; } if (this.personRole != null) { if (this.personRole.contains(oldPersonRole)) { for (PersonRole temp : this.personRole) { if (oldPersonRole.equals(temp)) { if (temp != oldPersonRole) { temp.setPerson((Person) null); } break; } } this.personRole.remove(oldPersonRole); oldPersonRole.setPerson((Person) null); } } } public void removeAllPersonRole() { if (personRole != null) { PersonRole oldPersonRole; for (java.util.Iterator<PersonRole> iter = getIteratorPersonRole(); iter.hasNext();) { oldPersonRole = (PersonRole) iter.next(); iter.remove(); oldPersonRole.setPerson((Person) null); } personRole.clear(); } } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public java.lang.String getName() { return name; } public void setName(java.lang.String name) { this.name = name; } }
package myPackage; public class Role extends PojoSupport<Role> { public Role(Integer id, String description) { this.id = id; this.description = description; } private Integer id; private java.lang.String description; private java.util.Collection<PersonRole> personRole; public java.util.Collection<PersonRole> getPersonRole() { if (personRole == null) { personRole = new java.util.LinkedHashSet<PersonRole>(); } return personRole; } public java.util.Iterator<PersonRole> getIteratorPersonRole() { if (personRole == null) { personRole = new java.util.LinkedHashSet<PersonRole>(); } return personRole.iterator(); } public void setPersonRole(java.util.Collection<PersonRole> newPersonRole) { removeAllPersonRole(); for (java.util.Iterator<PersonRole> iter = newPersonRole.iterator(); iter.hasNext();) { addPersonRole((PersonRole) iter.next()); } } public void addPersonRole(PersonRole newPersonRole) { if (newPersonRole == null) { return; } if (this.personRole == null) { this.personRole = new java.util.LinkedHashSet<PersonRole>(); } if (!this.personRole.contains(newPersonRole)) { this.personRole.add(newPersonRole); newPersonRole.setRole(this); } else { for (PersonRole temp : this.personRole) { if (newPersonRole.equals(temp)) { if (temp != newPersonRole) { removePersonRole(temp); this.personRole.add(newPersonRole); newPersonRole.setRole(this); } break; } } } } public void removePersonRole(PersonRole oldPersonRole) { if (oldPersonRole == null) { return; } if (this.personRole != null) { if (this.personRole.contains(oldPersonRole)) { for (PersonRole temp : this.personRole) { if (oldPersonRole.equals(temp)) { if (temp != oldPersonRole) { temp.setRole((Role) null); } break; } } this.personRole.remove(oldPersonRole); oldPersonRole.setRole((Role) null); } } } public void removeAllPersonRole() { if (personRole != null) { PersonRole oldPersonRole; for (java.util.Iterator<PersonRole> iter = getIteratorPersonRole(); iter.hasNext();) { oldPersonRole = (PersonRole) iter.next(); iter.remove(); oldPersonRole.setRole((Role) null); } personRole.clear(); } } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public java.lang.String getDescription() { return description; } public void setDescription(java.lang.String description) { this.description = description; } }
package myPackage; public class PersonRole extends PojoSupport<PersonRole> { private Integer id; private Person person; private Role role; public Person getPerson() { return person; } public void setPerson(Person newPerson) { if (this.person == null || this.person != newPerson) { if (this.person != null) { Person oldPerson = this.person; this.person = null; oldPerson.removePersonRole(this); } if (newPerson != null) { this.person = newPerson; this.person.addPersonRole(this); } } } public Role getRole() { return role; } public void setRole(Role newRole) { if (this.role == null || this.role != newRole) { if (this.role != null) { Role oldRole = this.role; this.role = null; oldRole.removePersonRole(this); } if (newRole != null) { this.role = newRole; this.role.addPersonRole(this); } } } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } }
以上就是使用 Java 語言對一個多對多關係的雙向相關實現。在這個實現中咱們使用了 java.util.LinkedHashSet 而非其它常見的集合類做爲承載容器,這是由於 java.util.LinkedHashSet 能夠經過重定義 equals 和 hashCode 方法簡單高效的實現比較從數據庫中查詢出對象的「業務邏輯上的相等與不等」,同時它是基於鏈表實現所以元素排列有序,在實際開發軟件時這些特性會給咱們帶來極大的好處。編程
雙向相關模型的優點json
模型的相關代碼已經所有給出,咱們能夠經過一個用例來測試這個模型的效果。咱們定義三我的員(甲、乙、丙)和兩個角色(管理員、用戶),其中甲是管理員,乙是用戶,丙既是管理員也是用戶。實現以下(爲使說明足夠簡單清晰,咱們假設 Person 類擁有 new Person(Integer id, String name) 的構造函數,Role 類擁有 new Role(Integer id, String description) 的構造函數):網絡
Person p1 = new Person(1, "甲"), p2 = new Person(2, "乙"), p3 = new Person(3, "丙"); Role r1 = new Role(1, "管理員"), r2 = new Role(2, "用戶"); PersonRole p1r1 = new PersonRole(), p2r2 = new PersonRole(), p3r1 = new PersonRole(), p3r2 = new PersonRole();
如下代碼經過 4 種不一樣的方式實現了甲、乙、丙三我的員和管理員、用戶兩個角色之間的關聯:mybatis
/* 姓名爲甲的人員具備管理員角色 */ p1r1.setPerson(p1); p1r1.setRole(r1); /* 姓名爲乙的人員具備用戶角色 */ p2.addPersonRole(p2r2); p2r2.setRole(r2); /* 姓名爲丙的人員具備管理員角色 */ p3r1.setPerson(p3); r1.addPersonRole(p3r1); /* 姓名爲丙的人員也具備用戶角色 */ p3.addPersonRole(p3r2); r2.addPersonRole(p3r2);
這說明雙向相關模型中,能夠經過調用不一樣模型的不一樣方法來獲得同一個結果。而咱們能夠選擇最容易獲得的對象來進行操做,這在不少狀況下能夠減小查詢數據庫的次數,同時使得維護業務數據一致性的成本變得很低。架構
假如咱們想達到對象關係之間的脫離,好比讓用戶甲再也不具備管理員角色,也有不止一種實現方法:
p1.removePersonRole(p1r1); r1.removePersonRole(p1r1); /* 或者是 */ p1r1.setPerson(null); p1r1.setRole(null); /* 或者是 remove 和 set(null) 的交叉組合 */
以上幾種方式執行的結果是徹底相同的,都讓用戶甲(p1)和管理員(r1)脫離了關聯。在實際開發中選擇哪一種方式取決於咱們更容易獲取哪一個對象。
在單向模型中咱們需手動添加對象變化後影響關聯對象的代碼,而雙向相關模型的對象改變後會馬上影響到關聯對象,每一個對象都會馬上發生相應變化,並且是在模型層面實現,在業務層面能夠直接使用這些特性而徹底不須要增長任何代碼,這對於咱們開發穩定且複雜的網絡應用是十分有幫助的。
雙向相關模型的優點不止體如今增刪改上,在展現數據時也有獨特的地方。仍以上面的用例來講,丙(p3)既是管理員(r1)又是用戶(r2),咱們把丙、管理員、用戶都序列化爲 json 看(這裏使用了FastJson中間件來序列化,由於雙向相關模型的對象中會包含循環引用,所以序列化時必須開啓循環處理。同時還開啓了 FastJson 的 prettyFormat 特性以使輸出結果便於閱讀):
用戶「丙」序列化後的json爲:
{ "id" : 3, "name" : "丙", "personRole" : [{ "person" : {"$ref" : "$"}, "role" : { "description" : "管理員", "id" : 1, "personRole" : [{"$ref" : "$.personRole[0]" }] } },{ "person" : {"$ref" : "$"}, "role" : { "description" : "用戶", "id" : 2, "personRole" : [{"$ref" : "$.personRole[1]" }] } }] }
角色「管理員」序列化後的json爲:
{ "description" : "管理員", "id" : 1, "personRole" : [{ "person" : { "id" : 3, "name" : "丙", "personRole" : [ {"$ref" : "$.personRole[0]" },{ "person" : { "$ref" : "$.personRole[0].person"}, "role" : { "description" : "用戶", "id" : 2, "personRole" : [{"$ref" : "$.personRole[0].person. personRole[1]" }] } }] }, "role" : { "$ref" : "$"} }] }
角色「用戶」序列化後的json爲:
{ "description" : "用戶", "id" : 2, "personRole" : [{ "person" : { "id" : 3, "name" : "丙", "personRole" : [{ "person" : {"$ref" : "$.personRole[0].person"}, "role" : { "description" : "管理員", "id" : 1, "personRole" : [{"$ref" : "$.personRole[0].person.personRole[0]"}] } },{"$ref" : "$.personRole[0]"}] }, "role" : {"$ref" : "$"} }] }
以上 json 中出現的 $ref 表示引用,$ 表示自身,這是 json 語法中的一種非官方約定表達方式,用來描述含有循環引用的對象的序列化,主流的 javascript 框架都具有解析這種 json 的功能,也能夠經過 javascript 原生語法來實現這個功能。以用戶「丙」的 json 爲例,通過解析後獲得的 personJson 有以下特性:
personJson.id = 3; personJson.name = "丙"; personJson.personRole [0].person = personJson; /* 代表對象是能夠自引用的 */ personJson.personRole [0].role.description = "管理員"; personJson.personRole [0].role.id = 1; personJson.personRole [0].role.personRole [0] = personJson.personRole [0]; personJson.personRole [1].person = personJson; personJson.personRole [1].role.description = "用戶"; personJson.personRole [1].role.id = 2; personJson.personRole [1].role.personRole [0] = personJson.personRole [1];
而若是以角色「管理員」爲例,通過解析後獲得的 roleJson 有以下特性:
roleJson.description = "管理員"; roleJson.id = 1; roleJson.personRole [0].person.id = 3; roleJson.personRole [0].person.name = "丙"; roleJson.personRole [0].person.personRole [0] = roleJson.personRole [0]; roleJson.personRole [0].person.personRole [1].person = roleJson.personRole [0].person roleJson.personRole [0].person.personRole[1].role.description = "用戶"; roleJson.personRole [0].person.personRole [1].role.id = 2; roleJson.personRole [0].person.personRole [1].role.personRole [0] = roleJson.personRole [0].person.personRole [1] roleJson.personRole [0].role = roleJson; /* 代表對象是能夠自引用的 */
而若是以角色「用戶」爲例,通過解析後獲得的 roleJson 有以下特性:
roleJson.description = "用戶"; roleJson.id = 2; roleJson.personRole [0].person.id = 3; roleJson.personRole [0].person.name = "丙"; roleJson.personRole [0].person.personRole [0].person = roleJson.personRole [0].person; roleJson.personRole [0].person.personRole[0].role.description = "管理員"; /* 由於「管理員」先於「用戶」加入,因此管理員序列化後「管理員」排在「用戶」前 */ roleJson.personRole [0].person.personRole [0].role.id = 1; roleJson.personRole [0].person.personRole [0].role.personRole [0] = roleJson.personRole [0].person.personRole [0]; roleJson.personRole [0].person.personRole [1] = roleJson.personRole [0]; roleJson.personRole [0].role = roleJson; /* 代表對象是能夠自引用的 */
由上可見此三者序列化後的 json 形式包含的信息量是相同的,也就是說,在雙向相關模型中咱們能夠對最容易獲取的對象進行序列化,它的 json 中會自動包含全部與它有關聯的對象(以及關聯對象的關聯對象)的信息,而後對json進行各類處理而不用擔憂丟失信息,所以咱們能夠說,雙向相關模型的對象能夠方便的進行「聚合」,同時由於數據自引用(壓縮)在傳輸中也節約了寶貴的帶寬空間。
在現實中咱們能夠對「多對一」關係進行自動裝載,對「一對多」關係按須要進行手動裝載,以免聚合的對象過於龐大,這也是經實踐證實可行的方式。
雙向相關模型的缺點
雙向相關模型對比單向模型須要更多的代碼,例如 addPersonRole、removePersonRole、removeAllPersonRole 這些方法在單向模型中不須要指定,但在雙向相關模型中須要嚴格定義,而 setPersonRole、setRole 這樣的 setter 方法實現起來也比單向模型更復雜,可是咱們能夠經過建模工具來自動生成模型代碼。然而,現實中的問題還不止如此。
以 Java 語言爲例,雙向相關模型的代碼須要全部模型類同時編譯,在目前的開發工具中不難作到,可是若是咱們使用 Maven 來管理項目,考慮到項目的可擴展性,把不一樣的業務模塊放在不一樣的 Maven 模塊中時,就會出現問題。由於 Maven 模塊不支持循環依賴(即模塊 A 依賴於 B 同時模塊 B 也依賴於 A),雖然能夠經過插件 build-helper-maven-plugin 來實現這種模塊的同時編譯,可是 Maven 自己並不鼓勵這麼作。在開發項目時爲了構建不借助插件就能夠編譯的模塊,咱們須要在雙向相關模型中再引入接口。仍以以上三個模型爲例,咱們新建兩個包:packageA 和 packageB,在 packageA 中存放 Role 類和一些接口,在 packageB 中存放 Person 類、PersonRole 類和一些接口,packageA 中的類徹底不調用 packageB 中的類,而 packageB 中的類會調用 packageA 中的類,以此來模擬兩個 Maven 模塊的單向依賴關係。
packageA 中代碼以下:
package myPackageA; public interface PersonRoleFace { void setRole(Role role); }
package myPackageA; public class Role extends PojoSupport<Role> { public Role(Integer id, String description) { this.id = id; this.description = description; } private Integer id; private java.lang.String description; private java.util.Collection<PersonRoleFace> personRole; public java.util.Collection<PersonRoleFace> getPersonRole() { if (personRole == null) personRole = new java.util.LinkedHashSet<PersonRoleFace>(); return personRole; } public java.util.Iterator<PersonRoleFace> getIteratorPersonRole() { if (personRole == null) personRole = new java.util.LinkedHashSet<PersonRoleFace>(); return personRole.iterator(); } public void setPersonRole(java.util.Collection<? extends PersonRoleFace> newPersonRole) { removeAllPersonRole(); for (java.util.Iterator<? extends PersonRoleFace> iter = newPersonRole.iterator(); iter.hasNext();) addPersonRole((PersonRoleFace) iter.next()); } public void addPersonRole(PersonRoleFace newPersonRole) { if (newPersonRole == null) return; if (this.personRole == null) this.personRole = new java.util.LinkedHashSet<PersonRoleFace>(); if (!this.personRole.contains(newPersonRole)) { this.personRole.add(newPersonRole); newPersonRole.setRole(this); } else { for (PersonRoleFace temp : this.personRole) { if (newPersonRole.equals(temp)) { if (temp != newPersonRole) { removePersonRole(temp); this.personRole.add(newPersonRole); newPersonRole.setRole(this); } break; } } } } public void removePersonRole(PersonRoleFace oldPersonRole) { if (oldPersonRole == null) return; if (this.personRole != null) if (this.personRole.contains(oldPersonRole)) { for (PersonRoleFace temp : this.personRole) { if (oldPersonRole.equals(temp)) { if (temp != oldPersonRole) { temp.setRole((Role) null); } break; } } this.personRole.remove(oldPersonRole); oldPersonRole.setRole((Role) null); } } public void removeAllPersonRole() { if (personRole != null) { PersonRoleFace oldPersonRole; for (java.util.Iterator<PersonRoleFace> iter = getIteratorPersonRole(); iter.hasNext();) { oldPersonRole = (PersonRoleFace) iter.next(); iter.remove(); oldPersonRole.setRole((Role) null); } personRole.clear(); } } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public java.lang.String getDescription() { return description; } public void setDescription(java.lang.String description) { this.description = description; } }
packageB 中代碼以下:
package myPackageB; public interface PersonRoleFace { void setPerson(Person person); }
package myPackageB; import myPackageA.Role; public class PersonRole extends PojoSupport<PersonRole> implements myPackageA.PersonRoleFace, myPackageB.PersonRoleFace { private Integer id; private Person person; private Role role; public Person getPerson() { return person; } public void setPerson(Person newPerson) { if (this.person == null || this.person != newPerson) { if (this.person != null) { Person oldPerson = this.person; this.person = null; oldPerson.removePersonRole(this); } if (newPerson != null) { this.person = newPerson; this.person.addPersonRole(this); } } } public Role getRole() { return role; } public void setRole(Role newRole) { if (this.role == null || this.role != newRole) { if (this.role != null) { Role oldRole = this.role; this.role = null; oldRole.removePersonRole(this); } if (newRole != null) { this.role = newRole; this.role.addPersonRole(this); } } } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } }
package myPackageB; public class Person extends PojoSupport<Person> { private Integer id; private java.lang.String name; private java.util.Collection<PersonRoleFace> personRole; public java.util.Collection<? extends PersonRoleFace> getPersonRole() { if (personRole == null) personRole = new java.util.LinkedHashSet<PersonRoleFace>(); return personRole; } public java.util.Iterator<? extends PersonRoleFace> getIteratorPersonRole() { if (personRole == null) personRole = new java.util.LinkedHashSet<PersonRoleFace>(); return personRole.iterator(); } public void setPersonRole(java.util.Collection<? extends PersonRoleFace> newPersonRole) { removeAllPersonRole(); for (java.util.Iterator<? extends PersonRoleFace> iter = newPersonRole.iterator(); iter.hasNext();) addPersonRole((PersonRoleFace) iter.next()); } public void addPersonRole(PersonRoleFace newPersonRole) { if (newPersonRole == null) return; if (this.personRole == null) this.personRole = new java.util.LinkedHashSet<PersonRoleFace>(); if (!this.personRole.contains(newPersonRole)) { this.personRole.add(newPersonRole); newPersonRole.setPerson(this); } else { for (PersonRoleFace temp : this.personRole) { if (newPersonRole.equals(temp)) { if (temp != newPersonRole) { removePersonRole(temp); this.personRole.add(newPersonRole); newPersonRole.setPerson(this); } break; } } } } public void removePersonRole(PersonRoleFace oldPersonRole) { if (oldPersonRole == null) return; if (this.personRole != null) if (this.personRole.contains(oldPersonRole)) { for (PersonRoleFace temp : this.personRole) { if (oldPersonRole.equals(temp)) { if (temp != oldPersonRole) { temp.setPerson((Person) null); } break; } } this.personRole.remove(oldPersonRole); oldPersonRole.setPerson((Person) null); } } public void removeAllPersonRole() { if (personRole != null) { PersonRoleFace oldPersonRole; for (java.util.Iterator<? extends PersonRoleFace> iter = getIteratorPersonRole(); iter.hasNext();) { oldPersonRole = (PersonRoleFace) iter.next(); iter.remove(); oldPersonRole.setPerson((Person) null); } personRole.clear(); } } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public java.lang.String getName() { return name; } public void setName(java.lang.String name) { this.name = name; } }
(packageA 和 packageB 中都有 PojoSupport 類,和以前的徹底相同,此處再也不累述)
如今咱們能夠把 myPackageA 包放在 Maven 模塊 A 中,把 myPackageB 包放在 Maven 模塊 B 中,PersonRole須要實現 myPackageA.PersonRoleFace 接口,所以B依賴A,這樣就實現了項目使用雙向相關模型建模同時又具有 Maven 易於擴展的優點。
總結
本文中的代碼能夠在 https://gitee.com/limeng32/biDirectPojo 中找到。雙向相關模型是面向對象編程在發展的過程當中發現的一種新理論,它表明軟件開發者對模型關係本質的更深刻理解。咱們都有這樣的體會,先進的軟件必以先進的算法爲基礎,而建模思想也是一種算法。固然,算法探索之路永無止境,但目前在B/S架構的軟件領域,雙向相關模型確實擁有優秀的表現,咱們能夠放心的使用這種建模方式做爲咱們愈來愈複雜的網絡應用的基石。
彩蛋
本文是本人開源項目 flying (地址見 https://www.oschina.net/p/flying)在開發最新版本時須要用到的理論基礎之一。由於這次 flying 新版本加入大量創新,預計不久後還會有另外一篇理論文章來描述新特性。順帶一提,由於 flying 每次要釋出多個版原本適配不一樣版的 mybatis,下一個版本將不在使用數字型名稱,取而代之的是漢字 「初雪」,大概在北京迎來第一場雪時就能發佈。至於爲何是這麼一個有詩意的名字,容我這裏賣個關子,當您看到 flying-初雪 新特性時就會明白。