我习惯于使用函数式编程(主要是Haskell),我从OO(scala)开始。
我在翻译代码时遇到了麻烦。例如,那是我对H树的B树定义:
data BTree a =
Leaf
|Node2 (BTree a) a (BTree a)
|Node3 (BTree a) a (BTree a) a (BTree a)
deriving (Eq,Read,Show)
这很简单。我的树是空的,或者它有一个值,是两棵树的父亲,或者它是3棵子树的父亲。
OO是什么?我没有线索。我无法弄清楚如何以理智的方式做到这一点。
答案 0 :(得分:18)
这里有一些好的答案,但我认为他们都错过了展示你所缺少点的机会。所以,你已经证明了这一点:
data BTree a =
Leaf
|Node2 (BTree a) a (BTree a)
|Node3 (BTree a) a (BTree a) a (BTree a)
deriving (Eq,Read,Show)
并询问如何以面向对象的方式实现它。所以,这是:
最重要的事情
trait Tree[A] {
// not required because it is inherited from AnyRef
// def equals(other: Any): Boolean
// not required because it is inherited from AnyRef
// def toString: String
// does not belong in the object
// def fromString(input: String): Tree[A]
// Stuff that is missing but is needed
def isEmpty: Boolean
def value: Option[A]
def subtrees: Seq[Tree[A]]
def iterator: Iterator[A]
def depthFirstIterator: Iterator[A]
def breadthFirstIterator: Iterator[A]
}
所以,这是交易:当你谈到面向对象时,你有一个BTree,一个手指树,或者其他任何无关的树结构。实际上,它应该隐藏。相关的是您可以做的内容。
您在执行此操作时遇到了麻烦,因为您正在从正确的方向接近问题。
不那么重要的事情
sealed abstract class BTree[A] extends Tree[A]
object BTree {
def apply[A](input: String): BTree[A] = { /* factory */ null.asInstanceOf[BTree[A]] }
private case object Leaf extends BTree[Nothing] {
// method implementation
}
private case class Node2[A](value: A, private one: BTree[A], private two: BTree[A]) extends BTree[A] {
// method implementation
}
private case class Node3[A](value: A, private one: BTree[A], private two: BTree[A], private three: BTree[A]) extends BTree[A] {
// method implementation
}
}
现在,您实际上提供了一个实现,但BTree的详细信息是完全隐藏的。您只能使用Tree
定义的方法。
这是理想的面向对象架构:客户端依赖于接口,数据结构是隐藏的。
答案 1 :(得分:17)
这是从功能性思维模式中获取OO的第一步:对象更像是函数而不是数据。将它们视为这样;而不是功能作用于透明的结构化数据,现在你有不透明的抽象行为。
从“好吧,这是我的数据的结构,现在我......”的角度来看,它正朝着它的方向发展。
尝试这样的事情:
首先弄清楚你的B树可以采取哪些基本行动(不要忘记show
和fmap
这里的事情)并根据那些。
对于类似树的和类,可以更容易地将基类留空,并在数据构造函数的不同变体上使用子类。作为OO的经验法则,您必须做出某种选择以大幅改变后续行为,强烈考虑使用子类型多态来区分案例。
在必要之前尽量不要担心内部表示,并且不要让表示详细信息从类中泄漏出来。拥有一堆返回原始类型的GetFoo()方法是“做错了”的标志。
最后:记住你正在使用Scala。出于某种原因,这是一种混合语言;在OO风格中,并非一切都有意义。翻阅设计模式书,您会发现其中一半是关于缺少语言功能的巴洛克式高维护性解决方法。
答案 2 :(得分:12)
由于您的标记列表中包含Scala,因此以下是在Scala中完成的方法:
你有一个基本特征(在Haskell中的类型),并从所有Haskell构造函数派生为case
类。这样你也可以在Scala模式匹配中使用它们。
sealed trait Tree[+A]
case object Leaf extends Tree[Any]
case class Node2[+A](a: A, t1: Tree[A], t2: Tree[A]) extends Tree[A]
case class Node3[+A](a: A, b: A, t1: Tree[A], t2: Tree[A], t2: Tree[A]) extends Tree[A]
在Java(自1.5),C ++和C#等语言中,您可以使用相同类型的模板来帮助键入安全性。它们基本上像Haskell中的类型变量一样工作。
此示例位于Scala中,但对于其他OO语言,您可以采用类似的方式执行此操作:创建抽象基类并将数据的构造函数转换为类/对象。
答案 3 :(得分:3)
定义“理智”。实际上,这很酷:这是我第一次看到有人在从功能到OO而不是其他方面遇到麻烦。
事实是,你将有更多的东西去做OO;这是功能很好的一个原因。你正在遇到功能有优势的特定情况。
在OO语言中(这不是任何特定语言,只是伪代码)你将需要一个节点类
class Node
children : array of Node;
end
然后你有方法,例如,将一个节点添加为一个孩子,这样你就可以用它来做事。
然后使用Node创建一个BTree类来进行插入,平衡等。
答案 4 :(得分:3)
好的,休克疗法的时间:Java。您的类型BTree成为类层次结构的顶部。没有类型类,你会覆盖equals和toString方法(不过没有等效的Read)。然后将所有函数放在对象中,作为方法(通常是BTree中的抽象版本,以及子类中的具体版本)。因为为每个Leaf使用一个新实例会很浪费,所以我们欺骗重用一个静态字段(使用匿名类初始化)(我们再次通过省略泛型类型来欺骗,因为Java没有“底部”)像Scala的Nothing)。当然这只是一个非常粗略的草图,没有错误处理等等。是的,它变得非常冗长,所以如果你可以使用Scala,那就高兴了......
public abstract class BTree<A> {
public static final BTree LEAF = new BTree {
//concrete implementations of the methods
public boolean isEmpty(){ return true; }
public String toString() { return ""; }
}
public abstract boolean isEmpty();
//concrete methods that are the same for all sub-classes
//abstract methods that are different
}
public class Node2<A> {
private BTree<A> left;
private BTree<A> right;
private A a;
public Node2(BTree<A> left, A a, BTree<A> right) {
this.left = left;
this.a = a;
this.right = right;
}
public String toString() {
return "(" + left + a + right + ")";
}
public boolean equals(Object o) {
if (o instanceof Node2) {
Node2<A> n2 = (Node2<A>) n2;
return a.equals(n2.a) && left.equals(n2.left) && right.equals(n2.right);
}
return false;
}
public boolean isEmpty(){ return false; }
//other concrete methods
}
//same for Node3
答案 5 :(得分:1)
我不知道Scala,但我知道Java。
在Java和对象中,通常会对特定事物进行建模,例如:汽车或B树等。
对象将存储有关此事物的数据(信息)。对象还将具有可以针对数据执行的行为(例如:打开车门),这通常会改变事物的状态(更改数据)。行为(方法)也可能只是告诉我们有关对象状态的信息,而不是改变状态。此外,内部数据的确切形式通常是隐藏的(通过良好实践)。
现在,在任何时间点,对象都将具有精确状态。
因此,如果我们考虑一个二叉树对象。我们可能有一个二进制树(包含整数),看起来完全像这样:
4
/ \
2 1
/ / \
1 3 1
因此,在任何时间点,它都会有一定数量的节点具有某些值,并以某种方式附加。
现在我们需要决定如何存储有关二叉树的信息。这将在每个二叉树对象内部完成。
因此我们知道每个二叉树将由一些节点组成。
因此我们需要一些方法来存储有关节点的信息。现在节点我们需要存储他们拥有的值以及他们拥有的左/右孩子。因为它们包含多条信息,所以我们需要将它们存储为对象。
因此每个节点对象都需要有值的变量,一个变量告诉我们左边的孩子是什么(如果有的话),还有一个变量是正确的孩子。
因此,对于包含整数值的节点,我们可以:
class Node {
int value;
Node left;
Node right;
}
现在并非所有节点都有左或右子节点(或任何子节点)。缺少左子项由左变量表示,其值为'null'而不是指实际节点。
上述代码通常表示一个节点,但不包含特定的状态信息,但我们创建的特定节点将具有“值”,“左”和“右”变量的特定值。
现在我们知道二叉树由许多节点组成。它以根节点开始。 然后根节点将只包含有关它下面的节点(其子节点)的信息。
class BinaryTree {
Node root;
}
但是我们也想给我们的二叉树(即对象)一些行为,这样我们就可以用它们做一些有趣的事情。毕竟,为什么我们甚至想要一个二叉树 - 所以我们可以做一些有用的东西!
首先我们需要一个“构造函数”,这样我们就可以创建一个二叉树对象,并将它的状态设置为一些初始值。所以我们只是让二进制树开始为空。我们通过让'root'变量为null来表示这一点。这只是意味着我们甚至没有根!所以它是空的。我们以与方法(属于类/对象的函数)相同的形式编写构造函数,除了我们给它与类本身相同的名称:
class BinaryTree {
Node root;
BinaryTree() {
root = null; // make it so that newly made objects start off being empty
}
}
我们可能希望为二进制树对象提供一些行为/方法,以便我们可以实际构建我们想要的任何类型的二叉树。只有能够制作空树而不改变它们可能没有用!
因此我们可以制作addLeftChild(Node addFrom,int value)和addRightChild(Node addFrom,int value)方法。 addLeftChild将创建一个具有给定值(并且没有子节点)的新节点,并使其成为addFrom给出的节点的左子节点,但前提是'addFrom'节点还没有左子节点。
class BinaryTree {
Node root;
BinaryTree() {
root = null; // make it so that newly made objects start off being empty
}
Node addLeftChild(Node addFrom, int value) {
// change the binary tree's state somehow to achieve this
}
Node addRightChild(Node addFrom, int value) {
// change the binary tree's state somehow to achieve this
}
}
我们可能还有一个添加新根节点addRoot(int value)的方法,因此我们可以在第一次创建二叉树时添加根节点。 您可能还有方法(行为)来删除节点。您可能有方法在树中搜索值/节点,或者为您提供有关树的信息(例如:深度,节点数)。
然后我们可能会写一些代码来实际制作二叉树对象,以某种方式与它们交互,例如:
// this is some main method,etc
BinaryTree ourBT = new BinaryTree(); // make an new binary tree
// remember these start off empty
Node rootNode; // variable so we can tell
// later add methods which node to add from
rootNode = ourBT.addRoot(4);
这将为我们提供这个名为ourBT(只是根节点)的二叉树
4
然后我们可能会去:
ourBT.addLeftChild(rootNode, 3); // remember the parameter rootNode refers
// to the root node we just added before
会使我们的二叉树处于这种状态:
4
/
3
然后我们可以去:
ourBT.addRightChild(rootNode, 1);
会使我们的二叉树处于这种状态:
4
/ \
3 1
然后在构建我们的二叉树后,我们可能会用它做一些其他有趣的事情(例如:搜索,删除)
这可能不是最好的例子,但希望我能够深入了解自定义数据结构是如何以OO风格编写的。