面向对象设计?

6
我正在尝试学习面向对象编程,但是由于我主要有结构化编程背景(主要是C语言,但随着时间的推移还有其他语言),所以很难克服这个问题。我想写一个简单的支票登记程序来练习一下。我很快就写了一个东西(Python是一种很棒的语言),我的数据存储在一些全局变量中,并且有许多函数。我不知道是否可以通过创建一些类来封装一些数据和函数来改进这个设计,如果可以的话,如何更改这个设计。
我的数据基本上是账户列表['checking','saving','Amex'],类别列表['food','shelter','transportation']和代表交易的字典列表[{'date':xyz,'cat':xyz,'amount':xyz,'description':xzy]。每个账户都有一个相关联的字典列表。
然后我在账户级别(create-acct(),display-all-accts()等)和交易级别(display-entries-in-account(),enter-a-transaction(),edit-a-transaction(),display-entries-between-dates()等)上有函数。
用户看到账户列表,然后可以选择一个账户并查看底层交易,具有添加、删除、编辑等账户和交易的功能。
我目前在一个大类中实现所有内容,以便可以使用self.variable,而不是显式的全局变量。
简而言之,我正在尝试弄清楚是否将其重新组织成一些类会有用,如果是这样,如何设计这些类。我已经读了一些面向对象编程的书籍(最近是《面向对象思考过程》)。我认为我的现有设计很易于阅读,并且不会重复自己。
欢迎提出任何建议。

你读过什么书?你读过哪些面向对象设计的书? - S.Lott
@S.Lott,除了我提到的《对象入门》、《Effective C++》、《四人帮》之外,还有一些我暂时想不起来的书。我刚开始阅读你的在线书籍。我尝试过《Head First》系列,但并不喜欢它们。 - user194219
5个回答

7
你不需要放弃结构化编程来进行面向对象编程。代码仍然是有结构的,只是它属于对象而不是与其分离。
在传统的编程中,代码是操作数据的驱动力,导致了一种二元性(以及代码可能对错误数据进行操作的可能性)。
在面向对象编程中,数据和代码是密不可分的 - 一个对象包含数据和操作该数据的代码(尽管技术上代码(有时还有一些数据)属于类而不是单个对象)。任何想要使用这些对象的客户端代码都应该只使用该对象内部的代码。这可以防止代码/数据不匹配的问题。
对于一个记账系统,我会按照以下方式处理:
1. 低级对象是账户和类别(实际上,在会计学中,这两者没有区别,这只是Quicken等软件为了将资产负债表项目与损益表分开而制造的假分离 - 我将只称它们为账户)。一个账户对象由(例如)账户代码、名称和起始余额组成,尽管在我工作过的会计系统中,起始余额总是零 - 我总是使用“启动”交易来最初设置余额。
2. 交易是一个平衡的对象,它由一组带有相关移动(金额变化)的账户/类别组成。平衡的意思是它们必须相加为零(这是双重记账法的关键)。这意味着它是一个日期、描述和一个元素数组或向量,每个元素包含一个账户代码和价值。
3. 整个会计“对象”(分类账)只是所有账户和交易的列表。
请记住,这是系统的“后端”(数据模型)。您希望有单独的类来查看数据(视图),这将使您能够根据用户喜好轻松更改它。例如,您可能想要整个分类账、仅平衡表或仅损益表。或者您可能需要不同的日期范围。
我想强调的一件事是,要创建一个好的会计系统,您需要像一个会计一样思考。我的意思是失去“账户”和“类别”之间的人为差异,因为这将使您的系统更加清洁(您需要能够在两个资产类账户之间进行交易(例如银行转账),如果每笔交易都需要一个“类别”,这将无法实现。数据模型应反映数据,而不是视图。
唯一的困难在于记住资产类账户的符号与您预期的相反(负值表示您在银行中有钱,而您非常高的正值公司汽车贷款是一种债务,例如)。这将使双重记账法完美地发挥作用,但您必须记住在显示或打印资产负债表时反转资产类账户(资产、负债和所有者权益)的符号。

关于“代码可能在错误的数据上运行”的可能性,你知道类型系统是用来做什么的吗? - MaD70
1
那么,@ MaD70,在您的C代码中,所有类型都将被完全typedef,并且您不会在任何地方使用整数或浮点数,是吗?否则,有可能会将错误的数据传递给函数。 - paxdiablo
我不会编写C语言程序,但当然我会尽可能充分利用编程语言(PL)的类型系统,无论它是面向对象、过程式、函数式、逻辑式(或基于任何范式)。这种类型安全性是由类型系统而非PL是否为OO所保证的。 - MaD70
无论如何,类型检查器可以防止您在调用不存在的方法或将错误类型的参数传递给方法时崩溃程序。没有类型检查器,编译时(对于静态类型编程语言)或运行时(对于动态类型编程语言)将没有消息:程序只会崩溃或开始以奇怪的方式行事。 - MaD70
“在面向对象编程中,数据才是王道”这种说法是错误的。从结构上看,面向对象并不区分状态和行为,将两种类型的基本单元都同等对待为“特征”。面向对象设计的过程就是通过将这些特征聚合到类中来最小化所有状态和行为特征之间的依赖关系。 - Doug Knesek
显示剩余2条评论

5

我知道它们非常受欢迎,但我无法喜欢这些书。 - user194219

3

我的数据基本上是一个帐户列表。

账户是一个类。

"表示交易的字典"

交易似乎是一个类。你选择将其表示为一个字典。

这是你对面向对象设计的第一次尝试。关注职责和协作者。

你至少有两类对象。


3
但不要让实现细节(例如由字典表示)决定您如何建模问题。 - Mitch Wheat
如何编写协作程序?目前,主程序创建一个包含所有方法的 Application 类,并根据用户输入调用它们。我是否应该设置 Application 类来创建相应的账户和交易实例,并在它们之间进行通信调节? - user194219
@S.Lott - 我在实现方面遇到了一些困难,很可能是因为我忽略了一些显而易见的东西。如果我在一个GUI框架中,我会有一个主应用程序类,它将用户操作(如点击或按键)绑定到回调函数(或通过调用槽函数来响应信号)。换句话说,每个用户操作都会调用某个方法/函数。在我的例子中,用户从一个账户列表开始(使用某个GUI元素显示),然后点击账户并获取交易列表。在这种情况下,如何在账户类或交易类中使用呢? - user194219
@S.Lott,只是为了澄清一下,如何:创建一个名为mydata的类,它可以保存并更改数据集,然后使程序的其余部分与mydata交互,而不是直接处理数据? - user194219
@rdp:你没有完全理解。你的mydata类应该包含其中的两个其他类——账户和交易——它们是独立的类。你可以使用mydata集合来代替真正的数据库。但是账户和交易独立的类,必须以这种方式定义。 - S.Lott
显示剩余4条评论

1

在设计过程中,有许多“心态”可以采用(其中一些指向面向对象编程,而另一些则不是)。我认为最好的方法通常是从问题开始,而不是从答案开始(即,不要说,“我如何将继承应用于此”,而应该问这个系统随着时间的推移可能会发生什么变化)。

以下是一些问题的答案,这些问题可能会指向设计原则:

  • 其他人会使用这个API吗?他们可能会破坏它吗?(信息隐藏
  • 我需要在许多机器上部署它吗?(状态管理、生命周期管理
  • 我需要与其他系统、运行时、语言进行互操作吗?(抽象和标准
  • 我的性能限制是什么?(状态管理、生命周期管理
  • 这个组件所处的安全环境是什么样的?(抽象、信息隐藏、互操作性
  • 如果我使用了一些对象,我会如何构造它们?(配置、控制反转、对象解耦、隐藏实现细节

这些并不是直接回答你的问题,但它们可能会让你进入正确的思维框架来自己回答。


+1 指出先采用解决方案,然后再开始寻找要用它们解决的问题是倒过来的。 - Tom Leys
有益的心态,尤其是当我第一个问题是类是否有用,而不是如何使用面向对象编程时。 - user194219
Rdp,如果我冒犯了你,很抱歉。我的意思不是说你在将OOP作为解决方案寻找问题,而是对于那些来自其他领域的人可能会成为问题。我一直觉得OO领域非常直观,当我开始接受C#中LINQ等功能时,我也确实这样做了。我注意到Python(例如)的函数语法比OO语法更不拘泥。也许在Python中,函数实现比OO实现更清晰? - Andrew Matthews
@Andrew,如果我显得生气了,很抱歉。我的问题的一部分是关于面向对象编程是否有用,你给出了一些非常有帮助的建议来考虑面向对象编程是否有用。 - user194219

-1

与其使用字典来表示您的交易,更好的容器是来自collections模块的namedtuple。namedtuple是元组的子类,它允许您通过名称和索引号引用其项目。

由于您可能在日志列表中有数千个交易,因此最好将这些项目保持尽可能小和轻量级,以便处理、排序、搜索等操作尽可能快速和响应。与普通元组一样,namedtuple不会占用比字典更多的内存空间。namedtuple还具有保持其项目顺序的额外优点,而字典则没有。

>>> import sys
>>> from collections import namedtuple
>>> sys.getsizeof((1,2,3,4,5,6,7,8))
60
>>> ntc = namedtuple('ntc', 'one two three four five six seven eight')
>>> xnt = ntc(1,2,3,4,5,6,7,8)
>>> sys.getsizeof(xnt)
60
>>> xdic = dict(one=1, two=2, three=3, four=4, five=5, six=6, seven=7, eight=8)
>>> sys.getsizeof(xdic)
524

所以你可以看到,在一个包含八个项目的交易中,几乎节省了9倍的内存。我使用的是Python 3.1,所以你的情况可能会有所不同。


1
命名元组从2.6版本开始引入。无论如何,这更多是一个低级实现问题而不是设计问题。记住这一点很有用。 - user194219

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接