设计原则从“懵B”到入门

作者: 程序员丶星霖 | 来源:发表于2017-04-11 18:27 被阅读111次

    设计原则从“懵B”到入门

    什么是接口?

    接口就是一系列方法的声明,是一些方法特征的集合,一个接口只有方法的特征没有方法的实现,因为这些方法可以在不同的地方被不同的类所实现,而这些实现可以具有不同的行为(功能)。

    • 接口是对抽象的抽象。
    • 接口就是标准,就是承诺。
    • 针对接口编程,不要针对具体编程是依赖倒置原则的另外一种表述。

    针对接口编程又叫面向接口编程,针对接口编程就是要先设计一系列的接口,把设计和实现分离开,使用时只需引用接口即可。如下图:

    面向接口编程.jpg

    针对接口编程可提高程序的可维护性、可伸缩性和可复用性。

    Java是面向对象编程的语言,而面向对象编程的核心之一就是要针对接口编程,不要针对实现编程。

    Android的框架设计的重要的原则和目的就是针对接口编程。

    面向对象设计的几个目标:

    1. 可扩展性,容易添加新的功能;
    2. 灵活性,容易添加新的功能代码修改平稳地发生;
    3. 可插入性,容易将一个类抽出去,同时将另一个有同样接口的类加入进来。

    判断软件设计质量的标准:
    高内聚,低耦合

    • 耦合性:也称块间联系。指软件系统结构中各模块间相互联系紧密程度的一种度量。模块之间联系越紧密,其耦合性就越强,模块的独立性就越差。模块间耦合性的高低取决于模块间接口的复杂性、调用的方式及传递的信息。
    • 内聚性:也称块内联系。指模块的功能强度的度量,即一个模块内部各个元素彼此结合的紧密程度的度量。如果一个模块内各个元素联系的越紧密,则它的内聚性越高。
    • 高内聚是指一个软件模块是由相关性很强的代码组成,只负责一项任务,不可分割。
    • 低耦合,一个完整的系统,模块与模块之间,尽可能的使其独立存在。也就是说,让每一个模块,尽可能的独立完成某个特定的子功能。模块与模块之间的接口,尽量少而简单。

    设计原则

    1.单一职责原则(Single Responsibility Principle)

    对于一个类而言,应该仅有一个引起它变化的原因。也就是说,一个类的功能要单一,则只做与它相关的事情。
    (There should never be more than one reason for a class to change .)

    体现:高内聚

    如果一个类完成额外的不太相关的功能或者完成其他类的功能,这就会使得引起一个类变化的因素太多。

    • 遵循单一职责原则会给测试带来极大的方便。
    • 违背单一职责原则会降低类的内聚性,增强类的耦合性。

    SRP的好处:

    • 可降低类的复杂度,一个类只负责一项职责,其逻辑肯定比负责多项职责简单的多;
    • 复杂度低,可读性自然提高;
    • 可维护性高,风险低。如果接口的单一职责原则好,一个接口修改只对应相应的实现类有影响。对其它接口无影响,这对系统的扩展性、维护性都有非常大的帮助。

    注意:单一职责原则推出了一个编写程序的标准,用“职责”或“变化原因”来衡量接口或类设计的是否优良,但是“职责”和“变化原因”都是不可度量的,因项目而异,因环境而异。

    示例代码如下所示:

    public class CustomerChart{
    
        CustomerDao dao;
    
        public void setDao(CustomerDao dao){
            this.dao=dao;
        }
    
        public void displayChart(){
            for(Customer customer: findCustomers()){
                System.out.println("customer:"+customer.name);
            }
        }
    }
    
    public class CustomerDao{
    
        protected List<Customer> findCustomers(){
            List<Customer> ret = new ArrayList<Customer>();
            ret.add(new Customer("zhangsan",30));
            ret.add(new Customer("lisi",28));
            return ret;
        }
    }
    

    UML图如下所示:

    单一职责原则.jpg

    2.开放封闭原则(Open Closed Principle)

    一个软件实体应当对扩展开放,对修改封闭。对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况;对修改封闭,意味着一旦设计完成,就可以独立完成其工作,而不要对类进行任何修改。开放封闭原则是所有面向对象原则的核心。(Software entities like classes , modules and functions should be open for extension but closed for modifications .)

    体现:高内聚,低耦合。

    优点:降低了程序各部分之间的耦合性,为了满足开闭原则,需要对系统进行抽象化设计,抽象化是开闭原则的关键。

    如何做到开放封闭原则呢?
    封闭变化,依赖接口和抽象类,而不要依赖具体实现类。要针对接口和抽象类编程,不要针对具体实现编程。因为接口和抽象类是稳定的,是一种对客户端的承诺,是相对不变的。

    注意:OCP对扩展开放,对修改关闭,并不意味着不做任何修改,低层模块的变更,必然要有高层模块进行耦合,否则就是一个孤立无意义的代码片段。

    变化可以归纳为以下三种类型:

    • 逻辑变化:之变化一个逻辑,而不涉及其他模块。可以通过修改原有类中的方法的方式来完成,前提条件是所有依赖或关联类都按照相同的逻辑处理。
    • 子模块变化:一个模块变化,会对其他的模块产生影响,特别是一个低层次的模块变化必然引起高层模块的变化,因此在通过扩展完成变化时,高层次的模块修改是必然的。
    • 可见视图变化:可见视图是提供给客户使用的界面。

    如何使用OCP?

    1. 抽象约束:抽象是对一组事物的通用描述,没有具体的实现,也就表示它可以有非常多的可能性,可以跟随需求的变化而变化。因此,通过接口或抽象类可以约束一组可能变化的行为,并且能够实现对扩展开放,包含了三层含义:
    • 通过接口或抽象类约束扩展,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的public方法;
    • 参数类型、引用对象尽量使用接口或者抽象类,而不是实现类;
    • 抽象层尽量保持稳定,一旦确定即不允许修改。
    1. 元数据控制模块行为:什么是元数据?用来描述环境和数据的数据,通俗地说就是配置参数,参数可以从文件中获得,也可以从数据库中获得。
    2. 制定项目章程:章程中指定了所有人员都必须遵守的约定,对项目来说,约定优于配置。
    3. 封装变化:对变化的封装包含两层含义:
    • 将相同的变化封装到一个接口或抽象类中;
    • 将不同的变化封装到不同的接口或抽象类中,不应该有两个不同的变化出现在同一个接口或抽象类中。

    示例代码如下:

    public class PieChart extends BaseChart{
        
        public void display(){
            System.out.println("pie chart display");
        }
    }
    
    public class BarChart extends BaseChart{
        
        public void display(){
            System.out.println("bar chart display");
        }
    }
    
    public class ChartDisplayManager{
        
        public void display(BaseChart chart){
            chart.display();
        }
    }
    
    public abstract class BaseChart{
        public abstract void display();
    }
    

    UML图如下所示:

    开闭原则.jpg

    3.里氏代换原则(Liskov Substitution Principle)

    一个软件实体如果使用的是基类,那么也一定适用于其子类,而且它根本觉察不出使用的是基类对象还是子类对象;反过来的代换是不成立的,即如果一个软件实体使用一个类的子类对象,那么它不能够适用于基类对象。

    体现:低耦合

    优点:可以很容易的实现统一父类下各个子类的互换,而客户端可以毫不察觉。增强程序的健壮性,版本升级时也可以保持非常好的兼容性。

    在面向对象的语言中,继承是必不可少的、非常优秀的语言机制,它的优点如下:

    • 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性;
    • 提高代码的重用性;
    • 子类可以形似父类,但又异于父类;
    • 提高代码的可扩展性;
    • 提高产品或项目的开放性。

    它的缺点如下:

    • 继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法;
    • 降低代码的灵活性。子类必须拥有父类的属性和方法,让子类的世界中多了些约束;
    • 增强了耦合性。当父类的常量、变量和方法被修改时,需要考虑子类的修改。

    里氏代换原则的定义包含了一下4层含义:

    1. 子类必须完全实现父类的方法
    2. 子类可以有自己的属性和方法
    3. 覆盖或实现父类的方法时输入参数可以被放大
    4. 覆盖或实现父类的方法时输出结果可以被缩小

    示例代码如下:

    public  abstract class Gun{
        
    }
    
    public class Rifle extends WeapGun{
        void shoot(){
            System.out.println("rifle  shoot");
        }
    }
    
    public class HandGun extends WeapGun{
        void shoot(){
            System.out.println("handGun  shoot");
        }
    }
    
    public class Soldier{
        public void killEnemy(WeapGun gun){
            gun.shoot();
            System.out.println("Soldier  kill  enemy");
        }
    }
    
    public class ToyGun extends Gun{
        void shoot(){
            System.out.println("toyGun can't shoot");
        }
    }
    
    public abstract class WeapGun {
        abstract void shoot();
    }
    

    UML图示如下:

    里氏替换原则.jpg

    4.迪米特法则(Law of Demeter)

    也称最少知识原则(Least Knowledge Principle),也就是说,一个对象应当对其他对象尽可能少地了解,“不要和陌生人说话”。
    狭义:如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。
    广义:一个模块设计好坏的一个重要标志就是该模块在多大程度上将自己的内部数据与实现的有关细节隐藏起来。

    体现:低耦合

    在运用LKP到系统的设计中时,要注意以下几点:

    1. 在类的划分上,应当创建弱耦合的类,类与类之间的耦合越低,就越有实现可复用的目标;
    2. 在类的结构设计上,每个类都应该降低成员的访问权限;
    3. 在类的设计上,只要有可能,一个类应当设计成不变的类;
    4. 在对其他类的应用上,一个对象对其他类的对象的应用应该降到最低;
    5. 尽量限制局部变量的有效范围。

    LKP对类的低耦合提出了明确要求,包含以下4层含义:

    1. 只和朋友交流:每个对象都必然会与其他对象有耦合关系,两个对象之间的耦合就成为了朋友关系,这种关系的类型有组合、聚合、依赖。
    2. 朋友间也是有距离的:LKP要求“羞涩”一点,尽量不要对外公布太多的public方法和非静态的public变量,尽量内敛,多实用private、package-private、protected等访问权限。
    3. 是自己的就是自己的:如果一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,那就放置在本类中。
    4. 谨慎使用Serializable

    示例代码如下:

    public class Lily{
        public void eat(){
            Hamberger hamberger =new KFC().sell;
            System.out.println("Lily  eat  a  hamberger");
        }
    }
    
    public class Lucy{
        public void eat(){
            Hamberger hamberger =new KFC().sell;
            System.out.println("Lily  eat  a  hamberger");
        }
    }
    
    public class Beef{
        public Beef(){
            
        }
    }
    
    public class Bread{
        public Bread(){
            
        }
    }
    
    public class Hamberger{
        public Hamberger(Beef beef,Vegetable vegetable,Bread bread){
            System.out.println("make  a  hamberger");
        }
    }
    
    public class Vegetable{
        public Vegetable(){
            
        }
    }
    
    public class KFC{
        public Hamberger sell(){
            Bread bread =new Bread();
            Vegetable vegetable =new Vegetable();
            Beef beef =new Beef();
            Hamberger hamberger =new Hamberger(beef,vegetable,bread);
            return hamberger;
        }
    }
    

    UML图示如下:

    迪米特法则.jpg

    5.接口隔离原则(Interface Segregation Principle)

    使用多个专一功能的接口比使用一个的总结口要好。
    Clients should not be forced to depend upon interfaces that they don't use .(客户端不应该依赖它不需要的接口。)
    The dependency of one class to another one should depend on the smalls possible interface .(类间的依赖关系应该建立在最小的接口上。)
    体现:高内聚

    优点:会使一个软件系统功能扩展时,修改的压力不会传到别的对象那里。

    接口主要分为两种:

    • 实例接口(Object Interface):在Java中声明一个类,然后用new关键字产生一个实例,它是对一个类型的事务的描述,这是一种接口。
    • 类接口(Class Interface):Java中经常使用的interface关键字定义的接口。

    接口隔离原则主要包含以下4层含义:

    1. 接口要尽量小:根据接口隔离原则拆分接口时,首先必须满足单一职责原则。
    2. 接口要高内聚:提高接口、类、模块的处理能力,减少对外的交互。
    3. 定制服务:单独为一个个体提供优良的服务。采用定制服务有一个要求,只提供访问者需要的方法。
    4. 接口设计是有限度的:接口的设计粒度越小,系统越灵活。

    接口隔离原则是对接口的定义,同时也是对类的定义,接口和类尽量使用原子接口或原子类来组装。在实践中可以根据以下规则来划分原子:

    • 一个接口只服务于一个子模块或逻辑业务。
    • 通过业务逻辑压缩接口中的public方法,接口时常去回顾,尽量让接口简洁。
    • 已经被污染了的接口,尽量去修改,如果变更的风险较大,则采用适配器模式进行转化处理。
    • 了解环境,拒绝盲从。环境不同,接口拆分的标准也就不同。

    示例代码如下:

    public class Searcher{
        public void searchActress(BasePrettyGirl girl){
            System.out.println("search a girl");
            girl.goodLooking();
            girl.niceFigure();
            girl.greatTemprament();
        }
    }
    
    public interface IGoodGirl{
        void goodLooking();
        void greatTemprament();
    }
    
    public class AngelaBaby extends BasePrettyGirl{
        public void goodLooking(){
            System.out.println("AngelaBaby is goodLooking");
        }
        public void niceFigure(){
            System.out.println("AngelaBaby is niceFigure");
        }
        public void greatTemprament(){
            System.out.println("AngelaBaby is greatTemprament");
        }
    }
    
    public class INiceFigure{
        void niceFigure();
    }
    
    public class SearcherB{
        public void searchModel(INiceFigure girl){
            System.out.println("search a super  model");
            girl.niceFigure();
        }
    }
    
    public class BasePrettyGirl implements IGoodGirl,INiceFigure{
        
    
    }
    

    UML图示如下:

    接口隔离原则.jpg

    6.依赖倒置原则(Dependence Inversion Principle)

    High level modules should not depend upon low level modules . Both should depend upon abstractions . Abstactions should not depend upon details . Details should depend upon abstractions .

    包含了三层意思:

    • 高层模块不应该依赖底层模块,两者都应该依赖其抽象;
    • 抽奖不应该依赖细节;
    • 细节应该依赖抽象。

    体现:低耦合

    在Java语言中,抽象就是指接口或抽象类,两者都是不能直接被实例化的;细节就是实现类,实现接口或继承抽象类而产生的的类就是细节。

    依赖倒置原则在Java中的表现如下:

    • 模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的;
    • 接口或抽象类不依赖于实现类;
    • 实现类依赖接口或抽象类。

    优点:

    • 减少类间的耦合性;
    • 提高系统的稳定性;
    • 降低并行开发引起的风险;
    • 提高代码的可读性和可维护性。

    对象的依赖关系有如下三种方式来传递:

    • 构造函数传递依赖对象:在类中通过构造函数声明依赖对象,按照依赖注入的说法,这种方式叫做构造函数注入。如下:
    public interface IDriver{
        
        public void drive(ICar car);
    }
    
    public class Driver implements IDriver{
        private ICar car;
    
        //构造函数注入
        public Driver(ICar car){
            this.car = car;
        }
        
        public void drive(Icar car){
            car.run();
        }
    }
    
    • Setter方法传递依赖对象:在抽象中设置Setter方法声明依赖关系,依照依赖注入的说法,这是Setter依赖注入。如下:
    public interface IDriver{
        
        public void drive(ICar car);
    }
    
    public class Driver implements IDriver{
        private ICar car;
    
        //Setter注入
        public void setCar(ICar car){
            this.car = car;
        }
        
        public void drive(Icar car){
            car.run();
        }
    }
    
    • 接口声明依赖对象:在接口的方法中声明依赖对象。如下:
    public interface IDriver{
        
        public void drive(ICar car);
    }
    
    public class Driver implements IDriver{
        
        public void drive(Icar car){
            car.run();
        }
    }
    
    public interface ICar{
        
        public void run();
    }
    
    public class Benz implements ICar{
        
        public void run(){
            System.out.println("奔驰汽车开始运行......");
        }
    }
    
    public class BMW implements ICar{
        
        public void run(){
            System.out.println("宝马汽车开始运行......");
        }
    }
    
    public class Client{
        public static void main(String[] args){
            IDriver zhangSan = new Driver();
            ICar benz = new Benz();
            zhangSan.drive(benz);
        }
    }
    

    依赖倒置原则就是通过抽象使各个类或模块的实现彼此独立,不互相影响,实现模块之间的低耦合,在使用中应该遵循如下几点:

    • 每个类尽量都有接口或抽象类,或者抽象类和接口两者都具备;
    • 变量的表面类型尽量是接口或者是抽象类;
    • 任何类都不应该从具体类派生;
    • 尽量不要覆写基类的方法;
    • 结合里氏替换原则使用;

    UML图示如下:

    依赖倒置原则.jpg

    好吧,几天没发文章了。与大家共勉。

    我的微信公众号.jpg

    相关文章

      网友评论

        本文标题:设计原则从“懵B”到入门

        本文链接:https://www.haomeiwen.com/subject/qfsuattx.html