设计模式原则,即程序员在编程时,应当遵守的原则
1.单一职责原则
简单理解就是一个类A负责两个职责A1和A2,当A1需求变更时A2可能会遭到破坏,此时需要将A的职责细粒度化
public class Hobby {
public void hobby1(String name){
System.out.println(name+"的爱好是:唱");
}
}
public class DemoTest {
public static void main(String[] args) {
Hobby hobby = new Hobby();
hobby.hobby1("张三");
hobby.hobby1("李四");
}
}
以上代码如果李四的爱好是跳呢?修改hobby1为跳但是张三的爱好又不能兼顾,改进增加一个hobb2的类,提供跳的爱好
public class Hobby2 {
public void hobby1(String name){
System.out.println(name+"的爱好是:跳");
}
}
public class DemoTest {
public static void main(String[] args) {
Hobby hobby = new Hobby();
hobby.hobby1("张三");
Hobby2 hobby2 = new Hobby2();
hobby2.hobby1("李四");
}
}
但是这样的做法开销实在太大,不但将类分解,还要对客户端进行改变,于是我们可以直接修改Hobby类
public class Hobby {
public void hobby1(String name){
System.out.println(name+"的爱好是:唱");
}
public void hobby2(String name){
System.out.println(name+"的爱好是:跳");
}
public void hobby3(String name){
System.out.println(name+"的爱好是:rap");
}
}
public class DemoTest {
public static void main(String[] args) {
Hobby hobby = new Hobby();
hobby.hobby1("张三");
//Hobby2 hobby2 = new Hobby2();
hobby.hobby2("李四");
hobby.hobby3("老蔡");
}
}
如此,我们虽然在类级别违反了单一职责,但是在方法级别遵守了单一职责原则,好处是显而易见的,我们没有对原来的类做很大的改动,只是增加了方法。
总结:
1.一个类只负责一项职责
2.提高代码的可读性,可维护性
3.降低未知的变更风险
4.在类方法比较少的时候可以在方法级别遵守单一职责原则
2.接口隔离原则
(1)使用多个专门的接口比使用单一的总接口要好。
(2)一个类对另外一个类的依赖性应当是建立在最小的接口上的。
(3)一个接口代表一个角色,不应当将不同的角色都交给一个接口。没有关系的接口合并在一起,形成一个臃肿的大接口,这是对角色和接口的污染。
(3)“不应该强迫客户依赖于它们不用的方法。接口属于客户,不属于它所在的类层次结构。”这个说得很明白了,再通俗点说,不要强迫客户使用它们不用的方法,如果强迫用户使用它们不使用的方法,那么这些客户就会面临由于这些不使用的方法的改变所带来的改变。
现在我们有一个reading接口,两个实现类A和B,MiddleStudent通过reading接口依赖A来读化学和读语文,CollegeStudent通过reading接口依赖B实现读化学和读高数
public interface Reading {
void chinese();
void Chemistry();
void highNumber();
}
public class ReadingimplA implements Reading {
@Override
public void chinese() {
System.out.println("语文");
}
@Override
public void Chemistry() {
System.out.println("化学");
}
@Override
public void highNumber() {
System.out.println("高数");
}
}
public class ReadingimplB implements Reading {
@Override
public void chinese() {
System.out.println("语文");
}
@Override
public void Chemistry() {
System.out.println("化学");
}
@Override
public void highNumber() {
System.out.println("高数");
}
}
public class CollegeStudent {
public void readChemistry(Reading reading){
reading.Chemistry();
};
public void readHighNumber(Reading reading){
reading.highNumber();
};
}
public class MiddleStudent{
public void readChemistry(Reading reading){
reading.Chemistry();
};
public void readingChinese(Reading reading){
reading.chinese();
};
}
但是A和B却都要实现reading的所有方法,实际上A不需要实现读高数的方法,同理B不需要实现读语文的方法,此时需要将reading接口拆分成两个接口
为了模拟更复杂的情况增加Reading3,具有读英语的能力
先把原来的一个接口分为三个
public interface Reading1{
void chinese();
void Chemistry();
}
public interface Reading2 {
void Chemistry();
void highNumber();
}
public interface Reading3 {
void English();
}
public class Readingimpl1 implements Reading1,Reading3{
//实现reading1和reading3
@Override
public void chinese() {
System.out.println("语文");
}
@Override
public void Chemistry() {
System.out.println("化学");
}
@Override
public void English() {
System.out.println("英语");
}
}
public class Readingimpl2 implements Reading2,Reading3 {
//实现reading2和reading3
@Override
public void Chemistry() {
System.out.println("化学");
}
@Override
public void highNumber() {
System.out.println("高数");
}
@Override
public void English() {
System.out.println("英语");
}
}
public class CollegeStudent {
//大学生通过接口2和3依赖readingimpl2
public void readChemistry(Reading2 reading2){
reading2.Chemistry();
};
public void readHighNumber(Reading2 reading2){
reading2.highNumber();
};
public void readingEnglish(Reading3 reading3){
reading3.English();
};
}
public class MiddleStudent{
//中学生通过接口1和3依赖readingimpl1
public void readChemistry(Reading1 reading1){
reading1.Chemistry();
};
public void readingChinese(Reading1 reading1){
reading1.chinese();
};
public void readingEnglish(Reading3 reading3){
reading3.English();
};
}
public class DemoTest {
public static void main(String[] args) {
CollegeStudent collegeStudent = new CollegeStudent();
MiddleStudent middleStudent = new MiddleStudent();
collegeStudent.readChemistry(new Readingimpl2());
collegeStudent.readHighNumber(new Readingimpl2());
collegeStudent.readingEnglish(new Readingimpl2());
middleStudent.readChemistry(new Readingimpl1());
middleStudent.readingChinese(new Readingimpl1());
middleStudent.readingEnglish(new Readingimpl1());
}
}
原本的reading接口有四个功能,被分为了三个接口,这样实现类只需要实现自己需要的方法即可,做到了接口隔离
3.依赖倒转原则
1、上层模块不应该依赖底层模块,它们都应该依赖于抽象。
2、抽象不应该依赖于细节,细节应该依赖于抽象。
3、中心思想即面向接口编程,但这里更强调抽象
在面向过程的开发,上层调用下层,上层依赖于下层,当下层剧烈变动时上层也要跟着变动,这就会导致模块的复用性降低而且大大提高了开发的成本。
而面向对象的开发很好的解决了这个问题,一般情况下抽象的变化概率很小,让用户程序依赖于抽象,实现的细节也依赖于抽象。即使实现细节不断变动,只要抽象不变,客户程序就不需要变化。这大大降低了客户程序与实现细节的耦合度。
如果有一个Person类,具有玩LOL的行为playLOL
最简单的实现方式是这样的
public class Person {
void playLOL(LOL lol) {
lol.play();
}
}
public class LOL {
void play(){
System.out.println("lol!");
}
}
但是如果这个人要玩王者荣耀呢?于是自然而然的又创建了王者荣耀的类…person类中添加对应的玩王者荣耀的方法…
结果是每多一个想玩的游戏就要新建一个类,并添加对应的方法,但是我们发现无论什么游戏都是游戏,其实我们可以写一个玩游戏的方法playGames!Person的play方法接收一个Game类,这样就不需要修改Person中的方法了
public class Person2 {
void playGames(Game game) {
game.play();
}
}
public interface Game {
void play();
}
public class Game1 implements Game {
@Override
public void play() {
System.out.println("play game1");
}
}
public class Game2 implements Game {
@Override
public void play() {
System.out.println("play game2");
}
}
public class DemoTest {
public static void main(String[] args) {
Person2 p2 = new Person2();
p2.playGames(new Game1());
p2.playGames(new Game2());
}
}
这样,如果想要玩刀塔,只需新建刀塔类实现Game接口就好了。
总结:
1.低层模块尽量都要有抽象类或者接口,或者两者都有,这样稳定性较好
2.变量的声明类型尽量使抽象类或者接口,是的变量引用和实际对象间存在一个缓冲层,利于程序扩展和优化
3.继承时遵循里氏替换原则
4.里氏替换原则
里氏替换原则,OCP作为OO的高层原则,主张使用“抽象(Abstraction)”和“多态(Polymorphism)”将设计中的静态结构改为动态结构,维持设计的封闭性。“抽象”是语言提供的功能。“多态”由继承语义实现。
里氏替换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 里氏替换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。
如此,问题产生了:“我们如何去度量继承关系的质量?”
Liskov于1987年提出了一个关于继承的原则“Inheritance should ensure that any property proved about supertype objects also holds for subtype objects.”——“继承必须确保超类所拥有的性质在子类中仍然成立。”也就是说,当一个子类的实例应该能够替换任何其超类的实例时,它们之间才具有is-A关系。
该原则称为Liskov Substitution Principle——里氏替换原则。
下面看一下代码
public class Father {
String name = "老布什";
String home = "三里屯";
int age = 44;
void getAge(){
System.out.println(age);
}
void getHome(){
System.out.println(home);
}
}
public class Son extends Father {
String name = "小布什";
String home = "自立门户";
int age = 11;
@Override
void getAge() {
System.out.println(age);;
}
void getHome(){
System.out.println(home);
}
}
public class DemoTest {
public static void main(String[] args) {
Father father = new Father();
father.getHome();
Son son = new Son();
son.getHome();
}
}
上面的例子不是很恰当,只是用来说明不遵循里氏替换原则可能的后果。我们假设小布什8岁还跟老布什住在一起,那么代码应该打印出两个"三里屯",但11岁时突然发现小布什自立门户了?这不科学,原来是小布什对getHome进行了重写(可能是不经意),好吧小布什重写是方便了爽了自己,但是对我的预期结果却产生了很大的影响甚至造成后续代码的出错!可孩子长大了就是要自立门户,怎么办?怎么办?怎么办?
将原有的父类和子类都继承一个更通俗的父类,原有的继承关系去掉,用依赖聚合组合等关系代替
public class Person {
//放一些最基础的属性或者方法
String country = "USA";
void getCountry() {
System.out.println(country);
}
}
public class Father extends Person{
String name = "老布什";
String home = "三里屯";
int age = 44;
void getAge(){
System.out.println(age);
}
@Override
void getHome(){
System.out.println(home);
}
}
public class Son extends Person {
//还想拥有老布什的一些方法或者属性,使用组合
private Father father = new Father();
String name = "小布什";
String home = "自立门户";
int age = 33;
void getAge() {
System.out.println(age);;
}
@Override
void getHome(){
System.out.println(home);
}
//还是想跟老布什一起住
void getHome2(){
father.getHome();
}
}
public class DemoTest {
public static void main(String[] args) {
Father father = new Father();
//明确知道要获得father的home
father.getHome();
Son son = new Son();
//son不再继承与father明确知道要获得son的home
son.getHome();
//如果son想要知道father的home使用组合,获得father的home
son.getHome2();
}
}
这样小布什就可以自立门户也可以选择跟老布什一起住了并且不存在一些潜在的风险。
总结:
使用继承时,尽量不要重写父类的方法
继承实际上使代码的耦合性增强了,在适当的情况下,可以通过聚合,组合,依赖来解决问题。
5.开闭原则
定义:一个软件实体。如类/模块/函都应该对扩展开放,对修改关闭。
软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现。
其实只要遵循其他的设计模式原则,设计出来的软件就是符合开闭原则的。
用抽象构建架构,用实现扩展细节
因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保证架构的稳定。而软件中易变的细节,我们用从抽象派生的实现类来进行扩展,当软件需要发生变化时,我们只需要根据需求重新派生一个实现类来扩展就可以了,当然前提是抽象要合理,要对需求的变更有前瞻性和预见性。
有如下代码:
/**
* 状态修改类(使用方)
*/
public class StatusModify {
private int status = 0;
/**
* 根据type来修改对应的状态
* @param order
*/
public void updateStatus(Order order){
if(order.type == 1){
status = 1;
}else if(order.type == 99){
status = 2;
}
}
}
public class Order {
int type;
}
public class CancledOrder extends Order{
public CancledOrder(){
type = 99;
}
}
public class CreatedOrder extends Order{
public CreatedOrder(){
type = 1;
}
}
public class DemoTest {
public static void main(String[] args) {
StatusModify statusModify = new StatusModify();
statusModify.updateStatus(new CancledOrder());
statusModify.updateStatus(new CreatedOrder());
}
}
代码很简单,但是现在要加一个FinishedOrder,就需要在使用方StatusModify 中新加一个else if 的判断,当这样的需求越来越多,代码维护越来越乱,不易于维护,切违背了对修改关闭的原则,这时候就可以用抽象构建架构,用实现扩展细节我们只需要根据需求重新派生一个实现类来扩展就可以了,当然前提是抽象要合理,要对需求的变更有前瞻性和预见性。
/**
* 状态修改类(使用方)
*/
public class StatusModify {
private int status = 0;
/**
* 根据具体的order实现来变更状态
* @param order
*/
public void updateStatus(Order order){
status = order.statusModify();
}
}
public abstract class Order {
int type;
public abstract int statusModify();
}
public class CancledOrder extends Order {
public CancledOrder(){
type = 99;
}
@Override
public int statusModify( ) {
return type;
}
}
public class CreatedOrder extends Order {
public CreatedOrder(){
type = 1;
}
@Override
public int statusModify() {
return type;
}
}
当我们需要作出扩展时,只需要新加一个FinishedOrder即可
public class FinishedOrder extends Order {
public FinishedOrder(){
type = 2;
}
@Override
public int statusModify() {
return type;
}
}
总结:开闭原则是其他原则的目的。
单一职责原则告诉我们实现类要职责单一;
里氏替换原则告诉我们不要破坏继承关系;
依赖倒置原则告诉我们要面向接口编程;
接口隔离原则告诉我们在设计接口的时候要精简单一;
迪米特法则告诉我们要降低耦合
开闭原则告诉我们要对扩展开发,对修改关闭;
6.迪米特原则
1.迪米特法则(Law ofDemeter,LoD)又叫最小知识原则(least knowledge principle,lkp),定义为:一个对象对其它对象尽可能少的理解。
2.类与类关系越密切,耦合度越大,迪米特法则简单理解为:只和朋友通信
3.朋友的定义:出现在成员变量,方法入参出参位置的类为直接朋友,出现在局部变量位置的类不是直接朋友。
代码示例:hr手机简历给主管,主管筛选需要的简历
public class Resume {
private String name;
private int id;
public Resume(String name, int id) {
this.name = name;
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Resume{" +
"name='" + name + '\'' +
", id=" + id +
'}';
}
}
public class HR {
List<Resume> list = new ArrayList<>();
public List<Resume> getList() {
return list;
}
public void setList(List<Resume> list) {
this.list = list;
}
/**
* 收集简历
* @return
*/
public void collectResume(){
list.add(new Resume("张三",1));
list.add(new Resume("李四",2));
list.add(new Resume("王五",3));
}
}
public class Supervisor {
/**
* 筛选简历
*/
public void filterResume(HR hr){
//违法迪米特法则,Supervisor的直接朋友为HR,而Resume不是直接朋友
for (Resume resume : hr.getList()) {
System.out.println(resume.toString());
}
}
}
public class DemoTest {
public static void main(String[] args) {
Supervisor supervisor = new Supervisor();
HR hr = new HR();
hr.collectResume();
supervisor.filterResume(hr);
}
}
上述代码中Supervisor类的直接朋友为HR,而Resume不是直接朋友,显然违反迪米特法则,对于Supervisor和Resume来讲,HR是第三者,可以把筛选简历的工作交给hr,主管只要最终结果即可。
HR类添加刷选简历的方法改进为
public class HR {
List<Resume> list = new ArrayList<>();
public List<Resume> getList() {
return list;
}
public void setList(List<Resume> list) {
this.list = list;
}
/**
* 收集简历
* @return
*/
public void collectResume(){
list.add(new Resume("张三",1));
list.add(new Resume("李四",2));
list.add(new Resume("王五",3));
}
/**
* 筛选简历
*/
public void filterResume(){
for (Resume resume : list) {
System.out.println(resume.toString());
}
}
}
Supervisor类改进为
public class Supervisor {
/**
* 筛选简历
*/
public void filterResume(HR hr){
hr.filterResume();
}
}
注意:迪米特要求减少类与类之间不必要的依赖关系,只是为了降低耦合,并不要求完全没有依赖关系
7.合成复用原则
尽量使用组合,聚合的方式,而不是继承。
1、把应用中可能变化的地方独立出来,不要和不变的代码混一起
2、针对接口编程,而不是实现
3、为了实现交互对象之间的松耦合而努力
代码举例可以参照里氏替换原则,依赖倒转原则等示例,如果B类只是为了使用A类的方法,只需要把A作为B的成员变量(成员变量引用方式)、构造函数传入一个A的实例、方法参数传入一个A的实例而不是使用继承。