对于OOP,想必大家都很熟悉,通常我们说的OOP有5大原则,即SOLID,这里我加上了两条也比较重要的迪米特法则和多用组合少用继承,组成了OO设计的七大原则,我们一起来复习一下。
笑话一则
问:换一个电灯泡需要几个程序员?
答:你还在用面向过程的思维考虑问题。一个设计良好的电灯泡类必然封装了换灯泡的方法,所以你要做的就是调用“换电灯泡”方法。
什么是OO
OO通常指的就是OOP(Object-oriented programming),面向对象程序设计,是种具有对象概念的程序编程范型,同时也是一种程序开发的方法。它可能包含数据、属性、代码与方法。对象则指的是类的实例。它将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性,对象里的程序可以访问及经常修改对象相关连的数据。在面向对象程序编程里,计算机程序会被设计成彼此相关的对象。通俗地讲,就是万物皆对象。通常我们会用到OO的两大特性,继承与多态。
七大原则
1.单一职责原则(SRP, Single Responsibiliy Principle)
2.开放封闭原则(OCP, Open Closed Principle)
3.里氏替换原则(LSP, Liskov Substitution Principle)
4.接口分离原则(ISP, Interface Segregation Principle)
5.依赖倒置原则(DIP, Dependency Inversion Principle)
6.最少知识原则(LKP, Least Knowledge Principle)
7.多用组合,少用继承
单一职责原则(SRP, Single Responsibiliy Principle)
当需要修改某个类的时候原因有且只有一个(THERE SHOULD NEVER BE MORE THAN ONE REASON FOR A CLASS TO CHANGE)。换句话说就是让一个类只做一种类型责任,当这个类需要承当其他类型的责任的时候,就需要分解这个类。这是一个听起来非常简单但是做起来很困难的原则,我们通常会不经意或者故意让一个类承担了多个职责,这是一种需要自己去平衡的原则,如何做到高内聚低耦合,十分考验设计功底。
class Customer { public void Add() { try { // Database code goes here } catch (Exception ex) { System.IO.File.WriteAllText(@"c:\Error.txt", ex.ToString()); } } }
Customer类做了两件事情,一件从数据库取数据(try里面的代码),另一部分是写log到文件里。
我们重构一下,分成两个类来实现,FileLogger负责写log文件,customer负责取数据,如果出异常,调用FileLogger写log
class FileLogger { public void Handle(string error) { System.IO.File.WriteAllText(@"c:\Error.txt", error); } } class Customer { private FileLogger obj = new FileLogger(); public virtual void Add() { try { // Database code goes here } catch (Exception ex) { obj.Handle(ex.ToString()); } } }
开放封闭原则
对扩展开放(利用多态/扩展方法),对修改封闭(不改变原有逻辑),这个听起来比较难理解,意思是说我们尽量避免修改已经实现的代码来适应需求变化,而应该通过继承和扩展方法改变实际的运行结果。
class Customer { private int _CustType; public int CustType { get { return _CustType; } set { CustType = value; } } public double getDiscount(double TotalSales) { if (CustType == 1) { return TotalSales - 100; } else { return TotalSales - 50; } } }
上面例子中Customer类的getDiscount方法只支持两种会员类型,如果我们新增一种会员类型,就需要修改getDiscount方法中的if else
我们通过继承来解决这个问题:
class Customer { public virtual double getDiscount(double TotalSales) { return TotalSales; } } class SilverCustomer : Customer { public override double getDiscount(double TotalSales) { return base.getDiscount(TotalSales) - 50; } } class goldCustomer : Customer { public override double getDiscount(double TotalSales) { return base.getDiscount(TotalSales) - 100; } }
这样即使新加了一种会员,我们只需要新实现一个类继承Customer就好了
里氏替换原则
当一个子类的实例应该能够替换任何其超类的实例时,它们之间才具有is-A关系
假设有个Bird类有个Fly方法
class Bird { public void Fly() { // Fly Logic } }
class Penguin: Bird { public override void Fly() { throw new Exception("Can Not Fly"); } } List Birds = new List(); // Add Bird Logic foreact(var bird in Birds) { bird.Fly(); }
当这个Birds里面有Penguin的实例时,就挂了,来看改过的
interface IFlyable { void Fly(); } foreact(var bird in Birds) { if(bird is IFlyable) bird.Fly(); }
接口分离原则
不能强迫用户去依赖那些他们不使用的接口。换句话说,使用多个专门的接口比使用单一的总接口总要好。
interface IBird { void Fly(); void Eat(); }
interface IFlyable { void Fly(); } interface IEatable { void Eat(); }
依赖倒置原则
1.高层模块不应该依赖于低层模块,二者都应该依赖于抽象
2.抽象不应该依赖于细节,细节应该依赖于抽象
最少知识原则(迪米特法则)
一个对象应该对其他对象保持最少的了解。正如最少知识原则这个定义一样,一个类应该对其依赖的其他类知道得最少。迪米特法则的初衷在于降低类之间的耦合。由于每个类尽量减少对其他类的依赖,因此,很容易使得系统的功能模块功能独立,相互之间不存在(或很少有)依赖关系。关于依赖的引入有那几种形式,它们直接的依赖程度是怎样的,可以参考这篇聊聊依赖
//总公司员工 class Employee{ private String id; public void setId(String id){ this.id = id; } public String getId(){ return id; } } //分公司员工 class SubEmployee{ private String id; public void setId(String id){ this.id = id; } public String getId(){ return id; } } class SubCompanyManager{ public List getAllEmployee(){ List list = new ArrayList(); for(int i=0; i<100; i++){ SubEmployee emp = new SubEmployee(); //为分公司人员按顺序分配一个ID emp.setId("分公司"+i); list.add(emp); } return list; } } class CompanyManager{ public List getAllEmployee(){ List list = new ArrayList(); for(int i=0; i<30; i++){ Employee emp = new Employee(); //为总公司人员按顺序分配一个ID emp.setId("总公司"+i); list.add(emp); } return list; } public void printAllEmployee(SubCompanyManager sub){ List list1 = sub.getAllEmployee(); for(SubEmployee e:list1){ System.out.println(e.getId()); } List list2 = this.getAllEmployee(); for(Employee e:list2){ System.out.println(e.getId()); } } } public class Client{ public static void main(String[] args){ CompanyManager e = new CompanyManager(); e.printAllEmployee(new SubCompanyManager()); } }
对于一个CompanyManager来说,不应该知道SubCompanyEmployee,它只需要关心Company的Employee,SubCompanyEmployee应该是SubCompanyManager关心的对象
class SubCompanyManager{ public List getAllEmployee(){ List list = new ArrayList(); for(int i=0; i<100; i++){ SubEmployee emp = new SubEmployee(); //为分公司人员按顺序分配一个ID emp.setId("分公司"+i); list.add(emp); } return list; } public void printEmployee(){ List list = this.getAllEmployee(); for(SubEmployee e:list){ System.out.println(e.getId()); } } } class CompanyManager{ public List getAllEmployee(){ List list = new ArrayList(); for(int i=0; i<30; i++){ Employee emp = new Employee(); //为总公司人员按顺序分配一个ID emp.setId("总公司"+i); list.add(emp); } return list; } public void printAllEmployee(SubCompanyManager sub){ sub.printEmployee(); List list2 = this.getAllEmployee(); for(Employee e:list2){ System.out.println(e.getId()); } } }
在设计类时,应该多思考public是否是必要的
Wrong:
public class FlightService : IFlightService { [Dependency(typeof(AService))] public IAService AService { get; set; } .... }
Right:
public class XService : IXService { [Dependency(typeof(AService))] protected IAService AService { get; set; } .... }
在设计方法的签名时,应该多思考是否真的需要传入一个大的对象
Wrong:
public class Hotel { public string Name; public int Id; public string Address; } private Passenger GetPassenger(Hotel hotel) { var passenger = PassengerList.First(p => p.hotelName = hotel.Name); return passenger; }
Right:
private Passenger GetPassenger(string hotelName) { var passenger = PassengerList.First(p => p.hotelName = hotelName); return passenger; }
多用组合,少用继承
面向对象的原则告诉我们,对类的功能的扩展要多用组合,而少用继承。其中的原因有以下几点:
1.子类对父类的继承是全部的公有和受保护的继承,这使得子类可能继承了对子类无用甚至有害的父类的方法。换句话说,子类只希望继承父类的一部分方法,怎么办?
2.实际的对象千变万化,如果每一类的对象都有他们自己的类,尽管这些类都继承了他们的父类,但有些时候还是会造成类的无限膨胀。
3.继承的子类,实际上需要编译期确定下来,这满足不了需要在运行内才能确定对象的情况。而组合却可以比继承灵活得多,可以在运行期才决定某个对象。