面向对象设计原则及常见设计模式的总结
参考文章
面向对象思想设计原则
- 单一职责原则
- 其实就是开发人员经常说的“高内聚,低耦合”
- 也就是说,每个类应该只有一个职责,对外只能提供一种功能,而引起类变化的原因应该只有一个。在设计模式中,所有的设计模式都应遵循这一原则。
- 开闭原则
- 核心思想是:一个对象对扩展开放,对修改关闭。
- 其实开闭原则的意思就是:对类的改动是通过增加代码进行的,而不是修改现有代码。
- 也就是说开发人员一旦写出了可以运行的代码,就不应该去改动它,而是要保证它能一直运行下去,如何能够做到这一点呢?这就需要借助于抽象和多态,即把可能变化的内容抽象出来,从而使抽象的部分是相对稳定的,而具体的实现则是可以改变和扩展的。
- 里氏替换原则
- 核心思想:在任何父类出现的地方都可以用它的子类来替代。
- 其实就是说:同一个继承体系中的对象应该有共同的行为特征。父类的功能子类也应该能实现。
- 依赖注入原则
- 核心思想:要依赖于抽象,不要依赖于具体实现。
- 其实就是说在应用程序中,所有的类如果使用或依赖于其他的类,则应该依赖这些其他类的抽象类,而不是这些其他类的具体实现类。为了实现这一原则,就要求我们在编程的时候针对抽象类或者接口编程,而不是针对具体实现编程。
- 接口分离原则
- 核心思想:不应该强迫程序依赖它们不需要使用的方法。
- 其实就是说:一个接口不需要提供太多的行为,一个接口应该只提供一种对外的功能,不应该把所有的操作都封装到一个接口中。
- 迪米特原则
- 核心思想:一个对象应当对其他对象尽可能少的了解
- 其实就是说:降低各个对象之间的耦合,提高系统的可维护性。在模块之间应该只通过接口编程,而不理会模块的内部工作原理,它可以使各个模块耦合度降到最低,促进软件的复用
设计模式
介绍
为了让我们的代码能更好地遵循上面的原则,从而达到易于维护的目的。我们就有必要了解一下设计模式。
设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。
设计模式不是一种方法和技术,而是一种思想
设计模式和具体的语言无关,学习设计模式就是要建立面向对象的思想,尽可能的面向接口编程,低耦合,高内聚,使设计的程序可复用
学习设计模式能够促进对面向对象思想的理解,反之亦然。它们相辅相成
设计模式的分类
我们可以将设计模式大致这样分类:
- **创建型模式(对象的创建):**简单工厂模式,工厂方法模式,抽象工厂模式,建造者模式,原型模式,单例模式。(6个)
- **结构型模式(对象的组成(结构)):**外观模式、适配器模式、代理模式、装饰模式、桥接模式、组合模式、享元模式。(7个)
- **行为型模式(对象的行为):**模版方法模式、观察者模式、状态模式、责任链模式、命令模式、访问者模式、策略模式、备忘录模式、迭代器模式、解释器模式。(10个)
常见的设计模式
设计模式的分类虽然很多,但是我们平时常用的也就下面的几种而已
- 简单工厂模式和工厂方法模式(接口)
- 模版设计模式(抽象类)
- 装饰设计模式(IO流)
- 单例设计模式(多线程)
- 建造者模式
- 适配器模式(GUI)
简单工厂模式
简单工厂模式,又叫静态工厂方法模式,它定义一个具体的工厂类负责创建一些类的实例。
比如,下面是一个简单工厂模式的例子:
/*
* 抽象的动物类,里面有抽象的方法
*/
public abstract class Animal {
public abstract void eat();
}
/*
* 具体的动物猫继承抽象动物类,重写抽象方法
*/
public class Cat extends Animal {
@Override
public void eat() {
System.out.println("猫吃鱼");
}
}
/*
* 具体的动物狗继承抽象动物类,重写抽象方法
*/
public class Dog extends Animal {
@Override
public void eat() {
System.out.println("狗吃骨头");
}
}
/*
* 动物工厂类,可以造猫和狗
*/
public class AnimalFactory {
private AnimalFactory() {
}
public static Animal createAnimal(String type) {
switch(type){
case "dog":
return new Dog();
case "cat":
return new Cat();
default:
return null;
}
}
}
public class AnimalDemo {
public static void main(String[] args) {
// 有了工厂之后,通过工厂创造动物
Animal a = AnimalFactory.createAnimal("dog");
a.eat();
a = AnimalFactory.createAnimal("cat");
a.eat();
}
}
运行后控制台结果:
狗吃骨头
猫吃鱼
我们运用了简单工厂模式后,不用每次用的时候去new对象,而是直接去调用这个工厂类里面的具体方法,它会给我们返回一个已经new好的对象。
我们来分析一下简单工厂模式的优点与缺点
- 优点
- 客户端不需要负责对象的创建,明确了各个类的职责。更有利于遵守单一职责原则。
- 缺点
- 这个静态工厂负责所有对象的创建。每当有新的对象增加,我们都需要去修改工厂类,这样不利于后期的维护。
工厂方法模式
工厂方法模式中抽象工厂类负责定义创建对象的接口,具体对象的创建工作由继承抽象工厂的具体类来实现。
我们将上面的例子改为工厂方法模式:
/*
* 抽象的动物类,里面有抽象的方法
*/
public abstract class Animal {
public abstract void eat();
}
/*
* 工厂类接口,里面有抽象的创造动物的方法
*/
public interface Factory {
public abstract Animal createAnimal();
}
/*
* 具体的猫类继承抽象动物类,重写抽象方法
*/
public class Cat extends Animal {
@Override
public void eat() {
System.out.println("猫吃鱼");
}
}
/*
* 猫工厂类实现工厂类并实现它的抽象方法,返回一个猫对象
*/
public class CatFactory implements Factory {
@Override
public Animal createAnimal() {
return new Cat();
}
}
/*
* 具体的狗类继承抽象动物类,重写抽象方法
*/
public class Dog extends Animal {
@Override
public void eat() {
System.out.println("狗吃肉");
}
}
/*
* 狗工厂类实现工厂类并实现它的抽象方法,返回一个狗对象
*/
public class DogFactory implements Factory {
@Override
public Animal createAnimal() {
return new Dog();
}
}
public class AnimalDemo {
public static void main(String[] args) {
// 需求:我要买只狗
Factory factory = new DogFactory();
Animal dog = factory.createAnimal();
dog.eat();
//需求:我要买只猫
factory = new CatFactory();
cat = factory.createAnimal();
cat.eat();
}
}
观察用工厂方法模式,会发现多了几个类。但是当我们这时要加入一种动物猪的时候,不再需要修改工厂类的代码,只需创建一个猪类继承抽象动物类,重写抽象方法,再创建一个猪的工厂类实现工厂类并实现它的抽象方法即可。这样的代码有很强的维护性和扩展性。
下面分析一下工厂方法模式的优点及缺点:
- 客户端不需要再负责对象的创建,明确了各个类的职责,如果有新的对象增加,只需要增加一个具体的类和具体的工厂类即可,不影响已有的代码,后期维护容易,增强了系统的扩展性。遵守了开闭原则,减少了修改。
- 需要额外编写代码,增加了我们的工作量。
单例模式
单例模式的出现是为了确保类在内存中只有一个对象,该实例必须自动创建,并且对外提供。
那么如何实现这个类在内存中仅有一个对象呢
- 构造私有
- 本身提供一个对象
- 通过公共的方法让外界访问
下面分别介绍单例模式中的懒汉式以及饿汉式。
饿汉式
饿汉式就是在类一加载时,就创建对象。
public class Student {
// 构造私有
private Student() {
}
// 自己造一个对象
// 静态方法只能访问静态成员变量,加静态
// 为了不让外界直接访问修改这个值,加private
private static Student s = new Student();
// 提供公共的访问方式
// 为了保证外界能够直接使用该方法,加静态
public static Student getStudent() {
return s;
}
}
public class StudentDemo {
public static void main(String[] args) {
// 通过单例得到对象
Student s1 = Student.getStudent();
Student s2 = Student.getStudent();
System.out.println(s1 == s2); //true
}
}
饿汉式的特点就是类一加载就创建对象,可以在代码中Student类中体现到。那么我们怎样才能在用这个对象的时候才去创建它呢,我们就要来看下懒汉式了。
懒汉式
懒汉式就是在对象使用的时候,再去创建对象.
public class Teacher {
private Teacher() {
}
private static Teacher teacher = null;
public static Teacher getTeacher() {
if (teacher == null) {
teacher = new Teacher();//当我们去用这个对象的时候才去创建它
}
return teacher;
}
}
public class TeacherDemo {
public static void main(String[] args) {
Teacher t1 = Teacher.getTeacher();
Teacher t2 = Teacher.getTeacher();
System.out.println(t1 == t2); //true
}
}
这样,就完成了懒汉式单例。可以看到,它是在使用到对象的时候,才去创建的。
对比
饿汉式我们经常在开发中使用,它是不会出问题的单例模式
懒汉式我们在回答时用,它是可能会出问题的单例模式。
关于单例模式需要了解的思想:
- 懒加载思想(延迟加载)
- 线程安全问题(考虑下面3个方面)
- 是否多线程环境
- 是否有共享数据
- 是否有多条语句操作共享数据
如果都是,就会存在线程的安全问题,我们上面的懒汉式代码是不完整的,应该给对象中的方法加上synchronized关键字,这样才算完整。
public synchronized static Teacher getTeacher() {
if (teacher == null) {
teacher = new Teacher();
}
return teacher;
}
模板方法模式
模版方法模式就是定义一个算法的骨架,而将具体的算法延迟到子类中来实现。
下面是模板方法的示例:
步骤1: 创建抽象模板结构(Abstract Class):炒菜的步骤
public abstract class Abstract Class {
//模板方法,用来控制炒菜的流程 (炒菜的流程是一样的-复用)
//声明为final,不希望子类覆盖这个方法,防止更改流程的执行顺序
final void cookProcess(){
//第一步:倒油
this.pourOil();
//第二步:热油
this.HeatOil();
//第三步:倒蔬菜
this.pourVegetable();
//第四步:倒调味料
this.pourSauce();
//第五步:翻炒
this.fry();
}
//定义结构里哪些方法是所有过程都是一样的可复用的,哪些是需要子类进行实现的
//第一步:倒油是一样的,所以直接实现
void pourOil(){
System.out.println("倒油");
}
//第二步:热油是一样的,所以直接实现
void HeatOil(){
System.out.println("热油");
}
//第三步:倒蔬菜是不一样的(一个下包菜,一个是下菜心)
//所以声明为抽象方法,具体由子类实现
abstract void pourVegetable();
//第四步:倒调味料是不一样的(一个下辣椒,一个是下蒜蓉)
//所以声明为抽象方法,具体由子类实现
abstract void pourSauce();
//第五步:翻炒是一样的,所以直接实现
void fry();{
System.out.println("炒啊炒啊炒到熟啊");
}
}
步骤2: 创建具体模板(Concrete Class),即”手撕包菜“和”蒜蓉炒菜心“的具体步骤
//炒手撕包菜的类
public class ConcreteClass_BaoCai extend Abstract Class{
@Override
public void pourVegetable(){
System.out.println(”下锅的蔬菜是包菜“);
}
@Override
public void pourSauce(){
System.out.println(”下锅的酱料是辣椒“);
}
}
//炒蒜蓉菜心的类
public class ConcreteClass_CaiXin extend Abstract Class{
@Override
public void pourVegetable(){
System.out.println(”下锅的蔬菜是菜心“);
}
@Override
public void pourSauce(){
System.out.println(”下锅的酱料是蒜蓉“);
}
}
**步骤3: **客户端调用
public class Template Method{
public static void main(String[] args){
//炒 - 手撕包菜
ConcreteClass_BaoCai BaoCai = new ConcreteClass_BaoCai();
BaoCai.cookProcess();
//炒 - 蒜蓉菜心
ConcreteClass_ CaiXin = new ConcreteClass_CaiXin();
CaiXin.cookProcess();
}
}
这样就成功实现了模板方法模式
下面分析一下它的优点及缺点:
- 优点
- 提高代码复用性
将相同部分的代码放在抽象的父类中 - 提高了拓展性
将不同的代码放入不同的子类中,通过对子类的扩展增加新的行为 - 实现了反向控制
通过一个父类调用其子类的操作,通过对子类的扩展增加新的行为,实现了反向控制 & 符合“开闭原则”
- 提高代码复用性
- 缺点
- 引入了抽象类,每一个不同的实现都需要一个子类来实现,导致类的个数增加,从而增加了系统实现的复杂度。
装饰模式
装饰模式:在不必改变原类文件和使用继承的情况下,动态地扩展一个对象的功能。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。
装饰模式中有这四个角色:抽象构件、具体构件、抽象装饰类 、具体装饰类
下面是装饰模式的一个示例
组件类
abstract class Component {
public abstract void display();
}
组件装饰者
public class ComponentDecorator extends Component{
private Component component; // 维持对抽象构件类型对象的引用
public ComponentDecorator(Component component){
this.component = component;
}
public void display() {
component.display();
}
}
继承类ListBox
public class ListBox extends Component{
public void display() {
System.out.println("显示列表框!");
}
}
继承类TextBox
public class TextBox extends Component{
public void display() {
System.out.println("显示文本框!");
}
}
黑框装饰者
public class BlackBoarderDecorator extends ComponentDecorator{
public BlackBoarderDecorator(Component component) {
super(component);
}
public void display() {
this.setBlackBoarder();
super.display();
}
public void setBlackBoarder() {
System.out.println("为构件增加黑色边框!");
}
}
滚动条装饰者
public class ScrollBarDecorator extends ComponentDecorator{
public ScrollBarDecorator (Component component) {
super(component); // 调用父类构造函数
}
public void display() {
this.setScrollBar();
super.display();
}
public void setScrollBar() {
System.out.println("为构件增加滚动条!");
}
}
客户端调用
public class Client {
public static void main(String args[]) {
Component component,componentSB,componentBB;
component = new Window();
componentSB = new ScrollBarDecorator(component);
componentSB.display();
componentBB = new BlackBoarderDecorator(componentSB);
componentBB.display();
}
}
对装饰模式举个例子
IO流中就用到了装饰模式
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter((new OutputStreamWriter(System.out)));
下面分析一下装饰模式的优点及缺点:
- 优点
- 使用装饰模式,可以提供比继承更灵活的扩展对象的功能,它可以动态的添加对象的功能,并且可以随意的组合这些功能
- 缺点
- 正因为可以随意组合,所以就可能出现一些不合理的逻辑
适配器模式
适配器模式把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够一起工作。
适配器模式分为两种
- 类适配器模式 :通过实现Target接口以及继承Adaptee类来实现接口转换
- 对象适配器模式:实现Target接口和代理Adaptee的某个方法来实现接口转换
角色介绍
- 目标(Target)角色:这就是所期待得到的接口。注意:由于这里讨论的是类适配器模式,因此目标不可以是类。
- 源(Adaptee)角色:现在需要适配的接口。
- 适配器(Adapter)角色:适配器类是本模式的核心。适配器把源接口转换成目标接口。显然,这一角色不可以是接口,而必须是具体类。
类适配器模式
类的适配器模式是把适配的类的API转换成为目标类的API。
步骤1.创建Target接口
public interface Target {
public void Request();
}
步骤2.创建需要适配的类Adaptee
public class Adaptee {
public void SpecificRequest(){
}
}
步骤3.创建适配器类Adapter
//适配器Adapter继承自Adaptee,同时又实现了目标(Target)接口。
public class Adapter extends Adaptee implements Target {
//目标接口要求调用Request()这个方法名,但源类Adaptee没有方法Request()
//因此适配器补充上这个方法名
//但实际上Request()只是调用源类Adaptee的SpecificRequest()方法的内容
//所以适配器只是将SpecificRequest()方法作了一层封装,封装成Target可以调用的Request()而已
@Override
public void Request() {
this.SpecificRequest();
}
}
步骤4.定义使用目标类,并通过Adapter类调用所需要的方法从而实现目标
public class AdapterPattern {
public static void main(String[] args){
Target mAdapter = new Adapter();
mAdapter.Request();
}
}
这样,就达到了我们的目的。让Target成功使用了Adaptee类的specificRequest方法。
对象适配器模式
与类的适配器模式相同,对象的适配器模式也是把适配的类的API转换成为目标类的API。
步骤1.创建Target接口
public interface Target {
public void Request();
}
步骤2.创建需要适配的类Adaptee
public class Adaptee {
public void SpecificRequest(){
}
}
步骤3.创建适配器类Adapter(使用包含的方式)
public class Adapter implements Target{
private Adaptee adaptee;
// 通过构造函数传入具体需要适配的被适配类对象
public Adapter(Adaptee adaptee) {
this.adaptee = adaptee;
}
@Override
public void Request() {
// 使用委托的方式完成特殊功能
this.adaptee.SpecificRequest();
}
}
步骤4.定义使用目标类,并通过Adapter类调用所需要的方法从而实现目标
public class AdapterPattern {
public static void main(String[] args){
//需要先创建一个被适配类的对象作为参数
Target mAdapter = new Adapter(new Adaptee());
mAdapter.Request();
}
}
两种模式的对比
- 类适配器模式
- 优点
- 使用方便,代码简化
仅仅引入一个对象,并不需要额外的字段来引用Adaptee实例
- 使用方便,代码简化
- 缺点
- 高耦合,灵活性低
使用对象继承的方式,是静态的定义方式
- 高耦合,灵活性低
- 优点
- 对象适配器模式
- 优点
- 灵活性高、低耦合
采用 “对象组合”的方式,是动态组合方式
- 灵活性高、低耦合
- 缺点
- 使用复杂
需要引入对象实例
- 使用复杂
- 优点
适配器模式的优缺点分析
-
优点
- 更好的复用性
系统需要使用现有的类,而此类的接口不符合系统的需要。那么通过适配器模式就可以让这些功能得到更好的复用。 - 透明、简单
客户端可以调用同一接口,因而对客户端来说是透明的。这样做更简单 & 更直接 - 更好的扩展性
在实现适配器功能的时候,可以调用自己开发的功能,从而自然地扩展系统的功能。 - 解耦性
将目标类和适配者类解耦,通过引入一个适配器类重用现有的适配者类,而无需修改原有代码 - 符合开放-关闭原则
同一个适配器可以把适配者类和它的子类都适配到目标接口;可以为不同的目标接口实现不同的适配器,而不需要修改待适配类
- 更好的复用性
-
缺点
- 过多的使用适配器,会让系统非常零乱,不易整体进行把握
建造者模式
建造者模式可以将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示 、
下面是以组装电脑为例的实例
步骤1: 定义组装的过程(Builder):组装电脑的过程
public abstract class Builder {
//第一步:装CPU
//声明为抽象方法,具体由子类实现
public abstract void BuildCPU();
//第二步:装主板
//声明为抽象方法,具体由子类实现
public abstract void BuildMainboard();
//第三步:装硬盘
//声明为抽象方法,具体由子类实现
public abstract void BuildHD();
//返回产品的方法:获得组装好的电脑
public abstract Computer GetComputer();
}
步骤2: 电脑城老板委派任务给装机人员(Director)
public class Director{
//指挥装机人员组装电脑
public void Construct(Builder builder){
builder. BuildCPU();
builder.BuildMainboard();
builder. BuildHD();
}
}
步骤3: 创建具体的建造者(ConcreteBuilder):装机人员
//装机人员1
public class ConcreteBuilder extend Builder{
//创建产品实例
Computer computer = new Computer();
//组装产品
@Override
public void BuildCPU(){
computer.Add("组装CPU")
}
@Override
public void BuildMainboard(){
computer.Add("组装主板")
}
@Override
public void BuildHD(){
computer.Add("组装主板")
}
//返回组装成功的电脑
@Override
public Computer GetComputer(){
return computer
}
}
步骤4: 定义具体产品类(Product):电脑
public class Computer{
//电脑组件的集合
private List<String> parts = new ArrayList<String>();
//用于将组件组装到电脑里
public void Add(String part){
parts.add(part);
}
public void Show(){
for (int i = 0;i<parts.size();i++){
System.out.println(“组件”+parts.get(i)+“装好了”);
}
System.out.println(“电脑组装完成,请验收”);
}
}
步骤5: 客户端调用
public class Builder Pattern{
public static void main(String[] args){
//逛了很久终于发现一家合适的电脑店
//找到该店的老板和装机人员
Director director = new Director();
Builder builder = new ConcreteBuilder();
//沟通需求后,老板叫装机人员去装电脑
director.Construct(builder);
//装完后,组装人员搬来组装好的电脑
Computer computer = builder.GetComputer();
//组装人员展示电脑
computer.Show();
}
}
下面我们来介绍一下建造者模式的优缺点
- 优点
- 易于解耦
将产品本身与产品创建过程进行解耦,可以使用相同的创建过程来得到不同的产品。也就说细节依赖抽象。 - 易于精确控制对象的创建
将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰 - 易于拓展
增加新的具体建造者无需修改原有类库的代码,易于拓展,符合“开闭原则“。
- 易于解耦
- 缺点
- 建造者模式所创建的产品一般具有较多的共同点,其组成部分相似;如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。
- 如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大。
广告时间
我是N0tExpectErr0r,一名广东工业大学的大二学生
欢迎来到我的个人博客,所有文章均在个人博客中同步更新哦
http://blog.N0tExpectErr0r.cn