单一职责原则
概念:
单一职责原则简称SRP,其定义是只有一个原因引起类的变化,即一个类只负责某个功能模块区域的一类功能,一个职责对应一个类
只有一个原因引起类的变化的意思是就是能引起类的变化的,只有这个类负责的那个职责的业务要求发生了变化,我们反过来想:当一个类负责了多个职责时,随着业务的变更,某个职责需要修改,修改时可能会造成类中其他的职责功能出现bug,而如果一个类只负责一个职责,只有这个职责的业务发生变化我们才会去修改这个类,而且对其他的类不会造成影响
在我们日常的编码中,我们都已经不知不觉的使用了单一职责原则了,如果我们把所有的功能接口都堆在一个类上的话,这个项目的代码逻辑以及类的分层就会非常的混乱,接手的人估计也会骂娘的,这是非常的不利于日后的维护。一般的情况下我们会把某个功能的模块中不同的一类功能接口分类放置到不同的类上,例如手机模块,根据业务需求我们可能会把上网的功能接口和通话的功能的接口放置在不同的接口中,根据不同的需求甚至可能会分得更细,例如通话功能可能还会分为了一类是拨电话和挂电话的功能,其职责是协议的管理;一类是通话进行中的语音的功能,其职责是数据的传输。这样的设计可以使整个项目的逻辑变得合理,项目的结构也会清晰,当修改某个功能时只要修改负责这个功能相应职责的接口。
从这里我们能得出的结论就是单一原则的好处:
1、可读性提升
2、可维护性提升
3、降低耦合度
4、接口的复杂性降低
我相信在大多数情况下程序员还是会本能的遵循这个原则的,但是这个原则又最难遵循,关于这个职责的划分也没有一个量化的标准,有时候很难划分的清楚某个功能模块的各个不同的职责是什么,所以这个原则也没有严格的规定我们要去怎么做,这更像是一种合格的程序员的本能意识,不多bb直接上代码
public interface Chat {//这个一个负责电话职责的接口
public void answer();//接电话
public void handUp();//挂电话
}
public interface Net {//这是一个负责上网职责的接口
public void browseWeb();//浏览网页
}
//IPhone只有把负责各项功能的接口集合起来才可以成为一个完整的手机
public class IPhone implements Chat,Net{
@Override
public void answer() {
}
@Override
public void handUp() {
}
@Override
public void browseWeb() {
}
image.png
里氏替换原则
概念:
里氏替换原则简称LSP,其定义是父类能出现的地方子类就能出现,而且替换为子类后不会出现任何的异常或错误,但是子类能出现的地方,父类未必能代替,子类对象替换父类的对象时程序不会发生任何变化
这个原则个人理解是对类的继承和类的抽象化的一种规范
这个规范带来的优点:
1、易维护易扩展,通用方法在父类中,特有方法在子类中
2、代码可重用
3、利用子类复用父类的属性及方法,减少了创建类的成本
里氏替换原则的实现可以遵循以下标准来实现:
1、子类必须完全实现父类的方法,即子类不要去修改父类已经定义好的非抽象类方法
public class Fa_add {
public int add(int a,int b){
return a+b;
}
}
public class Son_add extends Fa_add{
@Override
public int add(int a, int b) {
return a-b;
}
}
就如上图,本来系统设计的就是要加法,你可到好让子类直接与父类背道而驰,因为你没遵守LSP的原则,所以这样就很有可能破坏了系统原本与父类达成的规范和约束,把系统设计的原意都给曲解了,这是一个很危险的做法,我们最好不要去修改父类原有的已经实现了的功能
2、子类可以拥有自己的个性
在继承父类后,子类可以新增一些自己特有的方法,对应不同的需求场景
3、当子类重载或者实现父类的方法时,子类方法的参数应当比父类更加宽松,即子类重载方法的参数应当是父类对应方法参数的父类或者抽象
不多BB,直接上代码
public class Fa_hashnap {
public void add(HashMap hashMap){
System.out.println("我是父类的处理逻辑");
}
}
public class Son_map extends Fa_hashnap{
public void add(Map map){
System.out.println("我是子类的处理逻辑");
}
}
@Test
public void test(){
Fa_hashnap fa=new Fa_hashnap();
fa.add(new HashMap());
Son_map son=new Son_map();
son.add(new HashMap());
}
image.png
这几段代码中符合了LSP的定义的:子类对象替换父类的对象时程序不会发生任何变化。子类方法的参数比较宽松,子类帮助父类把参数传递到调用的方法里,但是并不是把父类的方法重写了,这个做法是正确的,符合了LSP原则
在看看下面的代码
public class Fa_map {
public void add(Map map){
System.out.println("我是父类的处理逻辑");
}
}
public class Son_hashmap extends Fa_map{
public void add(HashMap hashMap){
System.out.println("我是子类的处理逻辑");
}
}
@Test
public void test2(){
Fa_map fa=new Fa_map();
fa.add(new HashMap());
Son_hashmap son_hashmap=new Son_hashmap();
son_hashmap.add(new HashMap());
}
image.png
可以到看到调用父类和子类的add方法结果不一样,不符合子类对象替换父类的对象时程序不会发生任何变化,违背了LSP 原则
4、重写或者实现父类的方法时输出的结果被缩小,即结果是父类的被重写接口结果的子类
这里的重写指的是抽象接口,并不是非抽象接口,重写非抽象接口是违背LSP原则的,假如父类被重写的抽象接口结果是T类型,子类的重写接口的结果是S类型,那么S类型小于等于T类型
依赖倒置
概念:
依赖倒置简称DIP,其定义为:高层模块不应该依赖低层模块,两者都应该依赖其抽象,抽象不应该依赖细节,细节应该依赖抽象
定义中的高层模块是一个逻辑复杂的模块,低层模块是一个简单而不可分割并且具有原子性的逻辑模块,抽象就是接口或者抽象类,细节就是接口或者抽象的实现类,依赖倒置简而言之就是面向接口编程
不多bb我们直接看代码
public class IPhone {
public String name(){
return "使用苹果手机";
}
}
public class XiaoMi {
public String name(){
return "使用小米手机";
}
}
public class Person {
public void usePhone(XiaoMi xiaoMi){
System.out.println("我要用"+xiaoMi.name()+"打游戏");
}
public void usePhone(IPhone iPhone){
System.out.println("我要用"+iPhone.name()+"打游戏");
}
public static void main(String[] args) {
Person me=new Person();
me.usePhone(new XiaoMi());
me.usePhone(new IPhone());
}
}
如果我只有一部小米手机和苹果手机可以轮流切换打游戏,这么写可以,但是要是有一天我有钱了,带着一个背包的手机可以轮流用,那我们是不是要在上面写一堆usePhone的方法,又或者有一个背包的人不只有me了而花花有同样拥有,而花花只是用来自拍,是不是又要为花花实现一个类似这样的类,这样肯定不合理,增加了代码的冗余,我们可以试着使用面向接口开发改造代码
public interface Phone {
String name();
}
public class IPhone implements Phone{
public String name(){
return "使用苹果手机";
}
}
public class XiaoMi implements Phone{
public String name(){
return "使用小米手机";
}
}
public interface Person {
void usePhone(Phone phone);
}
public class Me implements Person {
@Override
public void usePhone(Phone phone) {
System.out.println("我要用"+phone.name()+"打游戏");
}
}
public class HuaHua implements Person{
@Override
public void usePhone(Phone phone) {
System.out.println("我要用"+phone.name()+"自拍");
}
}
public class Test {
public static void main(String[] args) {
Person me=new Me();
Person huahua=new HuaHua();
me.usePhone(new XiaoMi());
me.usePhone(new IPhone());
huahua.usePhone(new XiaoMi());
huahua.usePhone(new IPhone());
}
}
image.png
这里高层模块是Person类 ,底层模块和细节是XiaoMi类和IPhone类 ,抽象是PHone类,这样把小米和苹果,甚至以后是华为三星都抽象成为手机类,使用它们的抽象作为个个品牌手机的统一代表,人也一样,把me和花花也抽象为Person类。在增加其他手机或者是使用者时,只要修改高层Test类,不需要其他的低层模块做任何修改,把变更降到最低我们把抽象类型或者接口类型称为表面类型,高层中应当尽量把细节(实现类)以表面类型的形式去操作
在上面的例子中可以总结出依赖倒置的优点:
1、提高可读性
2、提高维护性
3、降低更改带来的风险
4、降低耦合性
总而言之,只要时刻记住面向接口开发!!面向接口开发!!!面向接口开发!!!重要的事情说三遍,就算是符合依赖倒置的原则了
接口隔离
概念:
接口隔离简称ISP,定义为:不应当让客户端去依赖它们不需要的接口,类之间的依赖关系应该建立在最小的接口上。
总而言之就是按需提供接口,使接口中的方法尽量的少,只提供客户端需要的即可
这其中可以包含的四层含义:
1、接口在尽可能小的前提,必须要满足单一原则,而不是一顿胡乱拆解接口
2、接口要高内聚,高内聚就是提高接口,类,模块的处理能力,减少对外交互。换句话来说就是让接口“多做事少说话”,在程序表现就是尽量少公布public方法,降低需求变更的风险
3、 能提供定制服务,即可以单独为不同的个体提供优良服务(只提供访问者需要的方法)。
4、接口隔离的设计要有限度,不要为了设计而大幅度增加了项目的难度
这些都比较概念很烦(想学习能操作上手的东西,这些经典不看又不行),累了,代码我不贴,面试官问你的时候就按照上面的回答就好了
迪米特法则
概念:
迪米特法则简称LoD,也称为最少知道原则,定义为:一个对象应该对其他对象有最少的了解。
这个定义的意思是一个类对于自己需要耦合或者调用的类知道得越少越好,我不关心你内部的内部是多么复杂,我只关心你提供给我的接口是这么多,我就调用这么多,其他我一概不关心。这个原则最大的优点就是能降低类与类之间的耦合
从小家里人就教过我们迪米特法则:在外面不要和陌生人说话,只和朋友说话,自己的事情自己做,不要老麻烦别人
1、不要和陌生人说话,只和朋友说话:
这里的朋友在代码中表现就是成员变量,方法的输入输出参数的类叫做朋友,而出现在代码方法体内的类叫做陌生人,也就是局部变量的类
不多BB我们看代码:
我(Me)从电商平台的app(App)上的苹果旗舰店(IPhoneStore)买了一台苹果11(IPone11),收到货后我们会对手机进行检查,如果手机没问题问就会确认收货收货,平台就会把钱发个苹果旗舰店,以下是错误演示:
public class IPone11 {
private boolean pingmu;
private boolean imei;
public boolean isPingmu() {
return pingmu;
}
public boolean isImei() {
return imei;
}
public void setPingmu(boolean pingmu) {
this.pingmu = pingmu;
}
public void setImei(boolean imei) {
this.imei = imei;
}
}
public class IPhoneStore {
private String name;
private String pintaiName;
public void setPintaiName(String pintaiName) {
this.pintaiName = pintaiName;
}
public void setName(String name) {
this.name = name;
}
public void receive(){
System.out.println(name+"货真价实,"+pintaiName+"平台已经打款钱了");
}
public void punish(){
System.out.println(name+"卖假货,"+pintaiName+"平台将其进行处罚了");
}
}
public class App {
private String name;
public App(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void isGive(IPhoneStore iPhoneStore, boolean ipone){
if (ipone){
iPhoneStore.receive();
}else {
iPhoneStore.punish();
}
}
}
public class Me {
public void checkIphone(IPone11 iPone11,String storeName,App pinduoduo){
IPhoneStore iPhoneStore=new IPhoneStore();
iPhoneStore.setName(storeName);
iPhoneStore.setPintaiName(pinduoduo.getName());
if (iPone11.isImei()&&iPone11.isPingmu()){
pinduoduo.isGive(iPhoneStore,true);
}else {
pinduoduo.isGive(iPhoneStore,false);
}
}
}
public class Test {
public static void main(String[] args) {
Me me=new Me();
IPone11 iPone11=new IPone11();
iPone11.setImei(true);
iPone11.setPingmu(true);
me.checkIphone(iPone11,"苹果旗舰店",new App("拼多多"));
}
}
image.png
虽然结果是对的,但是我们看到Me这个类里面出现了一个陌生人,并且还和陌生人交谈了起来(想象一下要是自己家里突然出现了一个陌生人,那是不是挺吓人的),这个陌生人就是IPhoneStore类,这就使Me的耦合性提高了,增加了后期的维护成本,从而破坏了Me类的代码健壮性,这个就是破坏了迪米特法则的代价,代码改造后可以这样写:
public class Me {
public void checkIphone(IPone11 iPone11,String storeName,App pinduoduo){
pinduoduo.setStoreName(storeName);
if (iPone11.isImei()&&iPone11.isPingmu()){
pinduoduo.isGive(true);
}else {
pinduoduo.isGive(false);
}
}
}
public class App {
private String name;
private String storeName;
private IPhoneStore iPhoneStore;
public App(String name,IPhoneStore iPhoneStore) {
this.name = name;
this.iPhoneStore=iPhoneStore;
}
public void setStoreName(String storeName) {
this.storeName = storeName;
}
public void isGive(boolean iphone){
this.iPhoneStore.setPintaiName(name);
this.iPhoneStore.setName(storeName);
if (iphone){
iPhoneStore.receive();
}else {
iPhoneStore.punish();
}
}
}
public class Test {
public static void main(String[] args) {
Me me=new Me();
IPone11 iPone11=new IPone11();
iPone11.setImei(true);
iPone11.setPingmu(true);
me.checkIphone(iPone11,"苹果旗舰店",new App("拼多多",new IPhoneStore()));
}
}
结果
image.png
效果是一样的效果,但是Me在也不用担心家里出现陌生人,妈妈再也不用担心Me和陌生人聊天了
2、自己的事情自己做,不要老麻烦别人
如果一个方法可以放在本类,也可以放在其他类上,但是放在本类上,既不增加类间的关系,也不对本类产生负面影响那就放在本类
从上面的分析我们应该隐约能猜到到它的优点:
1、降低代码的耦合性
2、高内聚
3、提供代码可维护性
开闭原则
概念:
软件实体应该对修改关闭,对扩展开放
就只是短短的一句话就成为了六大原则中的精神领袖,23种设计模式以及面向对象编程的基本思想
这句话的具体含义就是软件实体如类、抽象、方法、模块应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化,实现开闭原则的核心思想就是面向抽象编程,可以利用依赖倒置原则实现,其实开闭原则都是其余五个原则的基本思想,它们只是开闭原则的实现方法之一
用抽象构建框架,用实现扩展细节
不多BB直接代码:
我去拼多多买IPhone11要5499
public interface Goods {
int getPrice();
String getName();
}
public class IPhone11 implements Goods{
private String name;
private int price;
public IPhone11() {
}
public IPhone11(String name, int price) {
this.name = name;
this.price = price;
}
@Override
public int getPrice() {
return this.price;
}
@Override
public String getName() {
return this.name;
}
}
public class Pinduoduo {
public static void main(String[] args) {
Goods iPhone11=new IPhone11("IPhone11",5499);
System.out.println(iPhone11.getName()+"在拼多多卖"+iPhone11.getPrice());
}
}
有一天拼多多突然来了百亿补贴,各种家电,手机都补贴500,IPhone11只卖4999,那简单啊, 我们的惯性思维那不是简单在IPhone11这个类中价格方法中减去补贴不就完事了吗,于是这个IPhone11类写成了这样:
ublic class IPhone11 implements Goods{
private String name;
private int price;
public IPhone11() {
}
public IPhone11(String name, int price) {
this.name = name;
this.price = price;
}
@Override
public int getPrice() {
return this.price-500;
}
@Override
public String getName() {
return this.name;
}
}
我们看这样其实也是没有问题,简单而且满足业务需求,但是这样通过修改来满足需求是不符合开闭原则的,而且这样随意修改原代码,对于代码的复用性就会下降,根据开闭原则我们应该通过扩展来实现修改,我们可以这么实现,增加一个IPhone11的衍生类,OffIPhon11:
public class OffIPhone11 extends IPhone11 {
public OffIPhone11(String name, int price) {
super(name, price);
}
@Override
public int getPrice() {
return super.getPrice()-500;
}
}
public class Pinduoduo {
public static void main(String[] args) {
Goods iPhone11=new OffIPhone11("IPhone11",5499);
System.out.println(iPhone11.getName()+"在拼多多卖"+iPhone11.getPrice());
}
}
image.png
image.png
这样我们通过对IPhone11的getPrice()方法的重写,达到了不对实现类进行代码修改就能实现修改的目的,这样提高了代码的复用性,把修改的风险扩散降到了最低,实现这样的效果其核心手段就是依赖倒置的面向接口编程和里氏替换原则的父类能出现的地方子类就能出现,并且不对系统造成任何影响,我们证实了,开闭原则的实现手段其实就是其他的五大原则
由上面的例子我们可以总结开闭原则的优点:
1、代码的复用性提升
2、面向接口编程,使代码的耦合性降低
3、关闭了修改,利用扩展实现修改,使代码的维护性提升
4、通过扩展降低修改带来的风险,原有代码不修改,使系统能保持原有的稳定性
网友评论