Reputation: 81
Say I have a class PhoneShop
that has a getPhone
method that needs to be backwards compatible (we have to keep the arguments that we pass the same - this is a simplified version of a code test I've been given although I've changed the names of everything and I've been told that the API of the PhoneShop class should stay the same)
So far we only have one phone type, Nokia
, and we can get it like this:
PhoneShop shop = new PhoneShop();
Phone nokia = shop.getPhone("Nokia");
nokia.call();
I want to change the Phone
class to be an interface to allow multiple classes to implement it
public interface Phone {
String getName();
void call();
}
PhoneShop getPhone
public class PhoneShop {
public Phone getPhone(String name) {
return new Nokia();
}
}
Nokia
public class Nokia implements Phone {
public String getName() {
return "Nokia";
}
public void call() {
System.out.println("Calling from my " + getName());
}
}
So far the string argument is being passed but it isn't used by the business logic but now we have a new Phone type, Samsung:
public class Samsung implements Phone {
public String getName() {
return "Samsung";
}
public void call() {
System.out.println("Calling from my " + getName());
}
public void browseInternet() {
System.out.println("Browsing internet with my " + getName());
}
}
and now I want to also be able to call getPhone like this
Phone samsung = shop.getPhone("Samsung");
samsung.call();
samsung.browseInternet();
public Phone getPhone(String name) {
return switch (name) {
case "Samsung" -> new Samsung();
default -> new Nokia();
};
}
This code won't compile because browseInternet
is not defined on the Phone
interface.
How can I let the compiler know that when I call getPhone
with the string "Samsung" I actually have an instance of Samsung?
With only two phone types I could overload the method, so that eg. when it's called with no arguments, it returns an instance of Samsung
but as I add more phone types I won't have that option. Also that's not a great API - it's not obvious to the caller.
The only way I can get it to compile so far is to cast the return type like this
Samsung samsung = (Samsung) shop.getPhone("Samsung");
I don't really like that because it's open to developer error (I could mistakenly cast it to Nokia for example, and then I'd get a runtime error.
I'd like to be able to tell the compiler, "if I call this method with this particular string then it has this return type", but I don't think you can do that(?). In which case is there a pattern (factory pattern, generics ?) that I can use? (I am a newbie to Java and OOP in general - much more familiar with JavaScript and TypeScript).
Upvotes: 0
Views: 116
Reputation: 75
P.S.: Consider reading this comment from Rogue to see if his approach doesn't meet your needs.
I don’t understand why you have created classes for phone brands, since it seems like your concern is whether your devices are simply cellphones or smartphones, thus their brand could be just an internal state. Because of this, I’ll propose two different approaches:
You can create two classes: Phone
and Smartphone
. Phones can only make calls, while smartphones can either make calls and browse the internet. In this design, Smartphone
inherit from Phone
:
public class Phone {
private String name;
protected Phone(String name) {
this.name = name;
}
public void call() {
System.out.println("Calling from my " + getName());
}
// getter, setter
}
public class Smartphone extends Phone {
public Smartphone(String name) {
super(name);
}
public void browsingInternet() {
System.out.println("Browsing internet with my " + getName());
}
}
public class PhoneShop {
public PhoneShop() {
}
public Phone getPhone(String phoneName) {
return new Phone(phoneName);
}
public Smartphone getSmartphone(String smartphoneName) {
return new Smartphone(smartphoneName);
}
}
Phone
and Smartphone
can be abstract classes that will be inherited by phone models:
public abstract class Phone {
private String name;
protected Phone(String name) {
this.name = name;
}
public void call() {
System.out.println("Calling from my " + getName());
}
// getter and setter
}
public abstract class Smartphone extends Phone {
protected SmartPhone(String name) {
super(name);
}
public void browsingInternet() {
System.out.println("Browsing internet with my " + super.getName());
}
}
Smartphone
inherits the functionalities of Phone
. Now, to simplify the example, let's assume that “Nokia” and “Samsung”, although brands, are like phone models.
public class Samsung extends Smartphone {
public Samsung(String name) {
super(name);
}
}
public class Nokia extends Phone {
public Nokia(String name) {
super(name);
}
}
To reduce the possibility of typing mistakes, you can create an Enum
to be used as argument to a factory method in PhoneShop
:
public enum PhoneManufacturer {
SAMSUNG,
NOKIA
}
public class PhoneShop {
public PhoneShop() {
}
public Phone getDevice(PhoneManufacturer manufacturer) {
switch (manufacturer) {
case NOKIA -> {
return new Nokia(manufacturer.name().charAt(0) + manufacturer.name().substring(1).toLowerCase());
}
case SAMSUNG -> {
return new Samsung(manufacturer.name().charAt(0) + manufacturer.name().substring(1).toLowerCase());
}
}
return null;
}
}
Then, whenever you want a Phone
or Smartphone
, you can call it:
PhoneShop phoneShop = new PhoneShop();
Phone nokia = phoneShop.getDevice(PhoneManufacturer.NOKIA);
Smartphone samsung = (Smartphone) phoneShop.getDevice(PhoneManufacturer.SAMSUNG);
Note that, if you want to call Smartphone
's specific methods, such as browsingInternet()
, you'll need to cast the object returned from the PhoneShop#getDevice()
, otherwise, it'll behave just like an ordinary Phone
.
Upvotes: 0
Reputation: 425278
If you need a single abstract type, create an interface with browseInternet
that extends Phone
:
interface InternetPhone extends Phone {
void browseInternet();
}
That the Samsung would implement and check the type at the usage point:
if (phone instanceof InternetPhone browser) {
browser.browseInternet();
}
If you can separate the abstractions, which is cleaner, don't extend Phone
:
interface Browser {
void browseInternet();
}
And have Samsung extend both:
class Samsung implements Phone, Browser {
The usage code would be the same.
An aside: If you want getName()
to return the name of the class (like all of your examples do), you can implement that in the interface definition using the default
keyword:
public interface Phone {
default String getName() {
return getClass().getSimpleName();
}
void call();
}
Upvotes: 0
Reputation: 330
You have defined Phone as something that cannot browse the internet, hence, when you manipulate your objects as Phones, you cannot browse the internet.
Polymorphism will allow you to manipulate Samsung and Nokia objects as Phones for the sake of abstraction, if you want to use their behaviors as concrete implementation, you have to explicitly do so (Java is a strongly typed language).
Now if you want to avoid casts because of reliability (rightfully), another way is to test that a Phone is in fact a Samsung phone before calling a Samsung behavior :
if (samsung instanceof Samsung concreteSamsungPhone) {
concreteSamsungPhone.browseInternet();
}
this test will automatically use pattern matching to map your Phone to the concrete implementation of a Samsung if it is actually a Samsung (this feature is only available in recent versions of java though).
btw you are intuitively using a Factory Pattern :)
Upvotes: 0