简书 賈小強
转载请注明原创出处,谢谢!
这篇文章焦点并不在于继承以及多态性或者各种炫酷的设计模式上面,也不在与对未来的预测,而在于分析最基本的封装,提取类,组合
人遛狗的最简形式
public class People {
public static void main(String[] args) {
String name="小黑";
System.out.println("["+name+"]"+" 汪汪汪...");
}
}
如果问题本身就很简单,就不要搞复杂了
提取方法(Extract Method)
情况复杂一点,将name参数化,提取一个方法
public class People {
public void runDog(String name) {
System.out.println("["+name+"]"+" 汪汪汪...");
}
public static void main(String[] args) {
String[] names={"小黑","小花","小黄"};
for(String name: names){
people.runDog(name);
}
}
}
只要存在围绕数据的一个处理方法,那么提取方法不错
提取类 (Extract Class)
可能狗变得更复杂了一些,于是出现了提取一个Dog类,如下面向对象版本的人遛狗,People类依赖Dog类(组合)
public class Dog {
private String name;
private Integer age
public Dog(String name,Integer age) {
this.name = name;
this.age=age;
}
public void bark() {
System.out.println("[name: "+this.name+" age: "+this.age+"] 汪汪汪...");
}
public void feed() {
System.out.println("[name: "+this.name+" age: "+this.age+"] 吃东西...");
}
}
public class People {
public void runDog() {
Dog dog = new Dog("小黑",2);
dog.bark();
}
public static void main(String[] args) {
People people = new People();
people.runDog();
}
}
如果以几个数据项为单元进行处理,那么这几个数据项和对应的处理方法提取为一个类不错
- 比如这里的bark和feed方法都需要使用name,age,如果用面向过程的代码,那么将需要给这两个方法都传递相同的变量,或者如果是引用传递,或者方法需要返回多个值,这样的话不如将这几项参数变成字段,从而多个方法可见,这些字段对类中的方法算全局变量
- 比如dog1和dog2进行比较,那不如提取一个类将equal方法放在这个类中
适度完美
只有需要的时候才使用额外的抽象,因为需要而使用,而不是为使用而使用,不用期望一步到位直接就设计出了完美的设计,设计本身就是反复迭代分析的结果,而且能拆就能建,能建就能拆,不用定位太高,首先代码能用就好,最原始的设计无论是纯面向过程也好,是蹩脚的面向对象也罢,之后都还能重构
不过可以不用,但是不能不知得失,所以下面将继续深入分析
提取方法和提取类比较
Extract Method | Extract Class |
---|---|
面向过程程序设计 | 面向对象程序设计 |
以直接容易的方式将大代码段分解成小代码段 | 更抽象化,高层的方式让代码更内聚,清晰 |
每个方法具有自己的局部变量 | 每个类具有实例方法共享的字段 |
简单的将数据以及一个数据相关的操作封装起来 | 将多项数据以及多个相关操作封装起来 |
封装抽象的机制是方法 | 封装抽象的机制是类 |
正向面向对象设计与分析
正常情况下,面向对象的程序设计的过程可能包含如下步骤
功能->用例图->问题分解->需求->领域分析->初步设计->实现->交付
这一系列过程,在领域分析的过程中可能用文本分析的方式,名词可能是候选类,动词可能是类的方法,以这样正向的方式来设计出面向对象的程序
面向抽象编程
高层 | 低层 |
---|---|
比如人遛狗,这3个字在一个高层上面,高层不关心低层,高层只有人遛狗的逻辑 | 至狗怎么被溜,只需要低层实现了那样的接口(有被溜的方法可以调用)就行 |
这种分层,抽象的思想在正向分析里面非常有用,这里重点不是面向接口之后,子类利用多态性达到灵活可变的能力,而在于分层,能够在更高的层次上编码,不用干什么都需要接触底层细节
实际上如果按照这种面向抽象的正向分析建模,那么代码里面的数据和数据对应的操作总是相距不远,有时候不得不叹服自然语言和机器语言的不谋而合,相反直接按照机器语言即面向过程的思想编码,在可能一叶障目,在细节上纠结,陷入泥潭
封装变化
有一种说法是将容易变化的部分封装起来,也就是封装变化
Extract Method | Extract Class |
---|---|
当然进行这种重构之后,如果某个方法的算法变了,修改一个小方法总比修改一大段方法容易,但提取方法更加总要的是作用让当下代码更加清晰,容易理解 ,而不是重用或者应对变化 | 不可否则它可以将容易变化的部分封装到一个类里面,让未来更加容易只修改一个类中的代码而不用同时修改多个类,但未来不是重点,我想要的是当下如何设计让程序更加清晰,容易编写,不对未来做过多假设 |
结论:我不反对封装变化(如果第二次出现同样的变化,那就不要被同一颗子弹击中两次,更加准确的意思是第一次不对未来做过多假设),我更在乎现在,于是我的问题更加准确应该是探讨如何以当下为目标进行逆向面向对象设计与分析
如果狗只有一个字段
形式一:
public class Dog {
private String name;
public Dog(String name) {
this.name = name;
}
public void bark() {
System.out.println("["+this.name+"] 汪汪汪...");
}
}
public class People {
public void runDog() {
Dog dog = new Dog("小黑");
dog.bark();
}
public static void main(String[] args) {
People people = new People();
people.runDog();
}
}
形式二:
public class People {
public void runDog(String name) {
System.out.println("["+name+"] 汪汪汪...");
}
public static void main(String[] args) {
People people = new People();
people.runDog("小黑");
}
}
可以比较上面两种形式的代码,发现封装一个字段并没带来很明显好处
结论:一个类只封装一个字段会丧失对数据个数减少的优点,甚至额外多了一层类,反而复杂了,不过还是具有使类中字段对类的实例方法公用的价值
如果狗有两个字段
public class Dog {
private String name;
private int age;
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
public void bark() {
System.out.println("["+this.name+":"+ this.age+"岁] 汪汪汪...");
}
}
结论:针对狗有几个字段才能提出为一个类这个问题,首先必须承认的一点是,类可以将多个数据项封装成一个对象的强大能力,而让高层代码在几个变量组合起来的概念上进行编码,而这里想搞清楚的是封装几个字段为好,上面封装两个字段的方式就数量上来说确实取得了价值,鉴于《重构》中Data Clumps部分所讲只要新对象取代两个(或者更多)字段,你就值回票价了
如果只有一条狗
一个类可以实例化多个独立的不同对象,每个对象中都保存了各自的数据,这是面向对象封装所带来的好处,形如
public class Dog {
private String name;
private int age;
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void bark() {
System.out.println("["+this.name+":"+ this.age+"岁] 汪汪汪...");
}
}
import java.util.ArrayList;
public class People {
private ArrayList<Dog> dogs = new ArrayList<>();
public People() {
ArrayList<Dog> dogs = new ArrayList<>();
Dog dog1 = new Dog("小黑", 1);
Dog dog2 = new Dog("小花", 2);
Dog dog3 = new Dog("小黄", 3);
dogs.add(dog1);
dogs.add(dog2);
dogs.add(dog3);
this.dogs = dogs;
}
public void runDogs() {
for (Dog dog : this.dogs) {
dog.bark();
}
}
public static void main(String[] args) {
People people = new People();
people.runDogs();
}
}
在这种情况下,对象带来的便利尤其明显,但是,不应该以某个类有多个实例对象需要遍历而作为提出类的标准
结论:就算一个人只遛一条狗,只要人负责了人的责任,狗负责了狗的责任,关键是狗有必要提取为一个类(比如狗有多个字段,比如有多个相关方法围绕这几个字段处理)
如果用map_list的形式
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class People {
public void modify1(List mapList) {
for(Object map:mapList){
((HashMap)map).put("sex","公");
}
}
public void modify2(List mapList) {
for(Object map:mapList){
((HashMap)map).put("name",((HashMap)map).get("name")+"xx");
}
}
public static void main(String[] args) {
List mapList=new ArrayList();
Map map1=new HashMap();
map1.put("name","小黑");
map1.put("age",1);
Map map2=new HashMap();
map2.put("name","小花");
map2.put("age",2);
Map map3=new HashMap();
map3.put("name","小黄");
map3.put("age",3);
mapList.add(map1);
mapList.add(map2);
mapList.add(map3);
People people=new People();
people.modify1(mapList);
people.modify2(mapList);
for(Object map:mapList){
String name=(String)((HashMap)map).get("name");
Integer age=(Integer)((HashMap)map).get("age");
String sex=(String)((HashMap)map).get("sex");
System.out.println("name: "+name+" age: "+age+" sex: "+sex);
}
}
}
人有一堆狗,并有处理这一堆狗的相关方法,这种形式在多个字段的时候被几个方法公用的时候,按照map_list的方式可能参数显的挺长,优点是方便测试每一个方法
这种形式把数据和算法分离了,看起来,只要是map_list的形式,map就可以提取一个类
如果修改成面向对象的形式将,如下
import java.util.HashMap;
public class Dog {
private HashMap property;
public Dog(HashMap property) {
this.property = property;
}
public void modify1() {
property.put("sex","公");
}
public void modify2() {
property.put("name",property.get("name")+"xx");
}
public void print() {
System.out.println("name: "+property.get("name")+" age: "+property.get("age")+" sex: "+property.get("sex"));
}
}
import java.util.ArrayList;
import java.util.HashMap;
public class People {
private ArrayList<Dog> dogs=new ArrayList<>();
public People() {
ArrayList<Dog> dogs=new ArrayList<>();
HashMap map1=new HashMap();
map1.put("age",1);
map1.put("name","小黑");
Dog dog1=new Dog(map1);
HashMap map2=new HashMap();
map2.put("age",2);
map2.put("name","小花");
Dog dog2=new Dog(map2);
HashMap map3=new HashMap();
map3.put("age",3);
map3.put("name","小黄");
Dog dog3=new Dog(map3);
dogs.add(dog1);
dogs.add(dog2);
dogs.add(dog3);
this.dogs = dogs;
}
public void modifyDog1(){
for(Dog dog:this.dogs){
dog.modify1();
}
}
public void modifyDog2(){
for(Dog dog:this.dogs){
dog.modify2();
}
}
public void printDogs(){
for(Dog dog:this.dogs){
dog.print();
}
}
public static void main(String[] args) {
People people=new People();
people.modifyDog1();
people.modifyDog2();
people.printDogs();
}
}
如果狗自己有方法处理自己相关的两个字段,也就说数据以及对这些数据的处理都在一个类里里面才是最好的,这样写的代码应该才是耦合度最低的,自己干自己的事
纯数据类
public class Dog {
private String name;
private int age;
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
不能说这种类没有价值,至少在ORM映射数据库的时候,就没问题。
我的问题是自己写代码的时候是否使用这样的类,如果用的话会带来什么样的效果,我觉得不反对,但是尽量还是让数据和对数据处理的方法在一起吧,不然方法执行的时候肯定需要数据,实际上也变成上面的map_list形式了,原则就是既然你用的是遍历每一个map然后执行什么,那么不如将方法加入到每个map代表的对象上面
JSON
在Web开发中常常需要构造出一种复杂的成为JSON的复合数据结构,构造不是问题,但是构造出来之后,又对其中的数据进行处理,那么就陷入了数据+算法的面向过程方式,实际上这就和上面map_list一样的问题,在进行这样的设计之前不妨想想能不能更加面向对象的方式,上面说到遍历每一个map然后执行什么,那么不如将方法加入到每个map代表的对象上面,那么对与list_list的数据,对一个每个list也是同样道理
CSV
CSV作为一种平面二维表格,非常常见,可以参考下面的原则
- 如果只是对每行进行处理,或者行与行比较,那么可以将行提取为类,相关处理提取为方法
- 如果只是对每列进行处理,或者列与列比较,那么可以将列提取为类,相关处理提取为方法
- 如果即对每行处理比较,又对每列处理比较,那么没必要对行或者列提取类,就用list_list或者map_list,map里面又套了list的处理二维数据的方式处理吧
推论:需要对某一组常在一起的数据进行处理,比较,那么将这一组常在一起的数据和相关的操作封装为类
纯方法类
public class Strategy {
public void method(){
System.out.println("Hello World");
}
}
由于Java是静态语言,在需要把方法当参数传递的时候,这样是不错的,比如策略设计模式
小全局变量
对于一些规模较小的问题,将其分解为过程的开发方式比较理想。而面向对象更加适用于解决规模较大的问题。要想实现一个简单的Web浏览器可能需要大约2000个过程,这些过程可能需要对一组全局数据进行操作。采用面向对象的设计风格,可能只需要大约100个类,每个类平均包含20个方法。后者更加易于程序员掌握,也容易找到bug。假设给定对象的数据出错了,在访问这个数据项的20个方法中查找错误要比在2000个过程中查找容易得多。
那么问题变成,为什么可能需要全局变量?
- 如果不用全局变量,在多个方法针对同一组变量的处理上,需要每个方法都传递参数,导致参数长度很长
- 多个返回值的情况,如果没有全局变量,那么可能需要引用传递(好吧,这里全局变量战胜了引用传递)
那么也就是说类的作用是让全局变量变成小全局变量,只将相关的变量和方法放在一个类中,即全局变量不是洪水猛兽,只是控制在合适的范围中就把缺陷控制在接受的范围内,而利用其优点
间隔调用
假设数据1,经过方法1处理,然后并不是紧接着用方法2继续处理它,而是进行了别的任务,之后才用方法2来处理数据1
public class Dog {
private String name;
private Integer age;
public Dog(String name, Integer age) {
this.name = name;
this.age = age;
}
public void bark() {
System.out.println("Dog{" +
"name='" + name + '\'' +
", age=" + age +
'}' + "汪汪汪...");
}
public void sleep() {
System.out.println("Dog{" +
"name='" + name + '\'' +
", age=" + age +
'}' + "睡觉...");
}
public void fight() {
System.out.println("Dog{" +
"name='" + name + '\'' +
", age=" + age +
'}' + "打架...");
}
}
public class Story {
private String time;
private String context;
public Story(String time, String context) {
this.time = time;
this.context = context;
}
public void tell(){
System.out.println("Story{" +
"time='" + time + '\'' +
", context='" + context + '\'' +
'}');
}
}
import java.util.ArrayList;
public class People {
private Dog dog=new Dog("小黑",2);
private ArrayList<Story> stories=new ArrayList<>();
public People() {
initStories();
}
private void initStories(){
for(int i=1;i<5;i++){
Story story=new Story("time"+i,"Story"+i);
this.stories.add(story);
}
}
public void play_object_oriented(){
dog.bark();
stories.get(0).tell();
dog.sleep();
stories.get(1).tell();
dog.fight();
stories.get(2).tell();
}
public void play_process_oriented(){
String name="小黑";
Integer age=2;
ArrayList<Story> stories=new ArrayList<>();
for(int i=1;i<5;i++){
Story story=new Story("time"+i,"Story"+i);
stories.add(story);
}
System.out.println("Dog{" +
"name='" + name + '\'' +
", age=" + age +
'}' + "汪汪汪...");
stories.get(0).tell();
System.out.println("Dog{" +
"name='" + name + '\'' +
", age=" + age +
'}' + "睡觉...");
stories.get(1).tell();
System.out.println("Dog{" +
"name='" + name + '\'' +
", age=" + age +
'}' + "打架...");
stories.get(2).tell();
}
public static void main(String[] args) {
People people=new People();
people.play_object_oriented();
System.out.println();
people.play_process_oriented();
}
}
play_object_oriented展示了面向对象的效果
play_process_oriented展示了面向过程的效果
要完成这样效果在面向对象过程中,只能让数据1,对方法1,方法2,以及别的任务全局可见,而在面向对象中,对象调用了方法1,至于方法2,想啥时候调用就啥时候调用,代码更加清晰(虽然相比面向过程多了类的提取,但是不能说现代生活因为高科技让生活变的更难,这里的代码太简单想的夸张点比如参数20+方法10+,然后也就会觉得掌握额外的现代高科技相比原始社会完成相同的任务容易多了)
在自然语言中,比如写小说随着故事情节前进,实际上中间就用了很多提取的对象(比如故事人物),而对象(人物)进行某些事情,完全就是间隔调用,又一次机器语言和自然语言不谋而合,也许正是基于这一点面向对象的程序让混为一谈的面向过程程序变得容易控制了
副作用
面向过程的方式:
public class Dog {
public void bark() {
System.out.println("汪汪汪...");
}
}
public class People {
public static void main(String[] args) {
String status="Sad";
System.out.println(status);
Dog dog = new Dog();
dog.bark();
status="Happy";
System.out.println(status);
}
}
面向对象的方式如下:
public class Dog {
private String status = "Sad";
public String getStatus() {
return status;
}
public void bark() {
System.out.println("汪汪汪...");
this.status = "Happy";
}
}
public class People {
public static void main(String[] args) {
Dog dog = new Dog();
System.out.println(dog.getStatus());
dog.bark();
System.out.println(dog.getStatus());
}
}
可以发现面向过程的方式,明明和这个方法相关的改变,然而调用这个方法根本没有改变,而面向对象的方式就完全体现了某个方法执行之后引起的变动
间隔调用和副作用的共同点都是基于一个对象有多个不同的方法可供调用
小全局变量 | 间隔调用 | 副作用 |
---|---|---|
侧重在于,在类里的各个实例方法可以享有字段这个类内全局变量 | 侧重在于,一个对象的各个对外公开的方法之间没有特殊的关系,在需要的地方就可以调用想要的方法 | 侧重在于,一个方法调用,潜在的改变了对象的状态 |
以上三个特性都是用使用面向对象的理由
返回多个值
如果需要返回多个值可以通过下面几种方式解决:
- 将结果存在集合中返回,比如map
- 将结果存在自定义的数据类对象中返回
- 引用传递
如果存在第二种情况,那么也许这个方法本身就应该属于哪个类,于是演变成了Method Object重构手法
可能提取类的因素
- 几个方法都在围绕一个概念处理
- 差不多的参数被多个方法传递使用
- 差不多的参数被多个方法引用传递
- 差不多的参数传给外层方法1,有的部分继续传递给内层方法2
- 方法需要返回多个返回值
- 字段中老是有几个字段紧密相关
- 方法参数中是有几个参数紧密相关
- 方法中老是需要使用别的类提供的数据
- map_list形式的代码
结论:如果以上特征的代码都面向对象之后,那么代码将从一个大王国,变成多个小王国(这些小王国不一定是平级的,可能小王国里面又套小王国,就好比100行的代码面向过程的方式分解,也不会是每10行提取一个方法,而是从代码意义(某一段进行了某项处理,返回了什么)上提取,同理提取类也是以意义(某一段代码,或者某几项数据,或者某几个方法围绕归属一个概念)提取
如果文本分析类只有一个动词而已
这样的结果就是没有多个方法围绕数据在处理,也就说不能利用小全局变量的能力,直接使用面向过程的方法就能完成,那就用面向过程的方法吧,等问题逐渐变得复杂,比如满足上面的特征的时候,再升级为类
单元测试
在面向过程的方法中,一个方法就是一个整体,可能有输入参数,然后进过某些算法处理,然后可能有输出,在单元测试中直接调用方法,再期望和实际比较
而在面向对象中,一个类才是一个整体,要测试那么就对整个类为整体进行测试,首先需要实例化这个类的对象,然后再调用对象的方法,再期望和实际比较,之所以需要这样是因为类中的某个被测方法不是孤立的方法,可能其中使用了字段,而这个字段是经过别的方法处理后得到的
结论:从单元测试的角度来说,测试一个面向过程的方法比测试一个类容易,有一种感觉是面向过程挺笨(简单不用动脑子,但是代价随着问题规模变大,越难前进),但是胜在步步为营,逢山开路,遇水搭桥,而面向对象需要分析设计,至少当越来越艰难的时候不妨回头看看
付出与收益曲线
面向过程是起点低,复杂性增长率高的二次曲线(小规模的时候优于面向对象,而大规模收益锐减)
面向对象是起点高,复杂性增长率低的常数曲线(小规模的时候就是杀鸡用牛刀,不过随着问题规模增加胜在稳定)
于是编码需要结合面向对象与面向过程,将大问题以面向对象的方式将代码分解到较小的类,再了类中用面向过程的方式过程的方式解决问题
为什么面向过程的程序就没有这种建模抽象的能力
语法机制被限制了,从真实世界(自然语言),要建模某一个东西,那么这个东西必然即有数据,又有行为
面向过程将数据和算法分离,整个程序就是多个方法,比较散
面向对象将数据和相关的算法结合在一起,程序在局部区域高内聚
从让编码容易这个角度,面向对象是面向过程的高阶形式,在单个类里面离不开面向过程,如果说面向过程中提取方法是一次巨大进步,那么面向对象的提取类又是一次进步
另一个角度
比如写代码写着写着,觉得如果这个地方有一个什么样的功能就好了,那么,自然而然最先想到的是提取方法,即传给方法参数,处理之后返回期望的值,但是如果出现如下情况,那么就可能有必要将这个方法升级为类:
- 将参数传个了这个方法,这个方法又进一步提取了方法,而且参数进一步进行了传递
- 这个方法不是一个孤立的方法,1)比如ArrayList,如果在类外声明数组再传入,然后add,需要得到元素个数,则算数组的长度(副作用)。2)比如一个循环体中的方法,循环体外有个计数器(变相副作用)
Python
作为动态语言的Python有的不需要类就能解决的问题,在静态语言比如Java中,必须用类才能实现同样的功能,比如
- Python中方法可以作为参数传递,在Java中需要通过将方法放在对象里,然后传递对象
- Python方法可以返回多个值,在Java中只能返回一个值,要不将返回结果封装到一个map类集合里面,或者自定义纯数据类里面,或者将这个方法提取为一个类,或者引用传递
- Python中可以动态的给对象增加属性,Java中需要使用装饰器设计模式来达到类似的效果
结论
何时提取类,或者说遛狗的精髓在于狗有多么复杂(多个变量被多个方法使用)
面向对象的思想就好像学习打字,刚学习打字觉得很难,但一旦熟练就自然而然熟能生巧,越是熟练面向对象的付出就越低,而收益则越高
这就是本教程关于面向对象分析与设计的所有技术,欢迎评论/反馈。
Happy Learning !!