Special Case (Особый Случай)
Паттерн проектирования Special Case
Описание Special Case
Подкласс, содержащий особую логику для отдельных ситуаций.
Null-значения в ООП - неуклюжая вещь, так как она зарубает на корню полиморфизм. Обычно есть возможность вызова какого-либо метода на переменной заданного типа без необходимости беспокоиться о том, принадлежит ли эта переменная конкретному классу или подклассу. В строго типизированных языках эту проверку делает компилятор. Тем не менее, из-за того, что переменная может содержать null, существует опасность возникновения ошибки во время выполнения, при вызове метода на null-значении.
Если переменная может принимать значение null, вам нужно постоянно заботиться о проверках на null и правильной обработке null-значений. Часто, эта "правильная обработка" одинакова в большинстве случаев, и всё это заканчивается совершением греха дублированием кода (дословный перевод Мартина Фаулера).
Null-значения - яркий пример таких проблем, которые возникают постоянно и внезапно. А их много. Например, во многих системах приходится работать с бесконечностью, которая имеет особые правила для, например, сложения и нарушает обычные аксиомы, справедливые для натуральных чисел. Такие случаи предполагают изменение обычного поведения типа.
Вместо того, чтобы возвращать null или какое-то дополнительное значение, верните Special Case (Особый Случай) - объект с тем же интерфейсом, но ведущий себя иначе, чем основной.
Примеры реализации
// Special Case Pattern in JavaScript
class User {
constructor(id, name, email) {
this.id = id;
this.name = name;
this.email = email;
}
getName() {
return this.name;
}
getEmail() {
return this.email;
}
isNull() {
return false;
}
}
class NullUser {
getName() {
return 'Guest';
}
getEmail() {
return 'guest@example.com';
}
isNull() {
return true;
}
}
class UserService {
constructor() {
this.users = new Map();
this.users.set(1, new User(1, 'John Doe', 'john@example.com'));
this.users.set(2, new User(2, 'Jane Smith', 'jane@example.com'));
}
findById(id) {
const user = this.users.get(id);
return user || new NullUser();
}
}
// Usage
const userService = new UserService();
const user1 = userService.findById(1);
const user2 = userService.findById(999); // Non-existent user
console.log(user1.getName()); // John Doe
console.log(user1.isNull()); // false
console.log(user2.getName()); // Guest
console.log(user2.isNull()); // true
<?php
// Special Case Pattern in PHP
class User {
private $id;
private $name;
private $email;
public function __construct($id, $name, $email) {
$this->id = $id;
$this->name = $name;
$this->email = $email;
}
public function getName() {
return $this->name;
}
public function getEmail() {
return $this->email;
}
public function isNull() {
return false;
}
}
class NullUser {
public function getName() {
return 'Guest';
}
public function getEmail() {
return 'guest@example.com';
}
public function isNull() {
return true;
}
}
class UserService {
private $users;
public function __construct() {
$this->users = [
1 => new User(1, 'John Doe', 'john@example.com'),
2 => new User(2, 'Jane Smith', 'jane@example.com')
];
}
public function findById($id) {
return $this->users[$id] ?? new NullUser();
}
}
// Usage
$userService = new UserService();
$user1 = $userService->findById(1);
$user2 = $userService->findById(999); // Non-existent user
echo $user1->getName() . "\n"; // John Doe
echo $user1->isNull() ? 'true' : 'false' . "\n"; // false
echo $user2->getName() . "\n"; // Guest
echo $user2->isNull() ? 'true' : 'false' . "\n"; // true
?>
// Special Case Pattern in Go
package main
import "fmt"
type User interface {
GetName() string
GetEmail() string
IsNull() bool
}
type RealUser struct {
ID int
Name string
Email string
}
func (u RealUser) GetName() string {
return u.Name
}
func (u RealUser) GetEmail() string {
return u.Email
}
func (u RealUser) IsNull() bool {
return false
}
type NullUser struct{}
func (u NullUser) GetName() string {
return "Guest"
}
func (u NullUser) GetEmail() string {
return "guest@example.com"
}
func (u NullUser) IsNull() bool {
return true
}
type UserService struct {
users map[int]User
}
func NewUserService() *UserService {
return &UserService{
users: map[int]User{
1: RealUser{ID: 1, Name: "John Doe", Email: "john@example.com"},
2: RealUser{ID: 2, Name: "Jane Smith", Email: "jane@example.com"},
},
}
}
func (s *UserService) FindByID(id int) User {
if user, exists := s.users[id]; exists {
return user
}
return NullUser{}
}
// Usage
func main() {
userService := NewUserService()
user1 := userService.FindByID(1)
user2 := userService.FindByID(999) // Non-existent user
fmt.Println(user1.GetName()) // John Doe
fmt.Println(user1.IsNull()) // false
fmt.Println(user2.GetName()) // Guest
fmt.Println(user2.IsNull()) // true
}
Использована иллюстрация с сайта Мартина Фаулера.