编程的法则 迪米特法则(Law of Demeter)也称为“最少知识原则(Principle of Least Knowledge)包括如何实践
flyfish
2017-07-25
2024-07-18
迪米特法则(Law of Demeter)也称为“最少知识原则(Principle of Least Knowledge),是一种软件设计原则,其目的是通过减少模块之间的相互依赖性来提高代码的可维护性和可复用性。
一句话就是尽量减少对象之间的耦合。
每个开发人员都有一个“工具包”——那些倾向于反复使用的解决方案,因为根据个人的经验,它们有效。“往我们的包里装更多的技巧”。往包里装的技巧就是诸如设计模式和编程惯用法。
迪米特法则(Law of Demeter更适合归类为一种“惯用法”而非设计模式。通过了解这个惯用法并理解如何应用它,则代码会更好、更少出错、也更易于维护。减少代码中对象间“耦合”的一种技术。该惯用法也是许多不同设计模式中的组成部分。这种技术概念简单,老外却起了一个难记的名字(老外认为这个名字好记):迪米特法则,叫最少知识原则(Principle of Least Knowledge)更好。
如果你想让你的狗跑的话,你会对狗狗说还是对四条狗腿说?
示例:不遵循迪米特法则
class Leg { public: void move() { // 移动腿的逻辑 } }; class Dog { public: Leg frontLeft; Leg frontRight; Leg backLeft; Leg backRight; }; // 使用示例 Dog myDog; myDog.frontLeft.move(); myDog.frontRight.move(); myDog.backLeft.move(); myDog.backRight.move();
在这个示例中,直接操作 Dog
对象的 Leg
属性,这违反了迪米特法则。
示例:遵循迪米特法则
class Leg { public: void move() { // 移动腿的逻辑 } }; class Dog { public: void run() { frontLeft.move(); frontRight.move(); backLeft.move(); backRight.move(); } private: Leg frontLeft; Leg frontRight; Leg backLeft; Leg backRight; }; // 使用示例 Dog myDog; myDog.run();
通过 Dog
对象的 run()
方法与其交互,而不是直接操作 Leg
属性。这符合迪米特法则。
一个通过接口调用,一个直接深入到其他对象的成员变量
不遵循迪米特法则的代码
#include <iostream> #include <string> class Address { public: std::string city; Address(const std::string& city) : city(city) {} }; class Customer { public: Address address; Customer(const Address& address) : address(address) {} }; class Order { public: Customer customer; Order(const Customer& customer) : customer(customer) {} }; int main() { Address address("JiNan"); Customer customer(address); Order order(customer); // 不遵循迪米特法则的代码 std::string city = order.customer.address.city; std::cout << "City: " << city << std::endl; return 0; }
遵循迪米特法则的代码
#include <iostream> #include <string> class Address { public: std::string city; Address(const std::string& city) : city(city) {} }; class Customer { public: Address address; Customer(const Address& address) : address(address) {} std::string get_city() const { return address.city; } }; class Order { public: Customer customer; Order(const Customer& customer) : customer(customer) {} std::string get_customer_city() const { return customer.get_city(); } }; int main() { Address address("JiNan"); Customer customer(address); Order order(customer); // 遵循迪米特法则的代码 std::string city = order.get_customer_city(); std::cout << "City: " << city << std::endl; return 0; }
解释
不遵循迪米特法则的代码 :在这个示例中,直接访问了
order
对象的customer
成员,然后进一步访问了customer
的address
成员,再进一步访问了address
的city
成员。这种直接访问多个层级的成员变量的方法违反了迪米特法则,因为它增加了对象之间的耦合度。遵循迪米特法则的代码 :在这个示例中,在
Customer
类中添加了一个get_city
方法,然后在Order
类中添加了一个get_customer_city
方法。这两个方法分别返回了city
的值。这样做的好处是,在访问city
时,只调用了Order
对象的方法,避免了直接访问多个层级的成员变量,从而遵循了迪米特法则,降低了对象之间的耦合度。
看上去使用了迪米特法则, 由于糟糕的设计,会让类接口变得臃肿
如果一个对象有大量的属性,为每个属性都提供 get
方法, set
方法会显得繁琐且冗余
示例说明
假设我们有一个Customer
类,它有一个Address
对象,Address
对象包含Street
、City
和ZipCode
等属性。
class Address { public: std::string getStreet() const { return street; } std::string getCity() const { return city; } std::string getZipCode() const { return zipCode; } void setStreet(const std::string& str) { street = str; } void setCity(const std::string& cty) { city = cty; } void setZipCode(const std::string& zip) { zipCode = zip; } private: std::string street; std::string city; std::string zipCode; }; class Customer { public: Address getAddress() const { return address; } void setAddress(const Address& addr) { address = addr; } private: Address address; };
违反迪米特法则的代码
直接访问对象的属性会违反迪米特法则:
Customer customer; std::string street = customer.getAddress().getStreet(); // 直接访问对象的属性
遵循迪米特法则的代码
为了遵循迪米特法则,我们需要在Customer
类中添加包装方法:
class Customer { public: Address getAddress() const { return address; } void setAddress(const Address& addr) { address = addr; } std::string getStreet() const { return address.getStreet(); } std::string getCity() const { return address.getCity(); } std::string getZipCode() const { return address.getZipCode(); } private: Address address; }; // 使用示例 Customer customer; std::string street = customer.getStreet(); // 使用包装方法
处理大量属性的方法
如果一个对象有大量的属性,为每个属性都提供 get
方法会显得繁琐且冗余,与简化代码和提高可维护性的初衷相悖。然而,迪米特法则并不是要求机械地为每个属性都提供 get
方法,而是提倡一种设计理念:尽量减少对象之间的耦合,保持模块的独立性和接口的简洁性。
以下是处理大量属性的方法
0. 直接访问对象的属性,没有遵循迪米特法则
#include <iostream> #include <string> class Address { public: std::string street; std::string city; std::string state; std::string zipcode; }; class Customer { public: std::string first_name; std::string last_name; Address address; }; // 使用示例 int main() { Customer customer; customer.first_name = "John"; customer.last_name = "Doe"; customer.address.street = "123 Main St"; customer.address.city = "Anytown"; customer.address.state = "CA"; customer.address.zipcode = "12345"; std::cout << "Customer: " << customer.first_name << " " << customer.last_name << std::endl; std::cout << "Address: " << customer.address.street << ", " << customer.address.city << ", " << customer.address.state << " " << customer.address.zipcode << std::endl; return 0; }
简单的场景可以用,如果遇到以下场景就不适合了
代码直接依赖于 Customer 和 Address 类的内部结构。如果 Address 类的内部结构发生变化(例如,属性名称或类型改变),所有直接访问这些属性的代码都需要修改。
当代码中大量地方直接访问对象的属性时,维护变得困难。如果需要添加验证逻辑例如权限检查、数据验证或日志或修改属性的访问方式,需要在每个访问点进行修改。
1. 提供有意义的接口 以下是开始遵循迪米特法则
#include <iostream> #include <string> class Customer { std::string first_name; std::string last_name; public: Customer(const std::string& first_name, const std::string& last_name) : first_name(first_name), last_name(last_name) {} std::string get_full_name() const { return first_name + " " + last_name; } }; // 使用示例 int main() { Customer customer("Fly", "Fish"); std::cout << "Full Name: " << customer.get_full_name() << std::endl; return 0; }
2. 封装相关属性
#include <iostream> #include <string> class Address { std::string street; std::string city; std::string zipcode; public: Address(const std::string& street, const std::string& city, const std::string& zipcode) : street(street), city(city), zipcode(zipcode) {} std::string get_address() const { return street + ", " + city + ", " + zipcode; } }; class Customer { std::string name; Address address; public: Customer(const std::string& name, const Address& address) : name(name), address(address) {} Address get_address() const { return address; } }; // 使用示例 int main() { Address address("123 Main St", "JiNan", "12345"); Customer customer("Fly Fish", address); std::cout << "Address: " << customer.get_address().get_address() << std::endl; return 0; }
3. 使用数据传输对象(DTO)
#include <iostream> #include <string> class CustomerDTO { public: std::string name; std::string address; std::string phone; std::string email; CustomerDTO(const std::string& name, const std::string& address, const std::string& phone, const std::string& email) : name(name), address(address), phone(phone), email(email) {} }; class Customer { std::string name; std::string address; std::string phone; std::string email; public: Customer(const std::string& name, const std::string& address, const std::string& phone, const std::string& email) : name(name), address(address), phone(phone), email(email) {} CustomerDTO to_dto() const { return CustomerDTO(name, address, phone, email); } }; // 使用示例 int main() { Customer customer("Fly Fish", "123 Main St, JiNan", "555-1234", "Fly.Fish@example.com"); CustomerDTO dto = customer.to_dto(); std::cout << "DTO Name: " << dto.name << std::endl; std::cout << "DTO Address: " << dto.address << std::endl; std::cout << "DTO Phone: " << dto.phone << std::endl; std::cout << "DTO Email: " << dto.email << std::endl; return 0; }
4. 批量访问方法
#include <iostream> #include <string> #include <tuple> class Customer { std::string name; std::string address; std::string phone; std::string email; public: Customer(const std::string& name, const std::string& address, const std::string& phone, const std::string& email) : name(name), address(address), phone(phone), email(email) {} std::tuple<std::string, std::string, std::string> get_contact_info() const { return std::make_tuple(address, phone, email); } }; // 使用示例 int main() { Customer customer("Fly Fish", "123 Main St, JiNan", "555-1234", "Fly.Fish@example.com"); auto [address, phone, email] = customer.get_contact_info(); std::cout << "Address: " << address << std::endl; std::cout << "Phone: " << phone << std::endl; std::cout << "Email: " << email << std::endl; return 0; }
除了上面的做法还可以用设计模式、领域驱动设计、组合而非继承以及单一职责原则等等
迪米特法则(Law of Demeter)规定方法只能与以下对象通信:
该方法所属类的对象
作为方法参数传递的对象
对象的属性所引用的对象
方法创建的局部对象
全局变量引用的对象
#include <iostream> #include <string> class Address { private: std::string street; std::string city; std::string state; std::string zipcode; public: Address(const std::string& street, const std::string& city, const std::string& state, const std::string& zipcode) : street(street), city(city), state(state), zipcode(zipcode) {} std::string get_full_address() const { return street + ", " + city + ", " + state + " " + zipcode; } // 自身对象的方法 std::string get_city() const { return city; } }; class Customer { private: std::string first_name; std::string last_name; Address address; public: Customer(const std::string& first_name, const std::string& last_name, const Address& address) : first_name(first_name), last_name(last_name), address(address) {} // 自身对象的方法 std::string get_full_name() const { return first_name + " " + last_name; } // 作为方法参数传递的对象的方法 void print_address(const Address& addr) const { std::cout << addr.get_full_address() << std::endl; } // 对象的属性所引用的对象的方法 std::string get_customer_city() const { return address.get_city(); } // 方法创建的局部对象的方法 void print_greeting() const { std::string greeting = "Hello, " + first_name + "!"; std::cout << greeting << std::endl; } }; // 全局变量引用的对象 Address global_address("456 Another St", "Othertown", "TX", "67890"); int main() { Address address("123 Main St", "Anytown", "CA", "12345"); Customer customer("John", "Doe", address); // 该方法所属类的对象的方法 std::cout << "Customer: " << customer.get_full_name() << std::endl; // 作为方法参数传递的对象的方法 customer.print_address(address); // 对象的属性所引用的对象的方法 std::cout << "Customer's City: " << customer.get_customer_city() << std::endl; // 方法创建的局部对象的方法 customer.print_greeting(); // 全局变量引用的对象 std::cout << "Global Address: " << global_address.get_full_address() << std::endl; return 0; }