Understanding OOP Principles in passing around objects/values
I'm not quite grokking a couple of things in OOP and I'm going to use a fictional understanding of SO to see if I can get help understand.
So, on this page we have a question. You can comment on the question. There are also answers. You can comment on the answers.
Question
- comment
- comment
- comment
Answer
-comment
Answer
-comment
-comment
-comment
Answer
-comment
-comment
So, I'm imagining a very high level understanding of this type of system (in PHP, not .Net as I am not yet familiar with .Net) would be like:
$question = new Question;
$question->load($this_question_id); // from the URL probably
echo $question->getTitle();
To load the answers, I imagine it's something like this ("A"):
$answers = new Answers;
$answers->loadFromQuestion($question->getID()); // or $answers->loadFromQuestion($this_question_id);
while($answer = $answers->getAnswer())
{
echo $answer->showFormatted();
}
Or, would you do ("B"):
$answers->setQuestion($ques开发者_StackOverflow社区tion); // inject the whole obj, so we have access to all the data and public methods in $question
$answers->loadFromQuestion(); // the ID would be found via $this->question->getID() instead of from the argument passed in
while($answer = $answers->getAnswer())
{
echo $answer->showFormatted();
}
I guess my problem is, I don't know when or if I should be passing in an entire object, and when I should just be passing in a value. Passing in the entire object gives me a lot of flexibility, but it's more memory and subject to change, I'd guess (like a property or method rename). If "A" style is better, why not just use a function? OOP seems pointless here.
Thanks, Hans
While I like Jason's answer, it is not, strictly speaking OO.
$question = new Question($id);
$comments = $question->getComments();
$answers = $question->getAnswers();
echo $question->getTitle();
echo $question->getText();
foreach ($comments as $comment)
echo $comments->getText();
The problems are:
- There is no information hiding, a fundamental principle of OO.
- If the format of the answers needs to change, it must be changed in a place that is not associated with the object that houses the data.
- The solution is not extensible. (There is no behaviour to inherit.)
You must keep behaviour (tightly coupled) with the data. Otherwise you are not writing OO.
$question = new Question($id);
$questionView = new QuestionView( $question );
$questionView->displayComments();
$questionView->displayAnswers();
How the information is displayed is now an implementation detail, and reusable.
Notice how this opens up the following possibility:
$question = new Question( $id );
$questionView = new QuestionView( $question );
$questionView->setPrinterFriendly();
$questionView->displayComments();
$questionView->displayAnswers();
The idea is that now you can change how the questions are formatted from a single location in the code base. You can support multiple formats for the comments and answers without the calling code (a) ever knowing; and (b) ever needing to change (to a significant degree).
If you are coding text formatting details in more than one location because you are misusing accessor methods, the life of any future maintainers will be miserable. If the maintainer is a psychopath who knows where you live, you will be in trouble.
Objects, Data, and Views
Here's the problem, as I understand it:
Database -> Object -> Display Content
You want to keep the behaviour of the object centred around logic that is intrinsic to the object. In other words, you don't want the Object to have to do things that have nothing to do with its core responsibilities. Most commonly this will include load, save, and print functionality. You want to keep these separate from the object itself because if you ever have to change database, or output format, you want to make as few changes in the system as possible, and restrain the ripple effect.
To simplify this, let's take a look at loading only Comments
; everything is applicable to Questions
and Answers
as well.
Comment Class
The Comment class might offer the following behaviours:
- Reply
- Delete
- Update (requires permission)
- Restore (from a delete)
- etc.
CommentDB Class
We can create a CommentDB
object that knows how to manipulate the Comment
s in the database. A CommentDB
object has the following behaviours:
- Create
- Load
- Save
- Update
- Delete
- Restore
Notice that these behaviours will likely be common across all objects and can therefore be subject to refactoring. This will also let you change databases quite easily as the connection information will be isolated to a single class (the grandfather of all database objects).
Example usage:
$commentDb = new CommentDB();
$comment = $commentDb->create();
Later:
$comment->update( "new text" );
Notice that there are a number of possible ways to implement this, but you can always do so without violating encapsulation and information hiding.
CommentView Class
Lastly, the CommentView
class will be tightly coupled to a Comment
class. That it can obtain the attributes of Comment
class via accessors is expected. The information is still hidden from the rest of the system. The Comment
and its CommentView
are tightly coupled. The idea is that the formatting is kept in a single place, not scattered throughout classes that need to use the data willy nilly.
Any classes that need to display comments, but in a slightly different format, can inherit from CommentView
.
See also: Allen Holub wrote "You should never use get/set functions", is he correct?
Why pass either? What about:
<?php
$question = new Question($id);
$comments = $question->getComments();
$answers = $question->getAnswers();
echo $question->getTitle();
echo $question->getText();
foreach ($comments as $comment)
echo $comments->getText();
foreach ($answers as $answer)
{
$answer_comments = $answer->getComments();
echo $answer->getText();
foreach ($answer_comments as $comment)
echo $comment->getText();
}
Where getComments()
and getAnswers()
use $this->id
to retrieve and return an array of comment or answer objects?
You could build utility methods in the comment and answer objects that allow you to load by parent id. In which case, just taking an id as a parameter would be nice.
$question = new Question($id);
$answers = Answer::forQuestion($question->id);
$comments = Comment::forQuestion($question->id);
$ans_comments = Comment::forAnswer($answer->id); // or some way to distinguish what the parent object is.
Edit: Likely the child model (Comment or Answer in this case) doesn't need anything from the parent except and id to do db queries with. Passing in the entire parent object would be overkill. (Also, PHP has a terrible time garbage collecting objects with circular references, which might be fixed in the 5.3 series.)
Both styles are acceptable. Sometimes you only need the value, sometimes you'll need the object. In this example I would personally do something along the lines of your first example, but trivial programs like this don't tend to exist in the wild very often so maybe you want the second piece.
My rule of thumb is to do the thing in the least number of lines that still clearly demonstrates what you're attempting to do to anyone who comes after you. The overhead of most object creation vs value passing is something you'll likely never ever have to deal with on modern arch.
Adding to what @jasonbar already mentioned:
I don't know when or if I should be passing in an entire object, and when I should just be passing in a value.
It depends on the Coupling you need and the Cohesion you desire.
Passing in the entire object gives me a lot of flexibility, but it's more memory and subject to change.
PHP does not copy the object when you use it as an argument to a function. Neither do most other languages (either by default, like C# and Java, or upon explicit request, like C and C++)
To add to Dave Jarvis and jasonbar answers, I usually have DataMappers to convert between relational data and objects, instead of using an ActiveRecord approach. So, following your example, we would have these classes:
- Question
- Answer
- Comment
and their data mappers:
- QuestionMapper
- AnswerMapper
- CommentMapper
Each mapper implementing a similar interface:
- save(object) // creates or updates a record in the database (or text file, for that matter)
- delete(id)
- get(id)
Then, we would do as:
$q = QuestionMapper::get( $questionid );
// here we could either (a) just return a list of Answers
// previously eagerly-loaded by the
// QuestionMapper, or (b) lazy load the answers by
// calling AnswerMapper::getByQuestionID( $this->id ) or similar.
$aAnswers = $q->getAnswers();
foreach($aAnswers as $oAnswer){
echo $oAnswer->getText();
$aComments = $oAnswer->getComments();
foreach($aComments as $oComment){
echo $oComment->getText();
}
}
Regarding the use of things like QuestionView->render( $question ), I prefer to have Views which display the data using getters from the domain objects. If you pass a Question to a HTMLView, it will render it as HTML; if you pass it to a JSONView, then you'll get JSON-formatted content. This means that the domain objects need to have getters.
PS: We could also consider the QuestionMapper to load everything related to Questions, Answers, and Comments. Since Comments always belongs to Answers or Questions, and Answers always belong to Questions, it could make sense that the QuestionMapper loaded everything. Of course we would have to consider different strategies for lazy loading a Question's set of Answers and Comments, to avoid hogging the server.
精彩评论